DRY.rb + Sorbet - Can we unite?

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. :smile:

GitHub Issue on Sorbet

1 Like

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

Currently I’m trying to write a tapioca compiler for dry-struct.

https://github.com/YukiJikumaru/dry_tapioca_compiler

This app generates sorbet’s rbi files.

From this ruby code

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

# 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.

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))]]

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

alassek, Thank you for your reply!

My app’s source code can be used to generate RBS too!

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!

1 Like

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

I released α version.

https://rubygems.org/gems/tapioca-compilers-dry_struct

tapioca-compilers-dry_struct | RubyGems.org | your community gem host is now production ready!

2 Likes