何时使用lambda,何时使用Proc.new?

345

在 Ruby 1.8 中,proc/lambda 与 Proc.new 有微妙的差别。

  • 这些差别是什么?
  • 你能提供决定使用哪个的指南吗?
  • 在 Ruby 1.9 中,proc 和 lambda 是不同的。怎么回事?

3
另外参考Matz和Flanagan的《Ruby编程语言》一书,它全面涵盖了这个主题。proc的行为类似于块- yield语义,而lambda的行为类似于方法-方法调用语义。另外,在procs和lambdas中,return、break等都有不同的行为。 - Gishu
1
还可以查看一篇详细的文章,介绍Ruby Procs和Lambdas之间的控制流差异:http://www.akshay.cc/blog/2010-02-14-control-flow-differences-between-ruby-procs-and-lambdas.html - Akshay Rawat
你已经接受了一个回答,仅说明了proc和lambda之间的区别,而你的问题标题是何时使用这些东西。请注意回答问题的准确性。 - Shri
14个回答

384

lambdaProc.new 创建的proc之间还有一个重要但微妙的区别,就是它们如何处理 return 语句:

  • 在一个由 lambda 创建的 proc 中,return 语句只会从该 proc 返回
  • 而在一个由 Proc.new 创建的 proc 中,return 语句则更为出人意料:它不仅会从该 proc 返回,而且还会从包含该 proc 的方法中返回!

下面是 lambda 创建的 proc 中 return 的实际应用。它的行为可能符合你的预期:

def whowouldwin

  mylambda = lambda {return "Freddy"}
  mylambda.call

  # mylambda gets called and returns "Freddy", and execution
  # continues on the next line

  return "Jason"

end


whowouldwin
#=> "Jason"

现在我们来看一个由 Proc.new 创建的 proc,它的 return 做相同的事情。你将要看到一个 Ruby 违反了备受赞誉的最小惊奇原则的情况:

def whowouldwin2

  myproc = Proc.new {return "Freddy"}
  myproc.call

  # myproc gets called and returns "Freddy", 
  # but also returns control from whowhouldwin2!
  # The line below *never* gets executed.

  return "Jason"

end


whowouldwin2         
#=> "Freddy"

由于这种令人惊讶的行为(以及更少的输入),在创建procs时,我倾向于使用lambda而不是Proc.new


12
还有一个名为proc的方法,它只是Proc.new的简写形式吗? - panzi
6
@panzi,是的,proc 等同于 Proc.new - ma11hew28
32
@mattdipasquale 抱歉,我只说对了一半。在1.8中,proc的行为类似于lambda,但在1.9中则类似于Proc.new。请参考Peter Wagenet的回答。 - Kelvin
60
为什么这种行为是“令人惊讶的”?lambda 是一个匿名函数。由于它是一个函数,所以它会返回一个值,调用它的方法可以对该值进行任何操作,包括忽略它并返回不同的值。Proc 类似于粘贴代码片段。它不像函数一样运行。因此,当在 Proc 中发生返回时,那只是调用它的方法代码的一部分。 - Arcolye
4
我认为一个主要的区别也在于,当您提供缺少/额外参数时,Proc不会抛出错误,而lambda会抛出“参数数量错误”的错误。 - bigpotato
显示剩余3条评论

95

为了进一步澄清:

Joey说Proc.new的返回行为令人惊讶。然而,当您考虑到Proc.new的行为类似于块时,这并不令人惊讶,因为这正是块的行为方式。另一方面,lambdas的行为更像方法。

这实际上解释了为什么Procs在参数数量(arity)方面很灵活,而lambdas则不是。块不需要提供它们的所有参数,但方法需要(除非提供默认值)。在Ruby 1.8中,虽然不能为lambda参数提供默认值,但现在在Ruby 1.9中支持使用另一种lambda语法来实现(正如webmat所指出的):

concat = ->(a, b=2){ "#{a}#{b}" }
concat.call(4,5) # => "45"
concat.call(1)   # => "12"

Michiel de Mare(即原作者)对于Procs和lambda在Ruby 1.9中的arity表现不正确。我验证它们仍然保持与上述1.8版本相同的行为。

break语句在Procs和lambdas中都没有太多意义。在Procs中,break将会从已经完成的Proc.new中返回你;而在lambda中从方法的最高层级跳出是没有任何意义的。

nextredoraise在Procs和lambdas中的表现是相同的。而retry在两者中均不允许,并会引发异常。

