我可以在方法签名中指定鸭子类型吗?

Can I specify a duck type in method signatures?

示例代码如下:

# 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

运行 srb tc 失败

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?
                   ^^^^^^^^

我看到了几种解决此问题的方法:

  1. 在签名中使用类似 .returns(T.any(KeyGetter, KeyGetter::Null)) 的内容。
  2. 使 KeyGetter::Null 继承自 KeyGetter
  3. 提取一个 "interface" 并期待它。

    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
    

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

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

或者这是一个天生有缺陷的想法(因为理想情况下我们也需要注释 duck 类型的方法。这样做时不要迷失在语法中)。

所以是的,我们可以在这里注释鸭子类型内联吗?如果不是,我们应该怎么做?

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:

我发现 sorbet 对具有特定键的散列的支持非常有限(流称为 "sealed object")。您可以尝试这样的操作,但 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

See on Sorbet.run

他们试图用 Typed Struct ([T::Struct]) 来解决这个问题,但这与你自己定义 class/interface 没有什么不同。

Sorbet 确实支持元组,但在这里也不理想。 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).

既然要注解duck类型的方法,那就更需要为它定义一个class。在您概述的方法中,我最喜欢选项 (2)。

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

KeyGetter::NULL = KeyGetter.new(nil)