【发布时间】:2011-05-18 22:02:29
【问题描述】:
这两者本质上是一样的吗?他们看起来和我很相似。
lambda 表达式是否借鉴了 Ruby 的思想?
【问题讨论】:
-
Lambda 表达式借鉴了 1930 年代的 lambda 演算和 70 年代的 ML。
这两者本质上是一样的吗?他们看起来和我很相似。
lambda 表达式是否借鉴了 Ruby 的思想?
【问题讨论】:
不完全是。但它们非常相似。最明显的区别是,在 C# 中,一个 lambda 表达式可以去任何你可能有一个恰好是函数的值的地方。在 Ruby 中,每个方法调用只有一个代码块。
他们都从 Lisp(一种可追溯到 1950 年代后期的编程语言)借用了这个想法,而后者又从 Church's Lambda Calculus 借用了 lambda 概念,该概念是在 1930 年代发明的。
【讨论】:
Ruby 实际上有 4 个结构非常相似
块背后的想法是一种实现真正轻量级策略模式的方法。块将在函数上定义一个协程,该函数可以使用 yield 关键字将控制权委托给该协程。我们对 ruby 中的几乎所有东西都使用块,包括几乎所有的循环结构或任何你会在 c# 中使用using 的地方。块外的任何东西都在块的范围内,但反之则不成立,除了块内的 return 将返回外部范围。它们看起来像这样
def foo
yield 'called foo'
end
#usage
foo {|msg| puts msg} #idiomatic for one liners
foo do |msg| #idiomatic for multiline blocks
puts msg
end
proc 基本上是获取一个块并将其作为参数传递。一个非常有趣的用途是,您可以传入一个 proc 作为另一种方法中的块的替换。 Ruby 有一个特殊的 proc coercion 字符是 &,以及一个特殊规则,如果方法签名中的最后一个参数以 & 开头,它将是方法调用块的 proc 表示。最后,有一个名为block_given? 的内置方法,如果当前方法定义了一个块,它将返回true。看起来是这样的
def foo(&block)
return block
end
b = foo {puts 'hi'}
b.call # hi
为了更深入地了解这一点,rails 添加到 Symbol 中有一个非常巧妙的技巧(并在 1.9 中合并到核心 ruby 中)。基本上, & coercion 通过调用 to_proc 在它旁边的任何东西上来发挥它的魔力。因此,rails 人员添加了一个 Symbol#to_proc,它会在传入的任何内容上调用自己。这让您可以为任何聚合样式函数编写一些非常简洁的代码,这些函数只是在列表中的每个对象上调用一个方法
class Foo
def bar
'this is from bar'
end
end
list = [Foo.new, Foo.new, Foo.new]
list.map {|foo| foo.bar} # returns ['this is from bar', 'this is from bar', 'this is from bar']
list.map &:bar # returns _exactly_ the same thing
更高级的东西,但真正说明了你可以用 procs 做的那种魔法
lambda 在 ruby 中的用途与在 c# 中的用途几乎相同,这是一种创建内联函数以传递或在内部使用的方法。与块和 proc 一样,lambda 是闭包,但与前两个不同,它强制执行 arity,并且从 lambda 返回退出 lambda,而不是包含范围。您可以通过将一个块传递给 lambda 方法来创建一个,或者在 ruby 1.9 中传递给 ->
l = lambda {|msg| puts msg} #ruby 1.8
l = -> {|msg| puts msg} #ruby 1.9
l.call('foo') # => foo
只有认真的 ruby 极客才能真正理解这一点 :) 方法是一种将现有函数转换为可以放入变量中的东西的方法。您可以通过调用method 函数并传入一个符号作为方法名称来获得一个方法。您可以重新绑定一个方法,或者如果您想炫耀,您可以将其强制转换为 proc。一种重写以前方法的方法是
l = lambda &method(:puts)
l.call('foo')
这里发生的情况是,您正在为 puts 创建一个方法,将其强制转换为 proc,将其作为 lambda 方法的块的替换传递,而后者又会返回 lambda
如有任何不清楚的地方,请随时询问(在没有 irb 的情况下,在工作日的晚上写这篇文章,希望这不是纯粹的胡言乱语)
编辑:解决 cmets 中的问题
list.map &:bar 我可以用这个语法吗 一个代码块需要超过 一个论点?假设我有 hash = { 0 => “你好”,1 =>“世界”},我想 选择具有 0 的元素 钥匙。也许不是一个很好的例子。 – 布莱恩 沉
这里要深入一点,但要真正了解它的工作原理,您需要了解 ruby 方法调用的工作原理。
基本上,ruby 没有调用方法的概念,发生的是对象相互传递消息。您使用的obj.method arg 语法实际上只是更明确的形式obj.send :method, arg 的糖分,在功能上等同于第一种语法。这是语言中的一个基本概念,这就是为什么 method_missing 和 respond_to? 这样的东西有意义的原因,在第一种情况下,您只是在处理无法识别的消息,而在第二种情况下,您正在检查它是否正在侦听该消息.
要知道的另一件事是相当深奥的“splat”运算符*。根据使用的位置,它实际上做了非常不同的事情。
def foo(bar, *baz)
在方法调用中,如果它是最后一个参数,splat 将使该参数将传递给函数的所有附加参数汇总起来(有点像 C# 中的 params)
obj.foo(bar, *[biz, baz])
在方法调用(或任何其他需要参数列表的方法)中,它会将数组转换为裸参数列表。下面的 sn-p 等价于上面的 sn-p。
obj.foo(bar, biz, baz)
现在,考虑到send 和*,Symbol#to_proc 基本上是这样实现的
class Symbol
def to_proc
Proc.new { |obj, *args| obj.send(self, *args) }
end
end
所以,&:sym 将创建一个新的 proc,在传递给它的第一个参数上调用 .send :sym。如果传递了任何额外的 args,它们将被整合到一个名为 args 的数组中,然后被分配到 send 方法调用中。
我注意到 & 用在三个 地点:def foo(&block)、list.map &:bar 和 l = lambda &method(:puts)。 它们是否具有相同的含义? – 布莱恩·沉
是的,他们有。一个 & 会打电话给to_proc 它旁边有什么。在方法定义的情况下,它在最后一个参数上具有特殊含义,您将在其中拉入定义为块的协同例程,并将其转换为 proc。方法定义实际上是语言中最复杂的部分之一,在参数以及参数的放置中可以有大量的技巧和特殊含义。
b = {0 => "df", 1 => "kl"} p b.select {|键、值|关键零? } 我试过了 将此转换为 p b.select &:zero?, 但它失败了。我想那是因为 代码的参数数量 块是两个,但是&:零?只可以 取一个参数。有什么办法可以吗 去做? – 布莱恩·沉
这应该早点解决,不幸的是你不能用这个技巧来解决这个问题。
“方法是将现有的 功能成你可以放入的东西 一个变量。”为什么是 l = method(:puts) 不够?什么是 lambda & 在这种情况下是什么意思? – 布莱恩·沉
这个例子非常做作,我只是想展示与前面的例子等效的代码,我将一个过程传递给lambda 方法。稍后我会花一些时间重新编写该位,但您是对的,method(:puts) 完全足够了。我试图展示的是,您可以在任何需要阻塞的地方使用&method(:puts)。一个更好的例子是这个
['hello', 'world'].each &method(:puts) # => hello\nworld
l = -> {|msg|放味精}#ruby 1.9: 这对我不起作用。在我之后 检查了Jörg的答案,我认为 应该是 l = -> (msg) {puts msg}。要么 也许我使用的版本不正确 红宝石?我的是红宝石 1.9.1p738 – 布莱恩·沉
就像我在帖子中所说的那样,我在写答案时没有可用的 irb,你是对的,我搞砸了(我大部分时间都花在 1.8.7 上,所以我不是已经习惯了新的语法)
刺头和括号之间没有空间。试试l = ->(msg) {puts msg}。这种语法实际上有很多阻力,因为它与语言中的其他所有内容都如此不同。
【讨论】:
UnboundMethod,它是Method。 UnboundMethod 是不绑定到任何 self 的方法。它们是使用Module#instance_method 系列方法创建的。它们需要首先绑定到接收器,接收器会将它们转换为Method,然后可以调用它。顺便说一句:为什么不在最后一个例子中做l = method(:puts)?如果您想要演示Methods 的工作原理,为什么要将Method 解包成一个块,然后重新包装成Proc?
list.map &:bar 我可以将此语法用于包含多个参数的代码块吗?假设我有hash = { 0 => "hello", 1 => "world" },我想选择以 0 作为键的元素。也许不是一个很好的例子。
& 用在三个地方:def foo(&block)、list.map &:bar 和l = lambda &method(:puts)。它们的含义相同吗?
这两者本质上是一样的吗?他们看起来和我很相似。
它们非常不同。
首先,C# 中的 lambdas 做了两个非常不同的事情,其中只有一个在 Ruby 中具有等价物。 (相当于惊喜的是 lambdas,而不是块。)
在 C# 中,lambda 表达式文字被重载。 (有趣的是,据我所知,它们是 only 重载的文字。)并且它们的 result 类型 是重载的。 (同样,它们是 C# 中唯一可以在其结果类型上重载的东西,方法只能在其参数类型上被重载。)
C# lambda 表达式文字可以或者是一段匿名的可执行代码或一个匿名的可执行代码的抽象表示,这取决于他们的结果类型是Func / Action 还是Expression。
Ruby 没有任何等效的后一种功能(嗯,有解释器特定的非可移植非标准化扩展)。前一个功能的等价物是 lambda,而不是块。
lambda 的 Ruby 语法与 C# 非常相似:
->(x, y) { x + y } # Ruby
(x, y) => { return x + y; } // C#
在 C# 中,如果您只有一个表达式作为正文,则可以删除 return、分号和花括号:
->(x, y) { x + y } # Ruby
(x, y) => x + y // C#
如果你只有一个参数,你可以去掉括号:
-> x { x } # Ruby
x => x // C#
在 Ruby 中,如果参数列表为空,则可以省略:
-> { 42 } # Ruby
() => 42 // C#
在 Ruby 中使用文字 lambda 语法的替代方法是将块参数传递给 Kernel#lambda 方法:
->(x, y) { x + y }
lambda {|x, y| x + y } # same thing
这两者之间的主要区别在于您不知道lambda 做了什么,因为它可以被覆盖、覆盖、包装或以其他方式修改,而文字的行为不能在 Ruby 中修改。
在 Ruby 1.8 中,您也可以使用 Kernel#proc,尽管您应该避免使用这种方法,因为该方法在 1.9 中的作用有所不同。
Ruby 和 C# 的另一个区别是 调用 lambda 的语法:
l.() # Ruby
l() // C#
即在 C# 中,调用 lambda 的语法与调用其他任何东西的语法相同,而在 Ruby 中,调用方法的语法与调用任何其他类型的可调用对象的语法不同。
另一个区别是,在 C# 中,() 内置于语言中,仅适用于某些内置类型,如方法、委托、Actions 和 Funcs,而在 Ruby 中,.() 只是.call() 的语法糖,因此只需实现 call 方法就可以与 any 对象一起使用。
那么,究竟什么是 lambda?好吧,它们是Proc 类的实例。除了有一点点复杂:实际上有两种不同类型的 Proc 类实例,它们略有不同。 (恕我直言,Proc 类应该为两种不同类型的对象分成两个类。)
特别是,并非所有Procs 都是 lambda。您可以通过调用Proc#lambda? 方法检查Proc 是否为lambda。 (通常的约定是将 lambda Procs 称为“lambdas”,而将非 lambda Procs 称为“procs”。)
通过将块传递给 Proc.new 或 Kernel#proc 来创建非 lambda 过程。但是请注意,在 Ruby 1.9 之前,Kernel#proc 创建的是 lambda,而不是 proc。
有什么区别?基本上,lambda 的行为更像方法,proc 的行为更像块。
如果您关注了有关 Java 8 邮件列表的 Lambda 项目的一些讨论,您可能会遇到这样的问题,即完全不清楚非本地控制流应该如何处理 lambda。特别是,在 lambda 中,return 存在三种可能的合理行为(嗯,三种可能,但只有两种真正合理):
最后一个有点不确定,因为通常该方法已经返回了,但其他两个都非常有意义,而且没有一个比另一个更正确或更明显。 Java 8 的 Lambda 项目的当前状态是它们使用两个不同的关键字(return 和 yield)。 Ruby 使用了两种不同的Procs:
它们在处理参数绑定的方式上也有所不同。同样,lambda 的行为更像方法,而 proc 的行为更像块:
nil
Array(或响应 to_ary)并且 proc 有多个参数,数组将被解包并且元素绑定到参数(就像他们在解构赋值的情况下一样)块本质上是一个轻量级的过程。 Ruby 中的每个 方法都有恰好一个 块参数,它实际上并没有出现在其参数列表中(稍后会详细介绍),即是隐式的。这意味着在每个方法call上你可以传递一个块参数,不管方法是否期望它。
由于该块没有出现在参数列表中,因此没有可以用来引用它的名称。那么,你如何使用它呢?好吧,你唯一能做的两件事(不是真的,但稍后会更多)是调用它通过yield 关键字隐式地检查一个块是否通过block_given? 传递。 (由于没有名字,你不能使用call 或nil? 方法。你会怎么称呼它们?)
大多数 Ruby 实现都以非常轻量级的方式实现块。特别是,它们实际上并没有将它们实现为对象。但是,由于它们没有名称,因此您无法引用它们,因此实际上无法判断它们是否是对象。您可以将它们视为 procs,这使得它更容易,因为要记住一个不太不同的概念。只需将它们实际上并未作为块实现的事实视为编译器优化。
to_proc 和 &
是实际上是一种引用块的方式:& sigil / 修饰符 / 一元前缀运算符。它只能出现在参数列表和参数列表中。
在参数列表中,它的意思是“将隐式块包装到一个proc中并绑定到这个名字”。在参数列表中,它的意思是“解开这个Proc到一个块中”。
def foo(&bar)
end
在方法内部,bar 现在绑定到代表块的 proc 对象。例如,这意味着您可以将其存储在实例变量中以供以后使用。
baz(&quux)
在这种情况下,baz 实际上是一个接受零参数的方法。当然,它采用所有 Ruby 方法采用的隐式块参数。我们正在传递变量quux 的内容,但首先将其展开成一个块。
这种“展开”实际上不仅适用于Procs。 & 首先在对象上调用 to_proc,将其转换为 proc。这样,任何对象都可以转换成块。
使用最广泛的例子是Symbol#to_proc,我相信它在 90 年代后期的某个时候首次出现。当它被添加到 ActiveSupport 时,它变得流行起来,并从那里传播到 Facets 和其他扩展库。最后,它被添加到 Ruby 1.9 核心库并向后移植到 1.8.7。很简单:
class Symbol
def to_proc
->(recv, *args) { recv.send self, *args }
end
end
%w[Hello StackOverflow].map(&:length) # => [5, 13]
或者,如果您将类解释为用于创建对象的函数,您可以这样做:
class Class
def to_proc
-> *args { new *args }
end
end
[1, 2, 3].map(&Array) # => [[nil], [nil, nil], [nil, nil, nil]]
Methods 和 UnboundMethods另一个代表一段可执行代码的类是Method 类。 Method 对象是方法的具体代理。您可以通过在 any 对象上调用 Object#method 并传递要具体化的方法的名称来创建 Method 对象:
m = 'Hello'.method(:length)
m.() #=> 5
或使用方法引用运算符.::
m = 'Hello'.:length
m.() #=> 5
Methods 回复to_proc,所以你可以在任何你可以传递块的地方传递它们:
[1, 2, 3].each(&method(:puts))
# 1
# 2
# 3
UnboundMethod 是尚未绑定到接收器的方法的代理,即尚未定义 self 的方法。您不能调用UnboundMethod,但可以将bind 调用为一个对象(它必须是您从中获取方法的模块的实例),这会将其转换为Method。
UnboundMethod 对象是通过调用 Module#instance_method 系列中的一种方法创建的,并将方法名称作为参数传递。
u = String.instance_method(:length)
u.()
# NoMethodError: undefined method `call' for #<UnboundMethod: String#length>
u.bind(42)
# TypeError: bind argument must be an instance of String
u.bind('Hello').() # => 5
就像我在上面已经暗示的那样:Procs 和 Methods 并没有什么特别之处。 任何响应call的对象都可以被调用,任何响应to_proc的对象都可以转换为Proc,从而解包成一个块并传递到一个需要一个块的方法。
lambda 表达式是否借鉴了 Ruby 的思想?
可能不会。大多数现代编程语言都有某种形式的匿名文字代码块:Lisp (1958)、Scheme、Smalltalk (1974)、Perl、Python、ECMAScript、Ruby、Scala、Haskell、C++、D、Objective-C,甚至 PHP(! )。当然,整个想法可以追溯到 Alonzo Church 的 λ-calculus(1935 年甚至更早)。
【讨论】:
&、to_proc 和 Symbol#to_proc 以及其他几个地方有一些重复,但大多数情况下,这两个答案本身就很好。