最后,proc方法不应该使用,因为它不一致并具有意外的行为。在Ruby 1.8中,它实际上返回一个lambda!在Ruby 1.9中,这已经得到了修复,它返回一个Proc。如果要创建Proc,请使用Proc.new

更多信息,我强烈推荐O'Reilly的《Ruby编程语言》这本书,这是我大部分信息的来源。


1
然而,当你考虑到 Proc.new 的行为类似于一个块时,这并不令人惊讶,因为这正是块的行为方式。然而,块是对象的一部分,而 Proc.new 创建一个对象。lambda 和 Proc.new 都创建一个类为 Proc 的对象,为什么会有区别呢? - weakish
2
从 Ruby 2.5 开始,Procs 中的 break 抛出 LocalJumpError,而 lambdas 中的 break 的行为就像 return,也就是返回 nil - Masa Sakano

42
我找到了这个页面,它展示了Proc.newlambda之间的区别。根据该页面,唯一的区别是lambda对其接受的参数数量非常严格,而Proc.new会将缺少的参数转换为nil。下面是一个IRB会话的例子,说明了它们之间的区别:
irb(main):001:0> l = lambda { |x, y| x + y }
=> #<Proc:0x00007fc605ec0748@(irb):1>
irb(main):002:0> p = Proc.new { |x, y| x + y }
=> #<Proc:0x00007fc605ea8698@(irb):2>
irb(main):003:0> l.call "hello", "world"
=> "helloworld"
irb(main):004:0> p.call "hello", "world"
=> "helloworld"
irb(main):005:0> l.call "hello"
ArgumentError: wrong number of arguments (1 for 2)
    from (irb):1
    from (irb):5:in `call'
    from (irb):5
    from :0
irb(main):006:0> p.call "hello"
TypeError: can't convert nil into String
    from (irb):2:in `+'
    from (irb):2
    from (irb):6:in `call'
    from (irb):6
    from :0
该页面还建议除非您特别需要容错行为,否则请使用lambda。我同意这种观点。使用lambda似乎更加简洁,并且由于差异微不足道,在普通情况下它似乎是更好的选择。
至于Ruby 1.9,抱歉,我还没有研究过1.9,但我不认为它们会有太大的改变(不过不要听我的,因为似乎您已经听说了一些变化,所以我可能是错的)。

2
过程与 Lambda 表达式的返回值不同。 - Cam
"Proc.new将缺失的参数转换为nil。此外,Proc.new也会忽略额外的参数(当然,lambda表达式会报错)。" - weakish

15

Proc已经有些老了,但是它的返回语义对我来说非常反直觉(至少在我学习这门语言时是这样),因为:

  1. 如果你正在使用proc,你很可能正在使用某种函数式编程范式。
  2. Proc可以从封闭作用域中返回(参见先前的回答),这基本上就像goto,而且高度不符合函数式编程的特性。

Lambda在功能上更加安全和易于理解 - 我总是使用它来代替proc。


11

我无法深入探讨细微的差异。但我可以指出,Ruby 1.9现在允许在lambda和block中使用可选参数。

以下是1.9版本下stabby lambdas的新语法:

stabby = ->(msg='inside the stabby lambda') { puts msg }

Ruby 1.8没有这种语法。常规方法声明块/lambda也不支持可选参数:

# under 1.8
l = lambda { |msg = 'inside the stabby lambda'|  puts msg }
SyntaxError: compile error
(irb):1: syntax error, unexpected '=', expecting tCOLON2 or '[' or '.'
l = lambda { |msg = 'inside the stabby lambda'|  puts msg }

然而,Ruby 1.9版本即使使用旧语法也支持可选参数:

l = lambda { |msg = 'inside the regular lambda'|  puts msg }
#=> #<Proc:0x0e5dbc@(irb):1 (lambda)>
l.call
#=> inside the regular lambda
l.call('jeez')
#=> jeez

如果你想为Leopard或Linux构建Ruby1.9,请查看这篇文章(无耻的自我推广)。


Lambda内的可选参数非常需要,我很高兴它们在1.9中被添加了。我认为块也可以有可选参数(在1.9中),是吗? - mpd
你没有展示默认参数在块中的使用,只有在lambda表达式中。 - iconoclast

11

简短回答:关键在于return的作用:lambda 从自身返回,而proc 不仅从自身返回,还可以返回调用它的函数。

不太清楚的是为什么要使用它们中的哪一个。从函数式编程的角度来看,lambda 是我们预期的做法。它基本上是一个带有自动绑定当前作用域的匿名方法。在这两种情况下,您应该使用 lambda。

另一方面,Proc 对于实现语言本身非常有用。例如,您可以使用它们实现“if”语句或“for”循环。在 proc 中发现的任何返回将从调用它的方法返回,而不仅仅是“if”语句。这就是语言的工作方式,以及“if”语句的工作方式,因此我猜测 Ruby 在内部使用了这个,并且他们只是公开了它,因为它似乎很强大。

只有在创建新的语言结构(如循环、if-else 结构等)时才真正需要它。


3
“lambda returns out of itself, and proc returns out of itself AND the function that called it”这句话完全是错误的,是一个非常普遍的误解。proc 是闭包并且返回到创建它的方法中。请参见页面其他地方我的完整答案。 - ComDubh

11
一个好的理解方式是,lambda表达式在其自己的作用域中执行(就好像是方法调用),而Proc可以被视为与调用方法内联执行。至少这是在每种情况下决定使用哪个的一种好方法。

7

我没有在问题的第三种方法“proc”上注意到任何评论,这种方法已被弃用,但在1.8和1.9中处理方式不同。

下面是一个相当详细的示例,可以轻松地看到这三个类似调用之间的差异:

def meth1
  puts "method start"

  pr = lambda { return }
  pr.call

  puts "method end"  
end

def meth2
  puts "method start"

  pr = Proc.new { return }
  pr.call

  puts "method end"  
end

def meth3
  puts "method start"

  pr = proc { return }
  pr.call

  puts "method end"  
end

puts "Using lambda"
meth1
puts "--------"
puts "using Proc.new"
meth2
puts "--------"
puts "using proc"
meth3

1
Matz曾表示他计划弃用它,因为proc和Proc.new返回不同的结果很容易让人感到困惑。但在1.9版本中,它们的行为是相同的(proc是Proc.new的别名)。http://eigenclass.org/hiki/Changes+in+Ruby+1.9#l47 - Dave Rapin
@banister:在1.8中,“proc”返回了一个lambda;现在已经修复为在1.9中返回一个proc - 但这是一个破坏性的变化;因此不建议再使用。 - Gishu
我认为在镐子的某个脚注中说到proc已经被有效地弃用了或者类似的话。我没有确切的页码。 - dertoni

7

Ruby中的闭包是一个很好的概述,介绍了在Ruby中如何使用块、lambda和proc。


我在读到“一个函数不能接受多个块——违反了闭包可以自由地作为值传递的原则”后就停止阅读了。块不是闭包,过程是闭包,而且一个函数可以接受多个过程。 - ComDubh

6

lambda的作用和其他语言一样。

Proc.new有些令人惊讶和困惑。

Proc.new创建的proc中的return语句不仅会从自身返回控制,而且还会从封闭它的方法中返回。

def some_method
  myproc = Proc.new {return "End."}
  myproc.call

  # Any code below will not get executed!
  # ...
end

你可以认为Proc.new像块一样将代码插入到包围它的方法中。 但是Proc.new创建了一个对象,而块是对象的一部分。

另外,lambda和Proc.new之间还有另一个区别,就是它们对于(错误的)参数处理方式不同。 lambda会抱怨它,而Proc.new会忽略额外的参数或将缺少的参数视为nil。

irb(main):021:0> l = -> (x) { x.to_s }
=> #<Proc:0x8b63750@(irb):21 (lambda)>
irb(main):022:0> p = Proc.new { |x| x.to_s}
=> #<Proc:0x8b59494@(irb):22>
irb(main):025:0> l.call
ArgumentError: wrong number of arguments (0 for 1)
        from (irb):21:in `block in irb_binding'
        from (irb):25:in `call'
        from (irb):25
        from /usr/bin/irb:11:in `<main>'
irb(main):026:0> p.call
=> ""
irb(main):049:0> l.call 1, 2
ArgumentError: wrong number of arguments (2 for 1)
        from (irb):47:in `block in irb_binding'
        from (irb):49:in `call'
        from (irb):49
        from /usr/bin/irb:11:in `<main>'
irb(main):050:0> p.call 1, 2
=> "1"

顺便提一下,在 Ruby 1.8 中,proc 创建一个 lambda 函数,而在 Ruby 1.9+ 中则像 Proc.new 一样运行,这真的很令人困惑。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接