如何将ruby解包参数传递给Proc?

时间:2017-07-29 10:40:15

标签: ruby arguments proc argument-unpacking

a_proc = Proc.new {|a,b,*c| p c; c.collect {|i| i*b }}
puts a_proc[2,2,4,3]

上面的代码根据https://ruby-doc.org/core-2.2.0/Proc.html非常直观, a_proc [2,2,4,3] 只是 a_proc.call(2,2, 4,3)隐藏“呼叫”

但以下(效果很好)让我很困惑

a=[2,2,4,3]
puts a_proc.call(a)
puts a_proc.call(*a)

它似乎与普通函数调用非常不同,因为它不会检查传入的数字参数。

但是,正如预期的那样,如果同样使用参数

,则调用语义的方法将引发错误
def foo(a,b,*c)
  c.collect{|i| i*b}
end
foo([1,2,3,4]) #`block in <main>': wrong number of arguments (given 1, expected 2+) (ArgumentError)

foo(*[1,2,3,4]) #works as expected

我认为这种不一致性不是设计故障,所以对此的任何见解都将受到赞赏。

2 个答案:

答案 0 :(得分:4)

块使用与用于将参数绑定到参数的方法不同的语义。

在这方面,块语义更类似于赋值语义而不是方法语义。事实上,在旧版本的Ruby中,块字面上使用了参数绑定的赋值,你可以这样写:

class Foo; def bar=(val) puts 'setter called!' end end

some_proc = Proc.new {|$foo, @foo, foo.bar|}
some_proc.call(1, 2, 3)
# setter called!
$foo #=> 1
@foo #=> 2

值得庆幸的是,自Ruby 1.9以来不再是这种情况。但是,保留了一些语义:

  • 如果一个块有多个参数但只接收一个参数,那么该参数将被发送to_ary消息(如果它不是Array已经存在)并且参数将被绑定到Array
  • 的元素
  • 如果一个块接收的参数多于参数,则忽略额外的参数
  • 如果块接收的参数少于参数,则额外参数将绑定到nil

注意:#1是使Hash#each如此精美工作的原因,否则,您总是必须解构传递给块的数组。

简而言之,块参数的绑定方式与多次赋值的绑定方式大致相同。您可以想象没有setter,indexers,globals,实例变量和类变量的赋值,只有局部变量,这就是块的参数绑定的工作原理:复制并粘贴块中的参数列表,复制并粘贴参数列表从yield开始,在=之间插入一个Proc标志,然后您就明白了。

现在,你实际上并没有谈论一个街区,但是,你在谈论一个Proc。为此,您需要知道一些重要的事情:两种种类Proc,不幸的是,它们是使用同一个类实现的。 (IMO,它们应该是两个不同的类。)一种称为 lambda ,另一种通常称为 proc (令人困惑,因为两者都是{{1 }}为s)。

Procs就像块一样,既涉及参数绑定和参数传递(即上述赋值语义),也涉及return的行为(它从最接近的词法封闭方法返回) )。

Lambdas的行为类似于方法,既涉及参数绑定和参数传递(即严格的参数检查),也涉及return的行为(它从lambda本身。)

一个简单的助记符:&#34;阻止&#34;和&#34; proc&#34;韵,&#34;方法&#34;和&#34; lambda&#34;都是希腊人。

对你的问题的一点评论:

  

a_proc [2,2,4,3] 只是 a_proc.call(2,2,4,3)隐藏“通话”的语法糖

这是语法糖。相反,Proc只是将[]方法定义为与call完全相同。

是什么语法糖是这样的:

a_proc.(2, 2, 4, 3)

每次出现

foo.(bar, baz)

被解释为

foo.call(bar, baz)

答案 1 :(得分:2)

我相信可能令你困惑的是Procs的一些属性。如果给它们一个数组参数,它们将自动splat它。此外,ruby块通常有一些处理块参数的有趣方法。您期望的行为是您将使用Lambda获得的行为。我建议您阅读Proc.lambda? documentationbe careful when calling a ruby block with an array

现在,让我们从splat运算符开始,然后转到ruby处理块参数的方式:

def foo(a, b, *c) 
  c.map { |i| i * b } # Prefer to use map alias over collect
end

foo([1, 2, 3, 4]) # `block in <main>': wrong number of arguments (given 1, expected 2+) (ArgumentError)

foo(*[1, 2, 3, 4]) # works as expected

因此,在您的参数错误中,它是有道理的:def foo()至少需要两个参数:ab,以及*c的多个参数。 *the splat operator。它会将一个数组转换为单独的参数,或者在相反的情况下,将一个可变数量的参数转换为数组。因此,当您说foo([1,2,3,4])时,您正在给foo一个参数a,它是[1,2,3,4]。您没有设置b*c。例如,因为您要设置foo(1, 1, 1, 2, 3, 4])ab,所以可以使用c。这将是同一件事:foo(1, 1, *[1,2,3,4])

现在foo(*[1, 2, 3, 4])按预期工作,因为splat运算符(*)将其转换为foo(1, 2, 3, 4)或等效foo(1, 2, *[3, 4])

好的,现在我们已经覆盖了splat运算符,让我们回顾一下下面的代码(我做了一些小改动):

a_proc = Proc.new { |a, b, *c| c.map { |i| i * b }}
a = [1, 2, 3, 4]
puts a_proc.call(a)
puts a_proc.call(*a)

请记住,如果块/ proc被赋予一个array参数,它们将自动splat它。因此,如果您有一组数组arrays = [[1, 1], [2, 2], [3, 3]]并且arrays.each { |a, b| puts "#{a}:#{b}" },那么您将获得1:12:23:3作为输出。当每个元素作为参数传递给块时,它会看到它是一个数组并对其进行splats,将元素分配给尽可能多的给定块变量。您可以获得a,而不只是将该数组放在a = [1, 1]; b = nil a = 1; b = 1中。它与proc做同样的事情。

a_proc.call([1, 2, 3, 4])已转为Proc.new { |1, 2, [3, 4]| c.map { |i| i * b }},并将输出[6, 8]。它会自动拆分参数。