【问题标题】:How does ruby unpack arguments passed into Proc?ruby 如何解包传递给 Proc 的参数?
【发布时间】:2023-03-19 05:18:01
【问题描述】:
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

我不认为这种不一致是设计故障,因此我们将不胜感激任何对此的见解。

【问题讨论】:

  • 不确定设计方面为什么事情是这样的,或者 ruby​​ 如何处理 proc 参数,但请查看 lambda? 的文档,其中解释了 procs 的一些技巧(额外参数被忽略,缺少的参数是nil,数组被扩展,等等)
  • 感谢您提出一个有趣的问题。这与 SO 上的常规做法大相径庭。
  • p c 更改为 puts "a=#{a}, b=#{b}, c=#{c]" 并使用一系列参数调用 proc。然后将proc的参数更改为|a,*b,c|(并相应地更改主体),然后再次使用不同的参数调用proc,依此类推。您很快就能回答自己的问题。通常,如果您,人类,看不到分配块变量的逻辑和独特的方式(例如,call(1,2,3,4,5)|*a,b,*c|),Ruby 也不会,所以她会引发异常;如果你看到一个且只有一个逻辑分配,Ruby 会同意。

标签: ruby arguments proc argument-unpacking


【解决方案1】:

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

在这方面,块语义更类似于赋值语义而不是方法语义。事实上,在旧版本的 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)并且参数将绑定到@的元素987654329@
  • 如果一个块接收的参数多于它的参数,它会忽略额外的参数
  • 如果一个块接收的参数少于它的参数,额外的参数绑定到nil

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

简而言之,块参数的绑定方式与多重赋值的绑定方式大致相同。您可以想象没有设置器、索引器、全局变量、实例变量和类变量的赋值,只有局部变量,这几乎就是块的参数绑定的工作方式:从块中复制并粘贴参数列表,从@987654332 复制并粘贴参数列表@,在两者之间加上一个= 符号,你就明白了。

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

Procs 在参数绑定和参数传递(即前面描述的赋值语义)以及return 的行为(它从最近的词法封闭方法返回)时表现得像块。

Lambda 的行为类似于 方法,无论是在参数绑定和参数传递(即严格的参数检查)方面,还是在 return 的行为方面(它从 lambda 本身返回) )。

一个简单的助记词:“block”和“proc”押韵,“method”和“lambda”都是希腊语。


对你的问题的一个小评论:

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)

【讨论】:

  • 你能举一个If a block receives multiple arguments but only has a single parameter, the parameter will be bound to an array of all arguments的例子吗?如果你有a_proc = proc { |a| a } 并传递它a_proc.call(1, 2, 3),则返回值将是1,而不是[1, 2, 3]
  • 我记错了。现已修复。
  • Jörg,你是完美主义者,我想你可能对this 感兴趣。在超市收银台看到“10件以内”,我不禁咬牙切齿。
【解决方案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]) 会起作用,因为您正在设置 abc。这将是同一件事: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 作为输出。当每个元素作为参数传递给块时,它会看到它是一个数组并将其分解,将元素分配给尽可能多的给定块变量。不只是将该数组放入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]。它会自动拆分它自己的参数。

【讨论】:

  • 你指的ruby-doc.org/core-2.1.1/Proc.html#method-i-lambda-3F很棒!谢谢。 ruby 处理 block/proc 参数的方式有点混乱。
  • @ZeGao Block/proc 参数并不混乱,它们只是遵循您还不熟悉的规则。 Jörg 的回答更好地解释了这些规则
  • 鉴于def foo(a, b, *c)foo(1, 1, [1,2,3,4])foo(1, 1, *[1,2,3,4]) 不一样。在第一种情况下c[[1, 2, 3, 4]],在第二种情况下是[1, 2, 3, 4]。同样foo(1, 2, [3, 4])foo(1, 2, *[3, 4])也不相同,前者将c赋值为[[3, 4]],后者赋值为[3, 4]
  • @m.simonborg 你是对的。我在答案中解决了这个问题。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-09-12
  • 1970-01-01
相关资源
最近更新 更多