Can I specify a duck type in method signatures?

Ser*_*sev 8 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
Run Code Online (Sandbox Code Playgroud)

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?
                   ^^^^^^^^
Run Code Online (Sandbox Code Playgroud)

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
    
    Run Code Online (Sandbox Code Playgroud)

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)]
Run Code Online (Sandbox Code Playgroud)

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?

小智 2

但我想知道(并且在文档中没有找到)是:我可以描述鸭子类型吗?就像我们在 YARD 中可以做的那样,例如:

我发现 sorbet 对具有特定键的哈希的支持非常有限(流程称为“密封对象”)。您可以尝试类似的操作,但最多foo会被识别为或。T::Hash[T.untyped, T.untyped]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
Run Code Online (Sandbox Code Playgroud)

请参阅 Sorbet.run

他们尝试使用Typed Struct ( )来解决这个问题[T::Struct],但这与您自己定义类/接口没有什么不同。

Sorbet 确实支持元组,但这在这里也不是理想的。请参阅 Sorbet.run

或者它是一个本质上有缺陷的想法(因为理想情况下我们也需要注释鸭子类型的方法。并且在这样做时不要迷失在语法中)。

鉴于您想要注释鸭子类型的方法,因此更需要为其定义一个类。在您概述的方法中,我最喜欢选项(2)。

您还可以将 NULL 设置为常量值。但考虑到当前代码的实现方式,它可能不如选项(2)

KeyGetter::NULL = KeyGetter.new(nil)
Run Code Online (Sandbox Code Playgroud)