为什么在 Ruby 中屈服于 lambda splat 数组参数?

Why does yielding to lambda splat array arguments in Ruby?

在 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)

这是怎么回事?

差异可能来自 to_proc 的书写方式:对于简单的 lambda 或 proc,它只是 returns self。对于一个方法,它将 is_from_method 标志设置为 true,这在过程为 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);
}

看起来如果过程是从方法创建的,参数将被检查。

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

现在让我们追踪 yieldIt's completely separate from Proc::call. Starts here, then, then, then (VM_BH_FROM_PROC just checks that the thing is indeed a proc), then (due to the goto). I couldn't trace exactly the magic that happens in the second run of the switch, but it only makes sense that it goes through the block_handler_type_iseq case or exits the switch. Point is, invoke_block_from_c_splattable 知道假定过程的 lambda 状态,但除了验证它确实是 lambda 之外,它不用于任何其他用途。它不会阻止争论。


至于原因部分 - 我什至不确定它是否是故意的,只是缺少 procs 逻辑中的分支。 Lambdas 主要被宣传为 method-like ¯\_(ツ)_/¯

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

The & argument preserves the tricks if a Proc object is given by & argument.

据文档说,即使在使用 & 将 lambda 传递给方法时,也应保留处理参数的方式。

以下代码

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)

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

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

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