【问题标题】:Why does yielding to lambda splat array arguments in Ruby?为什么在 Ruby 中屈服于 lambda splat 数组参数?
【发布时间】:2017-06-11 18:05:45
【问题描述】:

在 Ruby 中,使用错误数量的参数调用 lambda 会导致 ArgumentError

l = lambda { |a, b| p a: a, b: b }
l.call(1, 2) 
# {:a=>1, :b=>2}

l.call(1)
# ArgumentError: wrong number of arguments (given 1, expected 2)

传递一个数组也不起作用:(因为数组只是一个对象,对吧?)

l.call([3, 4])
# ArgumentError: wrong number of arguments (given 1, expected 2)

除非我使用 splat (*) 将数组转换为参数列表,但我没有。

但是 ...如果我通过yield 隐式调用lambda,就会发生意想不到的事情:

def yield_to
  yield(1, 2)
  yield([3, 4])
end

yield_to(&l)
# {:a=>1, :b=>2}
# {:a=>3, :b=>4}   <- array as argument list!?

更令人困惑的是,通过Method#to_proc 派生的 lambda 确实可以按预期工作:

def m(a, b)
  p a: a, b: b
end

yield_to(&method(:m))
# {:a=>1, :b=>2}
# ArgumentError: wrong number of arguments (given 1, expected 2)

这是怎么回事?

【问题讨论】:

  • &amp; 不仅仅是打电话给#to_proc 所以你的最后一个例子不是那么公平。但我认为这里的关键是yield 不会调用#call,它会执行一个“bare bone” 块。而#call 方法检查参数然后执行块。
  • 看起来 yield 使用 call(arg)call(*args) 的等效项,具体取决于预期的参数数量。但是,很难找到相应的文档。
  • @ndn 如果我通过prc = method(:m).to_proc 检索过程并调用yield_to(&amp;prc),我会得到相同的结果。 prc 是一个带有两个必需参数的 lambda,就像 l
  • @EricDuminil 那么最后一个例子也不应该引发异常。
  • 相关但 IMO 不重复:stackoverflow.com/questions/23945533/…

标签: ruby lambda yield


【解决方案1】:

我在这里回答我自己的问题,因为这是一个已知的错误:

https://bugs.ruby-lang.org/issues/12705

它已在 Ruby 2.4.1 中修复(感谢@ndn)

【讨论】:

    【解决方案2】:

    显然,实施探索是一种方式。首先,为什么#call 会为参数不正确的 lambdas 引发异常? Proc::call explicitly checks for lambdas.

    现在让我们追踪yieldIt's completely separate from Proc::callStarts herethenthenthenVM_BH_FROM_PROC just checks that the thing is indeed a proc),then(由于goto)。我无法准确追踪开关第二次运行中发生的魔法,但只有通过the block_handler_type_iseq case 或退出开关才有意义。重点是,invoke_block_from_c_splattable 知道假定 proc 的 lambda 状态,但除了验证它确实是 lambda 之外,它不会将其用于任何其他用途。它并不能防止争论不休。


    至于为什么部分 - 我不确定它是否是故意的,只是缺少 procs 逻辑中的分支。 Lambda 主要宣传为类方法¯\_(ツ)_/¯

    【讨论】:

    • "invoke_block_from_c_splattable 知道 lambda 状态" – 它是,但 lambda { ... }Method#to_proc 都返回 procs 是 lambas(即有他们的 lambda -标志集)。然而,他们的待遇却不同。
    • @Stefan,方法过程属于block_handler_type_ifunc 的情况,这与block_handler_type_proc 不同?
    • 这里缺少 lambda 检查:github.com/ruby/ruby/blob/v2_4_0/vm_insnhelper.c#L2435 - 我不理解整个代码,但这可以解释为什么 lambdas 被视为普通 procs。
    【解决方案3】:

    差异可能来自to_proc 的编写方式:对于简单的 lambda 或 proc,它只返回 self。对于一个方法,它将is_from_method 标志设置为true,当proc 为invoked in the VM 时,这将发挥作用:

    if (proc->is_from_method) {
        return vm_invoke_bmethod(th, proc, self, argc, argv, passed_block_handler);
    }
    else {
        return vm_invoke_proc(th, proc, self, argc, argv, passed_block_handler);
    }
    

    看起来如果 proc 是从方法创建的,则将检查参数。

    【讨论】:

    • 除非你证明 lambda 是if_from_methoddoesn't seem to be the case),否则这只是一个推测。尽管方向可能是正确的。
    • @ndn 不幸的是,Proc 没有 from_method? 方法,因此无法轻松显示,但 eugen 是正确的:通过 lamba 创建的 proc 没有 @987654331 @标志集。
    • 另外,有谁知道,为什么处理方式不同?
    • @Stefan,我认为 eugen 是在说它已经设置好了,这就是我们得到不同结果的原因(与其他过程相反)。
    • 在 Ruby 的源代码中挖掘了一段时间后,我不确定这是否真的能解释它。提到的 C 函数 rb_vm_invoke_proc 似乎是在调用 Proc#call 时被调用的函数,如我的示例所示,call 按预期工作。只是yield 不同。
    【解决方案4】:

    查看文档后,我认为这是 Ruby 中的错误。 Proc class docs 明确表示:

    如果 Proc 对象由 & 给出,则 & 参数保留技巧 论据。

    据该文档所说,即使将 lambda 与 &amp; 一起传递给方法,也应保留处理参数的方式。

    以下代码

    l = lambda { |a, b| p a: a, b: b }
    
    def yield_to(&block)
      yield([3, 4])
      p block.lambda?
    end
    
    yield_to(&l)
    

    输出

    {:a=>3, :b=>4}
    true
    

    这是互斥的。

    有趣的是,这段代码以ArgumentError 失败

    l = lambda { |a, b| p a: a, b: b }
    
    def yield_to(&block)
      block.call([3, 4])
      p block.lambda?
    end
    
    yield_to(&l)
    

    【讨论】:

    • 如果您使用&amp; 捕获它并使用#call,它确实会保留它。我没有看到文档明确说明 yield 的行为。
    • 不确定,但我认为 yieldcall 应该与传递的块以相同的方式工作
    • 好吧,如果这是预期,那么是的 - 这是一个错误。我只是指出您提供的资源没有记录这种期望。
    • 我刚刚更新了我的问题,这是一个错误并且已经被报告了。
    猜你喜欢
    • 2013-05-14
    • 1970-01-01
    • 2014-07-19
    • 1970-01-01
    • 2017-12-14
    • 2018-05-02
    • 2016-10-19
    • 2020-01-02
    • 2013-03-25
    相关资源
    最近更新 更多