Sergio Tulentsev
Sergio Tulentsev

Reputation: 230461

Can I specify a duck type in method signatures?

Here's example code:

# typed: true

class KeyGetter

  sig {params(env_var_name: String).returns(KeyGetter)}
  def self.from_env_var(env_var_name)
    return Null.new if env_var_name.nil?

    return new(env_var_name)
  end

  def initialize(env_var_name)
    @env_var_name = env_var_name
  end

  def to_key
    "key from #{@env_var_name}"
  end

  def to_s
    "str from #{@env_var_name}"
  end

  class Null
    def to_key; end
    def to_s; end
  end
end

Running srb tc on it fails with

key_getter.rb:7: Returning value that does not conform to method result type https://srb.help/7005
     7 |    return Null.new if env_var_name.nil?
            ^^^^^^^^^^^^^^^
  Expected KeyGetter
    key_getter.rb:6: Method from_env_var has return type KeyGetter
     6 |  def self.from_env_var(env_var_name)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  Got KeyGetter::Null originating from:
    key_getter.rb:7:
     7 |    return Null.new if env_var_name.nil?
                   ^^^^^^^^

I see several ways of working around this:

  1. Use something like .returns(T.any(KeyGetter, KeyGetter::Null)) in the sig.
  2. Make KeyGetter::Null inherit from KeyGetter.
  3. Extract an "interface" and expect that.

    class KeyGetter
      module Interface
        def to_key; end
        def to_s; end
      end
    
      class Null
        include KeyGetter::Interface
      end
    
      include Interface
    
      sig {params(env_var_name: String).returns(KeyGetter::Interface)}
      def self.from_env_var(env_var_name)
        return Null.new if env_var_name.nil?
    
        return new(env_var_name)
      end
    

But what I'd like to know (and didn't find in the docs) is: can I describe the duck type? Like we can do in YARD, for example:

 # @returns [(#to_s, #to_key)]

Or is it an inherently flawed idea (because ideally we need to annotate the duck type's methods too. And not get lost in syntax while doing that).

So yes, can we annotate the duck type inline here? If not, what should we do instead?

Upvotes: 9

Views: 824

Answers (1)

hdoan
hdoan

Reputation: 377

But what I'd like to know (and didn't find in the docs) is: can I describe the duck type? Like we can do in YARD, for example:

I've found sorbet has very limited support for hash with specific keys (what flow calls "sealed object"). You could try something like this, but foo would be recognized as T::Hash[T.untyped, T.untyped], or at most T::Hash[String, String].

extend T::Sig

sig { returns({to_s: String, to_key: String}) }
def foo
  T.unsafe(nil)
end

T.reveal_type(foo)
foo.to_s
foo.to_key

See on Sorbet.run

They attempt to resolve that with Typed Struct ([T::Struct]), but that'd be no different from you defining the class/interface yourself.

Sorbet does support tuple but that wouldn't be ideal here either. See on Sorbet.run

Or is it an inherently flawed idea (because ideally we need to annotate the duck type's methods too. And not get lost in syntax while doing that).

Given that you want to annotate the duck type's methods, it's all the more to define a class for it. I like the option (2) the best among the approaches you outlined.

You can also make NULL a constant value instead. But given how the current code is implemented, it's probably not as good as option (2)

KeyGetter::NULL = KeyGetter.new(nil)

Upvotes: 2

Related Questions