Lambda
2004 年 9 月 8 日
随着人们对动态语言的兴趣日益浓厚,越来越多的人开始接触到一种名为 Lambda 的编程概念(也称为闭包、匿名函数或块)。来自 C/C++/Java/C# 语言背景的人没有 Lambda,因此不确定它们是什么。这里有一个简短的解释,那些在支持 Lambda 的语言中进行过相当多编程的人可能不会觉得这很有趣。
Lambda 已经存在很长时间了。我第一次在 Smalltalk 中真正遇到它们,在那里它们被称为块。Lisp 广泛使用它们。它们也存在于 Ruby 脚本语言中,并且是许多 Ruby 程序员喜欢使用 Ruby 进行脚本编写的主要原因之一。
本质上,Lambda 是一段可以作为参数传递给函数调用的代码块。我将用一个简单的例子来说明这一点。假设我有一个员工对象的列表,我想得到一个包含所有管理者的列表,我通过一个 IsManager 属性来确定。使用 C#,我可能会这样写。
public static IList Managers(IList emps) { IList result = new ArrayList(); foreach(Employee e in emps) if (e.IsManager) result.Add(e); return result; }
在支持 Lambda 的语言中,在本例中是 Ruby,我会这样写。
def managers(emps) return emps.select {|e| e.isManager} end
本质上,select 是 Ruby 集合类中定义的一个方法。它接受一个代码块,即一个 Lambda,作为参数。在 Ruby 中,你可以在花括号之间编写代码块(这不是唯一的方法)。如果代码块接受任何参数,你将在竖线之间声明这些参数。select 的作用是遍历输入数组,对每个元素执行代码块,并返回一个数组,其中包含代码块计算结果为真的元素。
现在,如果你是一个 C 程序员,你可能会想“我可以使用函数指针做到这一点”,如果你是一个 Java 程序员,你可能会想“我可以使用匿名内部类做到这一点”。这些机制与 Lambda 类似,但有两个明显的区别。
第一个是形式上的区别,Lambda 通常定义闭包,这意味着它们可以引用在定义时可见的变量。考虑这个方法
def highPaid(emps) threshold = 150 return emps.select {|e| e.salary > threshold} end
请注意,select 块中的代码引用了在封闭方法中定义的局部变量。许多在不支持真正的闭包的语言中替代 Lambda 的方法无法做到这一点。Lambda 允许你做更多有趣的事情。考虑这个函数。
def paidMore(amount) return lambda {|e| e.salary > amount} end
这个函数返回一个 Lambda,实际上它返回一个 Lambda,其行为取决于发送给它的参数。我可以使用这个函数并将其分配给一个变量。
highPaid = paidMore(150)
变量 highPaid 包含一段代码,它将返回测试对象是否拥有高于 150 的薪水。我可能会这样使用它。
john = Employee.new john.salary = 200 print highPaid.call(john)
表达式 highPaid.call(john) 调用了我之前定义的 e.salary > amount 代码,其中该代码中的 amount 变量绑定到我在创建 proc 对象时传入的 150。即使该 150 值在发出 print 调用时超出范围,绑定仍然存在。
因此,关于 Lambda 的第一个关键点是它们通常创建闭包,即一段代码加上对它们来自的环境的绑定。你可以拥有不创建闭包的 Lambda,但这种 Lambda 没什么用,因此并不常见,这就是为什么闭包通常被用作 Lambda 的替代术语的原因。 [1][2]
第二个区别不是一个明确的形式上的区别,但在实践中同样重要,甚至更重要。支持 Lambda 的语言允许你使用很少的语法来定义它们。虽然这可能看起来并不重要,但我认为这是至关重要的,它是使它们能够频繁使用的关键。看看 Lisp、Smalltalk 或 Ruby 代码,你会发现 Lambda 无处不在,比其他语言中类似的结构使用得更频繁。绑定到局部变量的能力是其中的一部分,但我认为最大的原因是使用它们的表示法简单明了。
一个很好的例子是,当 ex-Smalltalkers 开始使用 Java 时发生了什么。最初,包括我在内的许多人尝试使用匿名内部类来完成我们在 Smalltalk 中使用块完成的许多事情。但最终生成的代码太混乱和丑陋,所以我们放弃了。
我在 Ruby 中经常使用 Lambda,但我不会显式地创建它们并传递它们。大多数情况下,我对 Lambda 的使用是基于类似于我之前展示的 select 方法的集合管道。另一个常见用途是“执行周围方法”,例如处理文件时。
File.open(filename) {|f| doSomethingWithFile(f)}
这里,open 方法打开文件,执行提供的代码块,然后关闭它。这对于处理事务(记住提交或回滚)或任何你必须记住在最后做某事的事情来说可能非常方便。我在我的 xml 转换例程中广泛使用它。
这种对 Lambda 的使用实际上远不如 Lisp 和函数式编程世界中的人所做的那样。但即使在我的有限使用中,我在没有 Lambda 的语言中编程时也很想念它们。它们是那些你第一次遇到时看起来很小,但你很快就会喜欢上的东西之一。
Neal Gafter 在 闭包的历史 上有一篇很棒的帖子。Vadim Nasardinov 指引我找到了这个关于 Java 中闭包 的有趣历史片段,来自 Guy Steele。
笔记
1: 当我第一次在 2004 年发布这个 bliki 条目时,我使用术语“闭包”来指代这些语言特性。当时,“闭包”经常以这种方式使用。从那时起,“Lambda”这个术语变得更加流行,所以我改变了这个 bliki 条目以遵循这种用法。
2: Java 的匿名内部类可以访问局部变量,但前提是这些变量是 final 的。