Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Can I specify a duck type in method signatures?

Tags:

ruby

sorbet

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?

like image 302
Sergio Tulentsev Avatar asked Jun 21 '19 12:06

Sergio Tulentsev


People also ask

Does Java support duck typing?

Java does not support duck typing. The method signature has to specify the correct type for each argument. For example, an argument has to implement the “duck” interface. It is not enough for a class to have the same methods that are provided for in the interface; no, it is necessary to actually write implements Duck.

What languages have duck typing?

What languages support duck typing? Python and Ruby support duck typing, both of which are dynamic typed languages. In general, dynamic typed languages such as JavaScript and TypeScript support duck typing.

Does Python support duck typing?

Duck Typing is a type system used in dynamic languages. For example, Python, Perl, Ruby, PHP, Javascript, etc.

Is duck typing a form of touch typing?

The term Duck Typing is a lie.


1 Answers

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)
like image 149
hdoan Avatar answered Oct 14 '22 07:10

hdoan