# DRY.rb + Sorbet - Can we unite?

**URL:** <https://discourse.dry-rb.org/t/dry-rb-sorbet-can-we-unite/961>\
**Category:** Ideas\
**Created:** [December 4, 2019, 10:30pm UTC](https://discourse.dry-rb.org/t/dry-rb-sorbet-can-we-unite/961 "2019-12-04T22:30:52Z")\
**Posts on this page:** 9\
**Page:** 1

<div class="post-metadata">

**Author:** ![ianks](https://yyz1.discourse-cdn.com/flex031/user_avatar/discourse.dry-rb.org/ianks/32/449_2.png) [@ianks](https://discourse.dry-rb.org/u/ianks)\
**Post date:** [December 4, 2019, 10:30pm UTC](https://discourse.dry-rb.org/t/dry-rb-sorbet-can-we-unite/961/1 "2019-12-04T22:30:52Z")

</div>

### Problem

dry-types and Sorbet both independently solve the problem of runtime type checking. Both are able to allow for type assertions at runtime in their own way. However, there are some major pros and cons to each one.

### Pros + Cons

| dry-types | sorbet-runtime |
| --- | --- |
| + nicer api | + sigs allow for static checks |
| + already widely adopted | - T::Struct is not as nice as Dry::Struct (worse API) |
| + offers advanced functionality | - runtime checks are too simple (i.e. can’t define coercions, or other constraints) |
| - does not allow for static type checking | + lots of momentum, being adopted at Stripe + Shopify |

### What do we do?

I think we need to address the issue, and see if there is a way to make dry + sorbet play nicely together. Otherwise, users will be left with a choice:

> Should I use dry-types or Sorbet on this project? Managing two sets of type declarations is too much overhead.

Let’s see if we can come up with a way to make Sorbet infer static information from dry-types in a smooth way, so no one ever has to choose between dry or sorbet. 😄

### GitHub Issue on Sorbet

> <https://github.com/sorbet/sorbet/issues/2270>
>
> \### Problem
> 
> We have a few large codebases which have been "runtime-typed" usi…ng the \[DRY.rb\](https://dry-rb.org) family of gems for a while now. All of the necessary type information used in our apps has been declared already.
> 
> For example, we have typed structs with attributes defined like:
> 
> \`\`\`ruby
> class User \< Dry::Struct
> attribute :name, Types::Strict::String
> attribute :age, Types::Strict::Integer
> end
> \`\`\`
> 
> Ideally, it would be simple to communicate this type information to \`sorbet\`. There are currently two options, both of which are fairly painful:
> 
> 1. Convert to using \`T::Struct\`. While possible, this is an incredibly tedious solution and goes against the principles of gradual typing, because it would require a lot of rewriting.
> 
> 2. Use a plugin. This one is challenging as it operates on the text level of RBI. It requires parsing the source and printing RBI definitions, which is error-prone.
> 
> In an ideal world, I would be able to add \`sorbet\` to my Gemfile and get static checks for these types out of the box, and rely on \`dry-types\` and \`dry-struct\` to handle the runtime checks.
> 
> \### Proposed solution
> 
> I do not know what the best solution would be, but there are some things we can use to make this much easier.
> 
> 1. Use AST information from gems that provide them:
> 
> \`\`\`sh
> dev(main)\> Types::String.to\_ast
> =\> \[:nominal, \[String, {}\]\]
> \`\`\`
> 
> 1. Ability to detect classes for custom type generation, i.e. if we encounter something which responds to \`to\_ast\`, or inherits from a certain class, be able to pass along the type information
> 
> \### Summary
> 
> These are just some loose thoughts I've had when adding sorbet to our project. Overall, I think this project has huge potential and am very excited for Ruby's typed future. Thank you!

---

<div class="post-metadata">

**Author:** ![drselump14](https://yyz1.discourse-cdn.com/flex031/user_avatar/discourse.dry-rb.org/drselump14/32/532_2.png) [@drselump14](https://discourse.dry-rb.org/u/drselump14)\
**Post date:** [June 23, 2020, 11:29pm UTC](https://discourse.dry-rb.org/t/dry-rb-sorbet-can-we-unite/961/2 "2020-06-23T23:29:43Z")

</div>

Would love the feature, Is there any possibility to replace dry-types with sorbet-runtime?

---

<div class="post-metadata">

**Author:** ![jikumaru](https://avatars.discourse-cdn.com/v4/letter/j/ba9def/32.png) [@jikumaru](https://discourse.dry-rb.org/u/jikumaru)\
**Post date:** [October 2, 2022, 3:58am UTC](https://discourse.dry-rb.org/t/dry-rb-sorbet-can-we-unite/961/3 "2022-10-02T03:58:23Z")

</div>

Currently I’m trying to write a [tapioca compiler](https://github.com/Shopify/tapioca/blob/main/README.md#writing-custom-dsl-compilers) for dry-struct.

[https://github.com/YukiJikumaru/dry\_tapioca\_compiler](https://github.com/YukiJikumaru/dry_tapioca_compiler)

This app generates sorbet’s rbi files.

From this ruby code

```auto
require 'dry-types'
require 'dry-struct'

class ExampleBuiltin < ::Dry::Struct
  module Types
    include ::Dry.Types()
  end

  Dry::Types.load_extensions(:maybe)
  Dry::Types.load_extensions(:monads)

  attribute :builtin01, Types::Nominal::Any
  attribute :builtin02, Types::Nominal::Nil
  attribute :builtin03, Types::Nominal::Symbol
  attribute :builtin04, Types::Nominal::Class
  attribute :builtin05, Types::Nominal::True
  attribute :builtin06, Types::Nominal::False
  attribute :builtin07, Types::Nominal::Bool
  attribute :builtin08, Types::Nominal::Integer
  attribute :builtin09, Types::Nominal::Float
  attribute :builtin10, Types::Nominal::Decimal
  attribute :builtin11, Types::Nominal::String
  attribute :builtin12, Types::Nominal::Date
  attribute :builtin13, Types::Nominal::DateTime
  attribute :builtin14, Types::Nominal::Time
  attribute :builtin15, Types::Nominal::Array
  attribute :builtin16, Types::Nominal::Hash
end

```

to this rbi file

```auto
# types: strong
class ExampleBuiltin
  sig { returns(::T.untyped) }
  def builtin01; end

  sig { returns(::NilClass) }
  def builtin02; end

  sig { returns(::Symbol) }
  def builtin03; end

  sig { returns(::Class) }
  def builtin04; end

  sig { returns(::T::Boolean) }
  def builtin05; end

  sig { returns(::T::Boolean) }
  def builtin06; end

  sig { returns(T::Boolean) }
  def builtin07; end

  sig { returns(::Integer) }
  def builtin08; end

  sig { returns(::Float) }
  def builtin09; end

  sig { returns(::BigDecimal) }
  def builtin10; end

  sig { returns(::String) }
  def builtin11; end

  sig { returns(::Date) }
  def builtin12; end

  sig { returns(::DateTime) }
  def builtin13; end

  sig { returns(::ActiveSupport::TimeWithZone) }
  def builtin14; end

  sig { returns(::Array) }
  def builtin15; end

  sig { returns(::T::Hash[::T.untyped, ::T.untyped]) }
  def builtin16; end
end

```

Before long, I want to push a pull request to tapioca.

---

<div class="post-metadata">

**Author:** ![jikumaru](https://avatars.discourse-cdn.com/v4/letter/j/ba9def/32.png) [@jikumaru](https://discourse.dry-rb.org/u/jikumaru)\
**Post date:** [October 2, 2022, 2:24pm UTC](https://discourse.dry-rb.org/t/dry-rb-sorbet-can-we-unite/961/4 "2022-10-02T14:24:12Z")

</div>

My α version app generates rbi files below.

# example\_builtin.rb

| INPUT : dry declaration | OUTPUT : rbi return type |
| --- | --- |
| attribute :x, Types::Nominal::Any | ::T.untyped Types::Nominal::Nil |
| attribute :x, Types::Nominal::DateTime | ::DateTime |
| attribute :x, Types::Nominal::Time | ::Time |
| attribute :x, Types::Nominal::Array | ::Array |
| attribute :x, Types::Nominal::Hash | ::T::Hash[::T.untyped, ::T.untyped] |
| attribute :x, Types::Strict::Nil | ::NilClass |
| attribute :x, Types::Strict::Symbol | ::Symbol |
| attribute :x, Types::Strict::Class | ::Class |
| attribute :x, Types::Strict::True | ::T::Boolean |
| attribute :x, Types::Strict::False | ::T::Boolean |
| attribute :x, Types::Strict::Bool | ::T::Boolean |
| attribute :x, Types::Strict::Integer | ::Integer |
| attribute :x, Types::Strict::Float | ::Float |
| attribute :x, Types::Strict::Decimal | ::BigDecimal |
| attribute :x, Types::Strict::String | ::String |
| attribute :x, Types::Strict::Date | ::Date |
| attribute :x, Types::Strict::DateTime | ::DateTime |
| attribute :x, Types::Strict::Time | ::Time |
| attribute :x, Types::Strict::Array | ::Array |
| attribute :x, Types::Strict::Hash | ::T::Hash[::T.untyped, ::T.untyped] |
| attribute :x, Types::Coercible::String | ::String |
| attribute :x, Types::Coercible::Symbol | ::Symbol |
| attribute :x, Types::Coercible::Integer | ::Integer |
| attribute :x, Types::Coercible::Float | ::Float |
| attribute :x, Types::Coercible::Decimal | ::BigDecimal |
| attribute :x, Types::Coercible::Array | ::Array |
| attribute :x, Types::Coercible::Hash | ::T::Hash[::T.untyped, ::T.untyped] |
| attribute :x, Types::Params::Nil | ::NilClass |
| attribute :x, Types::Params::Date | ::Date |
| attribute :x, Types::Params::DateTime | ::DateTime |
| attribute :x, Types::Params::Time | ::Time |
| attribute :x, Types::Params::True | ::T::Boolean |
| attribute :x, Types::Params::False | ::T::Boolean |
| attribute :x, Types::Params::Bool | ::T::Boolean |
| attribute :x, Types::Params::Integer | ::Integer |
| attribute :x, Types::Params::Float | ::Float |
| attribute :x, Types::Params::Decimal | ::BigDecimal |
| attribute :x, Types::Params::Array | ::Array |
| attribute :x, Types::Params::Hash | ::T::Hash[::T.untyped, ::T.untyped] |
| attribute :x, Types::JSON::Nil | ::NilClass |
| attribute :x, Types::JSON::Symbol | ::Symbol |
| attribute :x, Types::JSON::Date | ::Date |
| attribute :x, Types::JSON::DateTime | ::DateTime |
| attribute :x, Types::JSON::Time | ::Time |
| attribute :x, Types::JSON::Decimal | ::BigDecimal |
| attribute :x, Types::JSON::Array | ::Array |
| attribute :x, Types::JSON::Hash | ::T::Hash[::T.untyped, ::T.untyped] |
| attribute :x, Types::Maybe::Strict::Class | ::T.any(::Dry::Monads::Maybe::Some, ::Dry::Monads::Maybe::None) |
| attribute :x, Types::Maybe::Strict::String | ::T.any(::Dry::Monads::Maybe::Some, ::Dry::Monads::Maybe::None) |
| attribute :x, Types::Maybe::Strict::Symbol | ::T.any(::Dry::Monads::Maybe::Some, ::Dry::Monads::Maybe::None) |
| attribute :x, Types::Maybe::Strict::True | ::T.any(::Dry::Monads::Maybe::Some, ::Dry::Monads::Maybe::None) |
| attribute :x, Types::Maybe::Strict::False | ::T.any(::Dry::Monads::Maybe::Some, ::Dry::Monads::Maybe::None) |
| attribute :x, Types::Maybe::Strict::Integer | ::T.any(::Dry::Monads::Maybe::Some, ::Dry::Monads::Maybe::None) |
| attribute :x, Types::Maybe::Strict::Float | ::T.any(::Dry::Monads::Maybe::Some, ::Dry::Monads::Maybe::None) |
| attribute :x, Types::Maybe::Strict::Decimal | ::T.any(::Dry::Monads::Maybe::Some, ::Dry::Monads::Maybe::None) |
| attribute :x, Types::Maybe::Strict::Date | ::T.any(::Dry::Monads::Maybe::Some, ::Dry::Monads::Maybe::None) |
| attribute :x, Types::Maybe::Strict::DateTime | ::T.any(::Dry::Monads::Maybe::Some, ::Dry::Monads::Maybe::None) |
| attribute :x, Types::Maybe::Strict::Time | ::T.any(::Dry::Monads::Maybe::Some, ::Dry::Monads::Maybe::None) |
| attribute :x, Types::Maybe::Strict::Array | ::T.any(::Dry::Monads::Maybe::Some, ::Dry::Monads::Maybe::None) |
| attribute :x, Types::Maybe::Strict::Hash | ::T.any(::Dry::Monads::Maybe::Some, ::Dry::Monads::Maybe::None) |
| attribute :x, Types::Maybe::Coercible::String | ::T.any(::Dry::Monads::Maybe::Some, ::Dry::Monads::Maybe::None) |
| attribute :x, Types::Maybe::Coercible::Integer | ::T.any(::Dry::Monads::Maybe::Some, ::Dry::Monads::Maybe::None) |
| attribute :x, Types::Maybe::Coercible::Float | ::T.any(::Dry::Monads::Maybe::Some, ::Dry::Monads::Maybe::None) |
| attribute :x, Types::Maybe::Coercible::Decimal | ::T.any(::Dry::Monads::Maybe::Some, ::Dry::Monads::Maybe::None) |
| attribute :x, Types::Maybe::Coercible::Array | ::T.any(::Dry::Monads::Maybe::Some, ::Dry::Monads::Maybe::None) |
| attribute :x, Types::Maybe::Coercible::Hash | ::T.any(::Dry::Monads::Maybe::Some, ::Dry::Monads::Maybe::None) |

# example\_nested.rb

| dry declaration | rbi return type |
| --- | --- |
| attribute? :x, Types.Instance(Fooo) | ::T.nilable(Fooo) |
| attribute? :x, Types.Instance(::Range) | ::T.nilable(::T::Range[::T.untyped]) |
| attribute? :x, Types.Instance(::Set) | ::T.nilable(::T::Set[::T.untyped]) |
| attribute? :x, Types.Constructor(Fooo) | ::T.nilable(Fooo) |
| attribute? :x, Types.Constructor(::Range) | ::T.nilable(::T::Range[::T.untyped]) |
| attribute? :x, Types.Constructor(::Set) | ::T.nilable(::T::Set[::T.untyped]) |
| attribute? :x, Types.Interface(:call).optional | ::T.untyped |
| attribute? :x, Types::Hash.schema(name: Types::String, age: Types::Coercible::Integer) | ::T::Hash[::T.untyped, ::T.untyped] OR ::T.nilable(name: ::String, age: ::Integer) |
| attribute :x, Types::Nil | Types::String | Types::Integer | ::T.nilable(::T.any(::String, ::Integer)) |
| attribute :x, Types::Nil | Types.Array(Types::String) | Types.Array(Types::Integer) | ::T.nilable(::T.any(::T::Array[::String], ::T::Array[::Integer])) |
| attribute :x, Types.Array(Types::String) | ::T::Array[::String] |
| attribute? :x, Types.Array(Types::String) | ::T.nilable(::T::Array[::String]) |
| attribute? :x, Types.Array(Types::String.optional) | ::T.nilable(::T::Array[::T.nilable(::String)]) |
| attribute :x, Types.Array(Fooo) | ::T::Array[Fooo] |
| attribute :x, Types.Array(Types.Constructor(Fooo)) | ::T::Array[Fooo] |
| attribute :x, Types.Array(Types::Nil | Types::String | Types::Integer) | ::T::Array[::T.nilable(::T.any(::String, ::Integer))] |
| attribute :x, Types.Array(Types.Array(Types::Nil | Types::String | Types::Integer)) | ::T::Array[::T::Array[::T.nilable(::T.any(::String, ::Integer))]] |

---

<div class="post-metadata">

**Author:** ![alassek](https://yyz1.discourse-cdn.com/flex031/user_avatar/discourse.dry-rb.org/alassek/32/314_2.png) [@alassek](https://discourse.dry-rb.org/u/alassek)\
**Post date:** [October 6, 2022, 6:30pm UTC](https://discourse.dry-rb.org/t/dry-rb-sorbet-can-we-unite/961/5 "2022-10-06T18:30:34Z")

</div>

I would have assumed that with the release of RBS, RBI was a dead-end. Certainly we don’t need two formats for this.

---

<div class="post-metadata">

**Author:** ![jikumaru](https://avatars.discourse-cdn.com/v4/letter/j/ba9def/32.png) [@jikumaru](https://discourse.dry-rb.org/u/jikumaru)\
**Post date:** [October 8, 2022, 5:38am UTC](https://discourse.dry-rb.org/t/dry-rb-sorbet-can-we-unite/961/6 "2022-10-08T05:38:50Z")

</div>

[alassek](https://discourse.dry-rb.org/u/alassek), Thank you for your reply!

[My app’s source code](https://github.com/YukiJikumaru/dry_tapioca_compiler/blob/main/sorbet/tapioca/compilers/dry_types.rb#L10) can be used to generate RBS too!

```ruby
compiler = DryAstCompiler.new
DryStruct.schema.each do |s|
  attribute_info = compiler.visit(s.to_ast)
  # => { name: 'ATTRIBUTE_NAME', type: AttributeClass, required: bool }

  sorbet_type = Tapioca::Compilers::DryTypes.to_sorbet_type(attribute_info[:name], attribute_info[:required])

  rbs_type = NewKlass.to_rbs(attribute_info[:name], attribute_info[:required])
end

```

So whichever becomes the default standard of Ruby’s type checker, class DryAstCompiler can be usefull!

---

<div class="post-metadata">

**Author:** ![alassek](https://yyz1.discourse-cdn.com/flex031/user_avatar/discourse.dry-rb.org/alassek/32/314_2.png) [@alassek](https://discourse.dry-rb.org/u/alassek)\
**Post date:** [October 11, 2022, 4:01am UTC](https://discourse.dry-rb.org/t/dry-rb-sorbet-can-we-unite/961/7 "2022-10-11T04:01:53Z")

</div>

Awesome! Nice work. I will try to find some time to look into this more deeply

---

<div class="post-metadata">

**Author:** ![jikumaru](https://avatars.discourse-cdn.com/v4/letter/j/ba9def/32.png) [@jikumaru](https://discourse.dry-rb.org/u/jikumaru)\
**Post date:** [October 12, 2022, 4:43pm UTC](https://discourse.dry-rb.org/t/dry-rb-sorbet-can-we-unite/961/8 "2022-10-12T16:43:19Z")

</div>

I released α version.

[https://rubygems.org/gems/tapioca-compilers-dry\_struct](https://rubygems.org/gems/tapioca-compilers-dry_struct)

---

<div class="post-metadata">

**Author:** ![jikumaru](https://avatars.discourse-cdn.com/v4/letter/j/ba9def/32.png) [@jikumaru](https://discourse.dry-rb.org/u/jikumaru)\
**Post date:** [October 17, 2022, 2:45am UTC](https://discourse.dry-rb.org/t/dry-rb-sorbet-can-we-unite/961/9 "2022-10-17T02:45:29Z")

</div>

[tapioca-compilers-dry\_struct | RubyGems.org | your community gem host](https://rubygems.org/gems/tapioca-compilers-dry_struct) is now production ready!
