一窝 Lambda 函数
lambda 表达式、匿名函数以及所有这些东西到底是怎么回事?
引言
最近我在网上越来越多地看到 lambda 这个主题,尤其是在我寻找有关 C# 和/或 LINQ 的信息时。“Lambda”抽象指的是未命名(匿名)的函数,它们可以接受指定的参数并可以返回指定的值,但在源代码中没有正式声明。这在需要一个函数接收另一个函数作为参数,但又不需要重用那个函数的情况下非常方便。例如,假设你编写了一个“执行 X 十次”的函数,其中 X 可以是任何其他函数;你已经封装了“执行十次”的部分,这样就不必为每个 X 重新创建它。你可以为每个要执行的 X 创建一个命名函数并在需要时将其作为参数传递,但如果某个特定的 X 非常简单,并且不打算从程序的其他地方调用,那么直接在代码中嵌入一个小函数而无需声明它可能会很有用。
近年来,包括 .NET 语言在内的流行编程语言都增加了使用匿名函数的功能。结果,阅读相关文章可能会给人一种印象,认为它对于计算领域来说是全新的东西;这让我觉得很讽刺,因为我上次听说它们已经是很久以前的事了,我几乎都记不清了。然而,它们又回来了,要理解它们到底是怎么回事,最好的办法就是翻出一些旧资料,并将其与新实现进行比较。我将把这当作一项研究调查。
往事回顾
快速搜索表明,lambda 表达式和匿名函数起源于 Alonzo Church 在 1936 年发明的“lambda 演算”,并且自 1958 年的 Lisp 以来一直是编程语言的一个特性。我曾经上过一门包含 Lisp 编程的课程,那是我以前听说 lambda 的地方。让我们挖掘一下 Lisp 并构建一个小的例子。我使用了 GNU CLISP,它是 Common Lisp 的一个实现。
简单示例
现在,我需要一个尽可能简单的示例问题作为例子,可以在各种平台上重现。因此,假设我们想要对任何单整数操作进行 0 到 9 的循环。然后我们可以用一个显示输入两倍的函数来尝试循环,再用一个显示输入平方的函数来尝试。如果你喜欢,可以添加更多。请注意,内部函数将各自产生副作用但不返回值;这只是为了更容易地看到函数正在执行。在测试我们的函数时,我们应该看到一个显示
0
2
4
6
8
10
12
14
16
18
用于第一种情况,以及
0
1
4
9
16
25
36
49
64
81
用于第二种情况。
在 Lisp 中
作为变量
那么,我们如何在 Lisp 中做到这一点呢?让我们取一个变量 n
,并用 (print (+ n n))
打印它的两倍(注意:对于非 Lisp 程序员来说,这可能看起来很奇怪,因为 +
是一个函数名,Lisp 不支持中缀运算符,函数名和每个参数都放在括号内,用空格分隔)。要将一个值传递给参数 n
,我们需要使用 lambda
宏将表达式转换为匿名函数,结果是 (lambda (n) (print (+ n n)))
。最后,我们可以将这个函数赋给一个变量 fn
,这样我们就可以将其用作其他东西的输入。
(setq fn (lambda (n) (print (+ n n))))
现在我们所要做的就是在 0-9 循环中调用 fn
所引用的函数
(dotimes (i 9) (funcall fn i))
这将打印第一个示例结果(后面跟着 NIL,因为没有东西会消耗最终的返回值)。要获得第二个结果,只需将函数更改为计算 (* n n)
。
作为参数
另一种方法是将循环定义为一个函数(我们称之为 Loop10
),并为每个测试用例传入一个匿名函数。
(defun Loop10 (fn)
(loop for i from 0 to 9 do
(funcall fn i)
)
)
(Loop10 (lambda (n) (print (+ n n))))
(Loop10 (lambda (n) (print (* n n))))
重要的是,lambda 只是提供了一种向函数传递参数的方式,而无需命名该函数。
在C#中
作为变量
现在,在 C# 中实现。以下与第一个 Lisp 示例相同,我们先将一个函数赋给一个变量,循环它,然后将另一个不同的函数重新赋给同一个变量并再次循环。
delegate void lx(int x);
static void Main(String[] args)
{
lx fn = n => { Console.WriteLine(n + n); };
for (int i = 0; i < 10; i++) { fn(i); }
fn = n => { Console.WriteLine(n * n); };
for (int i = 0; i < 10; i++) { fn(i); }
Console.ReadLine();
}
它有点冗长,因为 C# 将函数分配给委托而不是无类型变量。(注意:Console.ReadLine();
只是为了让程序等待我们阅读输出。)
令人困惑的部分是 =>
符号,它表示匿名函数或 lambda。它被认为是“语法糖”,据说可以使代码更容易阅读,尽管我不确定我是否同意,考虑到像 C++ 流运算符这样的其他语法糖并没有延续到 C# 中。从积极的一面来看,C# 允许你通过变量名调用函数,而不是用另一个函数来解析它。
要解开这个表达式,我们必须将 =>
读作一个中缀运算符,其中代码块作为右侧表达式,参数作为左侧表达式。最好的策略就是记住 n =>
意味着 lambda (n)
。另请注意,匿名函数的返回类型由编译器根据整个表达式确定;在这种情况下,表达式不返回值,因此返回类型是 void
。
作为参数
现在我们可以重新创建 Loop10
函数,并像第二个 Lisp 示例中那样将 lambda 传递给它。
delegate void lx(int x);
static void Loop10(lx fn)
{
for (int i = 0; i < 10; i++)
{
fn(i);
}
}
static void Main(String[] args)
{
Loop10(n => { Console.WriteLine(n + n); });
Loop10(n => { Console.WriteLine(n * n); });
Console.ReadLine();
}
为了节省为整数函数定义委托的额外开销,“M 的 Chris”建议使用预定义的委托“Action<int>
”,如下所示,这样就不会不必要地冗长。
static void Loop10(Action<int> fn)
{
for (int i = 0; i < 10; i++)
{
fn(i);
}
}
static void Main(String[] args)
{
Loop10(n => { Console.WriteLine(n + n); });
Loop10(n => { Console.WriteLine(n * n); });
Console.ReadLine();
}
在 JavaScript 中
JavaScript 程序一直使用匿名函数,尤其是在从外部向 DOM 元素分配脚本时。我在调用 jQuery 函数时多次使用过它们。我们需要模拟一个 JavaScript 控制台来尝试我们的示例问题。
我修改了一些代码,用于一个控制台,这个例子是我在其他地方找到的。我在 jsfiddle 上测试时它能正常工作。首先,我们需要一个放置输出的地方
<!-- Somewhere on the Web Page -->
<pre id="output"></pre>
然后在我们的测试之前的 JavaScript 中,我们添加一个模拟的控制台显示函数。
//******* Begin Test Harness Code *************
function display()
{
var args = Array.prototype.slice.call(arguments, 0);
document.getElementById('output').innerHTML += args.join(" ") + "\n";
}
//******* End Test Harness Code *************
作为变量
现在这些都完成了,我们可以使用函数变量来尝试我们的测试用例。
var fn = (function(n){display(n + n);});
for (var i = 0; i < 10; i++) {fn(i);}
fn = (function(n){display(n * n);});
for (var i = 0; i < 10; i++) {fn(i);}
它看起来像一个常规的函数声明,只是名字不见了。它几乎就像一个名为“function
”的函数。这种语法确实表达了意思,而且与命名声明没有太大区别。
作为参数
同样,我们可以重新创建 Loop10
函数并向其传递匿名函数。
function Loop10(fn)
{
for (var i = 0; i < 10; i++)
{
fn(i);
}
}
Loop10(function(n){display(n + n);});
Loop10(function(n){display(n * n);});
在 VB.NET 中
作为变量
为了完整起见,我们应该在 VB.NET 中尝试一下。VB 不关心语法糖,所以你可能会看到与 JavaScript 示例更多的相似之处。
Private Delegate Sub Lx(n As Integer)
Sub Main()
Dim fn As Lx = Sub(n As Integer) Console.WriteLine(n + n)
For i As Integer = 0 To 9
fn(i)
Next
fn = Sub(n As Integer) Console.WriteLine(n * n)
For i As Integer = 0 To 9
fn(i)
Next
Console.ReadLine()
End Sub
顺便说一句,请注意,我们的示例函数会产生副作用并且不返回值,因此在 VB.NET 中,在这种特定情况下,我们最终使用 Sub
关键字而不是 Function
关键字。
作为参数
现在我们可以再次重新创建 Loop10
函数,并向其传递匿名函数——或子例程。
Private Delegate Sub Lx(n As Integer)
Private Sub Loop10(fn As Lx)
For i As Integer = 0 To 9
fn(i)
Next
End Sub
Sub Main()
Loop10(Sub(n As Integer) Console.WriteLine(n + n))
Loop10(Sub(n As Integer) Console.WriteLine(n * n))
Console.ReadLine()
End Sub
这种语法比其他语言稍微冗长一些,但与 JavaScript 一样,语法与命名声明没有太大变化。
在 Python 3 中
作为变量
好吧,为什么要停下来呢?Python 是最简洁的现代语言之一,那么 lambda 在 Python 中是什么样的呢?
fn = lambda n: print(n + n)
for i in range(0,9):
fn(i)
fn = lambda n: print(n * n)
for i in range(0,9):
fn(i)
看起来 lambda
关键字回来了!这个翻译相对容易阅读,所以如果我之前不熟悉 Lisp 中的 lambda,这可能是一个很好的起点。
作为参数
我开始习惯翻译这些了。现在是 Loop10
函数版本。
def Loop10(fn):
for i in range(0,9):
fn(i)
Loop10(lambda n: print(n + n))
Loop10(lambda n: print(n * n))
语法简洁明了,没有晦涩难懂。这是我喜欢 Python 的一点。
在 D 语言中
作为变量
我是一个 D 语言的新手,但是 D 语言有一个我们可以使用的 function
关键字。
import std.stdio;
int main(string[] argv)
{
void function(int x) fn;
fn = function void(int n) {writeln(n + n); };
for (int i = 0; i < 10; i++) {fn(i); }
fn = function void(int n) {writeln(n * n); };
for (int i = 0; i < 10; i++) {fn(i); }
readln();
return 0;
}
D 语言最近增加了对 lambda 的 =>
语法支持,但是我使用的编译器在使用此语法处理等效表达式时,没有选择 void function(int)
签名;也许它以 Lisp 的方式间接引用了函数。使用 auto
而不是 function
或 delegate
来接收输出可以解决数据类型问题,但对我来说,writeln
由于某种原因没有产生输出——或者输出被定向到其他地方。就我个人而言,我更喜欢我们这里使用的语法,它可以提供一个明确的返回类型,但是如果我或读者找到一种使用符号简写来编写它的方法,我就会将其添加到本文中。
作为参数
这是 loop10
版本。
import std.stdio;
void loop10(void function(int x) fn)
{
for (int i = 0; i < 10; i++)
{
fn(i);
}
}
int main(string[] argv)
{
loop10(function void(int n) {writeln(n + n);});
loop10(function void(int n) {writeln(n * n);});
readln();
return 0;
}
在 F# 中
作为变量
一直以来,Lisp 是我在这项调查中唯一使用的真正的函数式语言。函数式语言是 lambda 最普遍的地方,所以我们应该至少再尝试一种。Visual Studio 附带 F#,但我从未使用过它,所以我对此一无所知。让我们看看我最终会得到什么。
let mutable fn = (fun n -> printfn "%d" (n + n))
for i=0 to 9 do
fn(i)
fn <- (fun n -> printfn "%d" (n * n))
for i=0 to 9 do
fn(i)
System.Console.ReadLine() |> ignore
这有点复杂,因为正如我刚刚发现的,F# 并不真正喜欢变量,所以我们必须使用 mutable
关键字和 <-
运算符。
显然,在 F# 中,lambda 是 fun
。这个词和 ->
将参数与代码主体分开,
作为参数
幸运的是,Loop10
版本使用了参数而不是变量,因此更容易阅读。实际上,它与 Python 示例一样简洁。
let Loop10 fn =
for i=0 to 9 do
fn(i)
Loop10(fun n -> printfn "%d" (n + n))
Loop10(fun n -> printfn "%d" (n * n))
System.Console.ReadLine() |> ignore
结论
一些编程语言对匿名函数使用的语法看起来与它们的命名函数声明语法相似,但另一些则与此模型大相径庭。在下面的表格中,我显示了一个独立的示例,比较了命名函数和匿名函数之间关键字变化的部分,并用粗体突出显示。
语言 | 命名声明 | 匿名 / Lambda |
Lisp | (defun PrintDouble (n) (print (+ n n)))) | (lambda (n) (print (+ n n))) |
C# | static void PrintDouble(int n) {Console.WriteLine(n + n);} | n => {Console.WriteLine(n + n);} |
JavaScript | function PrintDouble(n){display(n + n);} | function(n){display(n + n);} |
VB.NET |
|
|
Python 3 | def PrintDouble(n): print(n + n) | lambda n: print(n * n) |
D | void printDouble(int n) {writeln(n + n);} | function void(int n) {writeln(n + n);} |
F# |
| (fun n -> printfn "%d" (n + n)) |
JavaScript 和 VB.NET 版本除了函数名消失之外,没有发生重大变化。Lisp、Python 3 和 D 语言版本只是用一个关键字替换了函数名和声明关键字。C# 以及使用 C# 语法的 D 语言用一个位于新位置的符号替换了函数名和声明关键字,并强制使用隐式返回类型。F# 用一个新关键字和一个新符号替换了声明关键字、名称和绑定运算符。我认为更多的差异意味着更多的混淆空间。
在以这种方式比较不同平台之前,尽管我曾在 Lisp 中接触过 lambda,并在 JavaScript 中广泛使用过匿名函数,但 C# 中不一致的语法使得很难看到它们之间的相似之处。通过在所有三种语言中比较相同的测试用例,它们被联系起来并为我澄清了。此外,我能够将这种清晰度扩展到另外四种语言。我发现比较不同平台上的概念有助于理解(就像罗塞塔石碑),我强烈推荐这种方法;你可以使用任何你觉得有用的语言组合,在其他概念上尝试一下。
历史
- 2015年4月16日:首次发布