提问人:sker 提问时间:10/21/2008 最后编辑:hippietrailsker 更新时间:1/31/2023 访问量:178222
“闭包”和“lambda”有什么区别?
What is the difference between a 'closure' and a 'lambda'?
答:
并非所有闭包都是 lambda,也不是所有的 lambda 都是闭包。两者都是函数,但不一定是我们习惯知道的方式。
lambda 本质上是一个内联定义的函数,而不是声明函数的标准方法。Lambda 经常可以作为对象传递。
闭包是一种函数,它通过引用其主体外部的字段来封闭其周围状态。封闭状态在闭包调用之间保持不变。
在面向对象的语言中,闭包通常通过对象提供。然而,一些面向对象语言(例如 C#)实现了更接近于纯函数式语言(例如 lisp)提供的闭包定义的特殊功能,这些语言没有要包含状态的对象。
有趣的是,C# 中 Lambdas 和 Closures 的引入使函数式编程更接近主流用法。
评论
当大多数人想到函数时,他们会想到命名函数:
function foo() { return "This string is returned from the 'foo' function"; }
当然,这些是按名称命名的:
foo(); //returns the string above
使用 lambda 表达式,您可以拥有匿名函数:
@foo = lambda() {return "This is returned from a function without a name";}
在上面的示例中,您可以通过分配给它的变量调用 lambda:
foo();
然而,比将匿名函数分配给变量更有用的是将它们传递给高阶函数或从高阶函数传递,即接受/返回其他函数的函数。在很多情况下,命名函数是不必要的:
function filter(list, predicate)
{ @filteredList = [];
for-each (@x in list) if (predicate(x)) filteredList.add(x);
return filteredList;
}
//filter for even numbers
filter([0,1,2,3,4,5,6], lambda(x) {return (x mod 2 == 0)});
闭包可以是命名函数或匿名函数,但当它“闭合”定义函数的范围内的变量时,即闭包仍将引用具有闭包本身中使用的任何外部变量的环境。下面是一个命名的闭包:
@x = 0;
function incrementX() { x = x + 1;}
incrementX(); // x now equals 1
这似乎不多,但是如果这一切都在另一个函数中并且您传递给外部函数呢?incrementX
function foo()
{ @x = 0;
function incrementX()
{ x = x + 1;
return x;
}
return incrementX;
}
@y = foo(); // y = closure of incrementX over foo.x
y(); //returns 1 (y.x == 0 + 1)
y(); //returns 2 (y.x == 1 + 1)
这就是在函数式编程中获得有状态对象的方式。由于不需要命名“incrementX”,因此在本例中可以使用 lambda:
function foo()
{ @x = 0;
return lambda()
{ x = x + 1;
return x;
};
}
评论
lambda 只是一个匿名函数 - 一个没有名称的函数。在某些语言(如 Scheme)中,它们等同于命名函数。事实上,函数定义被重写为在内部将 lambda 绑定到变量。在其他语言中,如 Python,它们之间存在一些(相当不必要的)区别,但它们在其他方面的行为方式相同。
闭包是在定义它的环境中闭合的任何函数。这意味着它可以访问不在其参数列表中的变量。例子:
def func(): return h
def anotherfunc(h):
return func()
这将导致错误,因为 - 中的环境未关闭。 仅在全球环境中关闭。这将起作用:func
anotherfunc
h
func
def anotherfunc(h):
def func(): return h
return func()
因为这里,在 和 python 2.3 及更高版本(或类似的数字)中定义,当他们几乎正确地闭包时(突变仍然不起作用),这意味着它关闭了 的环境并可以访问其中的变量。在 Python 3.1+ 中,使用 nonlocal
关键字时,mutation 也有效。func
anotherfunc
anotherfunc
另一个重要的点 - 将继续关闭 的环境,即使它不再在 中进行评估。此代码也适用:func
anotherfunc
anotherfunc
def anotherfunc(h):
def func(): return h
return func
print anotherfunc(10)()
这将打印 10。
正如你所注意到的,这与 lambdas 无关——它们是两个不同(尽管相关)的概念。
评论
从编程语言的角度来看,它们是完全不同的东西。
基本上,对于图灵完备语言,我们只需要非常有限的元素,例如抽象、应用和约简。抽象和应用提供了构建 lamdba 表达式的方法,而 reduc 则揭示了 lambda 表达式的含义。
Lambda 提供了一种将计算过程抽象出来的方法。 例如,为了计算两个数字的总和,可以抽象出一个接受两个参数 x、y 并返回 x+y 的过程。在 scheme 中,您可以将其写为
(lambda (x y) (+ x y))
您可以重命名参数,但其完成的任务不会更改。 在几乎所有的编程语言中,您都可以为 lambda 表达式指定一个名称,这些名称是命名函数。但是没有太大的区别,它们在概念上可以被认为是语法糖。
好了,现在想象一下如何实现这一点。每当我们将 lambda 表达式应用于某些表达式时,例如
((lambda (x y) (+ x y)) 2 3)
我们可以简单地将参数替换为要计算的表达式。这个模型已经非常强大了。 但是这个模型不能让我们改变符号的值,例如,我们不能模仿状态的变化。因此,我们需要一个更复杂的模型。 简而言之,每当我们想要计算 lambda 表达式的含义时,我们都会将对符号和对应的值放入环境(或表)中。然后通过查找表中的相应符号来评估其余 (+ x y)。 现在,如果我们提供一些原语来直接对环境进行操作,我们就可以对状态的变化进行建模!
在此背景下,检查此功能:
(lambda (x y) (+ x y z))
我们知道,当我们计算 lambda 表达式时,x y 将被绑定到一个新表中。但是我们如何以及在哪里可以查找 z?实际上,z 被称为自由变量。必须有一个外部 包含 z 的环境。否则,不能仅通过绑定 x 和 y 来确定表达式的含义。为了清楚这一点,您可以在 scheme 中编写如下内容:
((lambda (z) (lambda (x y) (+ x y z))) 1)
因此,z 将在外表中绑定到 1。我们仍然得到一个接受两个参数的函数,但它的真正含义也取决于外部环境。 换言之,外部环境在自由变量上关闭。在set!的帮助下,我们可以使函数有状态,也就是说,它不是数学意义上的函数。它返回的内容不仅取决于输入,还取决于 z。
这是你已经非常清楚的事情,对象的方法几乎总是依赖于对象的状态。这就是为什么有人说“闭合是穷人的物品。但是我们也可以把对象看作是穷人的闭包,因为我们真的很喜欢一流的函数。
我用 scheme 来说明这些想法,因为 scheme 是最早具有真正闭包的语言之一。这里的所有材料在 SICP 第 3 章中都有更好的介绍。
总而言之,lambda 和闭包实际上是不同的概念。lambda 是一个函数。闭包是一对 lambda 和关闭 lambda 的相应环境。
评论
就这么简单:lambda 是一种语言结构,即匿名函数的简单语法;闭包是一种实现它的技术 - 或任何一流的函数,就此而言,命名或匿名。
更准确地说,闭包是一类函数在运行时表示的方式,作为其“代码”和环境的一对“闭合”,该代码中使用的所有非局部变量。这样,即使这些变量的来源外部作用域已经退出,仍然可以访问这些变量。
不幸的是,有许多语言不支持函数作为第一类值,或者只支持残缺形式。所以人们经常用“闭合”这个词来区分“真物”。
概念与上面描述的相同,但如果你是PHP背景,这将进一步解释使用PHP代码。
$input = array(1, 2, 3, 4, 5);
$output = array_filter($input, function ($v) { return $v > 2; });
function ($v) { return $v > 2; } 是 lambda 函数定义。我们甚至可以将它存储在一个变量中,这样它就可以重用:
$max = function ($v) { return $v > 2; };
$input = array(1, 2, 3, 4, 5);
$output = array_filter($input, $max);
现在,如果要更改筛选数组中允许的最大数量,该怎么办?您必须编写另一个 lambda 函数或创建一个闭包 (PHP 5.3):
$max_comp = function ($max) {
return function ($v) use ($max) { return $v > $max; };
};
$input = array(1, 2, 3, 4, 5);
$output = array_filter($input, $max_comp(2));
闭包是在其自己的环境中计算的函数,该环境中具有一个或多个绑定变量,这些变量可以在调用函数时访问。它们来自函数式编程领域,其中有许多概念在起作用。闭包类似于 lambda 函数,但更智能,因为它们能够与定义闭包的外部环境中的变量进行交互。
下面是一个更简单的PHP闭包示例:
$string = "Hello World!";
$closure = function() use ($string) { echo $string; };
$closure();
这个问题很古老,有很多答案。
现在,Java 8 和官方 Lambda 是非官方的闭包项目,它重新提出了这个问题。
Java 上下文中的答案(通过 Lambda 和闭包 — 有什么区别?
“闭包是一个 lambda 表达式,与将其每个自由变量绑定到一个值的环境配对。在 Java 中,lambda 表达式将通过闭包来实现,因此这两个术语在社区中可以互换使用。
评论
简单地说,闭包是关于范围的技巧,lambda 是一个匿名函数。我们可以更优雅地使用 lambda 实现闭包,并且 lambda 通常用作传递给更高函数的参数
关于 lambda 和闭包存在很多混淆,即使在此处对 StackOverflow 问题的回答中也是如此。与其随便问那些从某些编程语言的实践中学到闭包的程序员或其他无知的程序员,不如去源头(一切开始的地方)。由于 lambda 和闭包来自 Alonzo Church 在 30 年代发明的 Lambda 演算,甚至还没有第一台电子计算机,这就是我所说的来源。
Lambda 演算是世界上最简单的编程语言。你唯一能做的事情:►
- 应用:将一个表达式应用于另一个表达式,表示为 .
(把它想象成一个函数调用,其中是函数,是它的唯一参数)f x
f
x
- 抽象:绑定表达式中出现的符号,以标记该符号只是一个“插槽”,一个等待用值填充的空白框,一个“变量”。它是通过在希腊字母 (lambda) 前面加上一个,然后是符号名称(例如),然后在表达式前面加上一个点来完成的。然后,这会将表达式转换为需要一个参数的函数。
例如:接受表达式并告诉此表达式中的符号是一个绑定变量 - 它可以替换为您作为参数提供的值。
请注意,以这种方式定义的函数是匿名的——它没有名称,所以你还不能引用它,但你可以通过向它提供它正在等待的参数来立即调用它(还记得应用程序吗?),如下所示: .然后,表达式(在本例中为文字值)被替换为所应用 lambda 的子表达式,因此得到 ,然后通过通用算术规则简化为。λ
x
.
λx.x+2
x+2
x
(λx.x+2) 7
7
x
x+2
7+2
9
因此,我们解开了一个谜团:
lambda 是上面示例中的匿名函数。λx.x+2
在不同的编程语言中,函数抽象 (lambda) 的语法可能不同。例如,在 JavaScript 中,它看起来像这样:
function(x) { return x+2; }
您可以立即将其应用于某些参数,如下所示:
(function(x) { return x+2; })(7)
或者,您可以将此匿名函数 (lambda) 存储到某个变量中:
var f = function(x) { return x+2; }
这有效地给它起了一个名字,允许您引用它并在以后多次调用它,例如:f
alert( f(7) + f(10) ); // should print 21 in the message box
但你不必给它命名。您可以立即调用它:
alert( function(x) { return x+2; } (7) ); // should print 9 in the message box
在 LISP 中,lambda 是这样制作的:
(lambda (x) (+ x 2))
您可以通过立即将此类 lambda 应用于参数来调用它:
( (lambda (x) (+ x 2)) 7 )
好了,现在是时候解开另一个谜团了:什么是闭包。 为此,让我们谈谈 lambda 表达式中的符号(变量)。
正如我所说,lambda 抽象的作用是在其子表达式中绑定一个符号,使其成为可替换的参数。这样的符号称为绑定。但是,如果表达式中有其他符号呢?例如:。在此表达式中,符号受其前面的 lambda 抽象的约束。但另一个符号,,是没有约束的——它是自由的。我们不知道它是什么,它来自哪里,所以我们不知道它的含义和它代表什么价值,因此,在我们弄清楚意味着什么之前,我们无法评估这个表达。λx.x/y+2
x
λx.
y
y
事实上,其他两个符号也是如此,并且 .只是我们对这两个符号太熟悉了,以至于我们通常会忘记计算机不知道它们,我们需要通过在某个地方定义它们来告诉它它们的含义,例如在图书馆或语言本身中。2
+
你可以把自由符号看作是在表达式的其他地方,在它的“周围上下文”中定义的,这被称为它的环境。环境可能是一个更大的表达式,这个表达式是它的一部分(正如 Qui-Gon Jinn 所说:“总有一条更大的鱼”;)),或者在某个库中,或者在语言本身(作为基元)。
这使我们可以将 lambda 表达式分为两类:
- CLOSED 表达式:这些表达式中出现的每个符号都受到一些 lambda 抽象的约束。换句话说,它们是自给自足的;它们不需要评估任何周围的环境。它们也被称为运算器。
- OPEN 表达式:这些表达式中的某些符号是没有绑定的,也就是说,其中出现的某些符号是自由的,它们需要一些外部信息,因此在提供这些符号的定义之前,无法计算它们。
您可以通过提供环境来关闭打开的 lambda 表达式,该环境通过将所有这些自由符号绑定到某些值(可能是数字、字符串、匿名函数(又名 lambda 等)来定义它们。
这里是闭包部分:
lambda 表达式的闭包是在外部上下文(环境)中定义的一组特定符号,它们为该表达式中的自由符号提供值,使它们不再是非自由符号。它将一个开放的 lambda 表达式(仍然包含一些“未定义”的自由符号)变成一个封闭的表达式,它不再有任何自由符号。
例如,如果您有以下 lambda 表达式: ,则符号是绑定的,而符号是自由的,因此表达式是且不能计算的,除非您说出 what means(与 和 相同,它们也是自由的)。但假设你也有这样的环境:λx.x/y+2
x
y
open
y
+
2
{ y: 3,
+: [built-in addition],
2: [built-in number],
q: 42,
w: 5 }
此环境为我们的 lambda 表达式 (, , ) 和几个额外的符号 (, ) 中的所有“未定义”(自由)符号提供定义。我们需要定义的符号是环境的这个子集:y
+
2
q
w
{ y: 3,
+: [built-in addition],
2: [built-in number] }
而这正是我们的 lambda 表达式的闭包:>
换句话说,它关闭一个打开的 lambda 表达式。这就是名称闭包的最初由来,这就是为什么这么多人在这个线程中的答案不太正确:P
那么他们为什么会弄错呢?为什么这么多人说闭包是内存中的一些数据结构,或者他们使用的语言的某些功能,或者为什么他们将闭包与 lambda 混淆?:P
好吧,Sun/Oracle,Microsoft,Google等企业市场是罪魁祸首,因为这就是他们用自己的语言(Java,C#,Go等)称呼这些结构的原因。他们经常将“闭包”称为“闭包”,而这些闭包应该只是 lambda。或者,他们称“闭包”是他们用来实现词法范围的一种特殊技术,也就是说,函数可以访问在定义时在其外部作用域中定义的变量。他们经常说函数“封闭”了这些变量,也就是说,将它们捕获到某个数据结构中,以免在外部函数完成执行后被破坏。但这只是事后编造的“民俗词源”和营销,这只会让事情变得更加混乱,因为每个语言供应商都使用自己的术语。
更糟糕的是,他们所说的话总是有一点道理,这不会让你轻易将其视为虚假:P让我解释一下:
如果要实现一种使用 lambda 作为一等公民的语言,则需要允许它们使用在其周围上下文中定义的符号(即,在 lambda 中使用自由变量)。即使周围的函数返回,这些符号也必须存在。问题在于,这些符号绑定到函数的某个本地存储(通常在调用堆栈上),当函数返回时,这些存储将不再存在。因此,为了让 lambda 按照您期望的方式工作,您需要以某种方式从其外部上下文中“捕获”所有这些自由变量并保存它们以备后用,即使外部上下文将消失也是如此。也就是说,您需要找到 lambda 的闭包(它使用的所有这些外部变量)并将其存储在其他地方(通过制作副本,或者预先为它们准备空间,而不是在堆栈上)。用于实现此目标的实际方法是语言的“实现细节”。这里重要的是闭包,它是来自 lambda 环境中的一组自由变量,需要保存在某个地方。
没过多久,人们就开始将他们在语言实现中用于实现闭包的实际数据结构称为“闭包”本身。结构通常如下所示:
Closure {
[pointer to the lambda function's machine code],
[pointer to the lambda function's environment]
}
这些数据结构作为参数传递给其他函数,从函数返回,并存储在变量中,以表示 lambda,并允许它们访问其封闭环境以及在该上下文中运行的机器代码。但这只是实现闭包的一种方式(众多方法之一),而不是闭包本身。
正如我上面所解释的,lambda 表达式的闭包是其环境中定义的子集,这些定义为该 lambda 表达式中包含的自由变量提供值,从而有效地关闭表达式(将尚未计算的开放 lambda 表达式转换为封闭的 lambda 表达式,然后可以对其进行计算,因为其中包含的所有符号现在都已定义)。
其他任何事情都只是程序员和语言供应商的“货物崇拜”和“voo-doo magic”,他们不知道这些概念的真正根源。
我希望这能回答你的问题。但是,如果您有任何后续问题,请随时在评论中提问,我会尽力更好地解释。
评论
这取决于函数是否使用外部变量来执行操作。
外部变量 - 在函数范围之外定义的变量。
Lambda 表达式是无状态的,因为它依赖于参数、内部变量或常量来执行操作。
Function<Integer,Integer> lambda = t -> { int n = 2 return t * n }
闭包保持状态是因为它使用外部变量(即在函数体范围之外定义的变量)以及参数和常量来执行操作。
int n = 2 Function<Integer,Integer> closure = t -> { return t * n }
当 Java 创建闭包时,它会将变量 n 与函数一起保留,以便在传递给其他函数或在任何地方使用时可以引用它。
评论
Lambda 表达式只是一个匿名函数。例如,在纯 Java 中,您可以这样编写:
Function<Person, Job> mapPersonToJob = new Function<Person, Job>() {
public Job apply(Person person) {
Job job = new Job(person.getPersonId(), person.getJobDescription());
return job;
}
};
其中类 Function 只是用 java 代码构建的。现在你可以打电话给某个地方来使用它。这只是一个例子。在有语法之前,这是一个 lambda。Lambdas 是实现这一目标的捷径。mapPersonToJob.apply(person)
关闭:
当 Lambda 可以访问此范围之外的变量时,它就会成为闭包。我想你可以说它的魔力,它神奇地可以环绕它所处的环境,并使用其范围(外部范围)之外的变量。 所以需要明确的是,闭包意味着 lambda 可以访问它的 OUTER SCOPE。
在 Kotlin 中,lambda 始终可以访问其闭包(其外部范围内的变量)
Lambda 是一个匿名函数定义,它(不一定)绑定到标识符。
“匿名函数起源于阿朗佐·丘奇(Alonzo Church)发明的lambda演算,其中所有函数都是匿名的” - 维基百科
Closure 是 lambda 函数实现。
“Peter J. Landin 在 1964 年将术语闭包定义为具有环境部分和控制部分,他的 SECD 机器用于评估表达式” - 维基百科,自由的百科全书
其他响应中介绍了 Lambda 和 Closure 的一般解释。
对于具有 C++ 背景的用户,C++ 11 中引入了 Lambda 表达式。将 Lambda 视为创建匿名函数和函数对象的便捷方式。
“lambda 和相应的闭包之间的区别完全等同于类和类实例之间的区别。类仅存在于源代码中;它在运行时不存在。运行时存在的是类类型的对象。闭包之于 lambda,就像对象之于类一样。这应该不足为奇,因为每个 lambda 表达式都会导致生成一个唯一的类(在编译期间),并且还会导致该类类型的对象,即(在运行时)创建闭包。
C++ 允许我们检查 Lambda 和 Closure 的细微差别,因为您必须显式指定要捕获的自由变量。
在下面的示例中,Lambda 表达式没有空闲变量,即空捕获列表 ()。它本质上是一个普通函数,从最严格的意义上讲不需要闭包。因此,它甚至可以作为函数指针参数传递。[]
void register_func(void(*f)(int val)) // Works only with an EMPTY capture list
{
int val = 3;
f(val);
}
int main()
{
int env = 5;
register_func( [](int val){ /* lambda body can access only val variable*/ } );
}
一旦在捕获列表 () 中引入了来自周围环境的自由变量,就必须生成一个 Closure。[env]
register_func( [env](int val){ /* lambda body can access val and env variables*/ } );
由于这不再是一个普通的函数,而是一个闭包,因此它会产生编译错误。
no suitable conversion function from "lambda []void (int val)->void" to "void (*)(int val)" exists
该错误可以通过函数包装器来修复,该包装器接受任何可调用的目标,包括生成的闭包。std::function
void register_func(std::function<void(int val)> f)
有关 C++ 示例的详细说明,请参阅 Lambda 和闭包。
评论
Lambda 与闭包
Lambda
是匿名函数(方法)
Closure
是关闭(捕获)变量的函数,它从其封闭范围(例如非局部变量)关闭
爪哇岛
interface Runnable {
void run();
}
class MyClass {
void foo(Runnable r) {
}
//Lambda
void lambdaExample() {
foo(() -> {});
}
//Closure
String s = "hello";
void closureExample() {
foo(() -> { s = "world";});
}
}
Swift[关闭]
class MyClass {
func foo(r:() -> Void) {}
func lambdaExample() {
foo(r: {})
}
var s = "hello"
func closureExample() {
foo(r: {s = "world"})
}
}
评论
这个问题已经有 12 年的历史了,我们仍然把它作为 Google 中“闭包与 lambda”的第一个链接。 所以我不得不说,因为没有人明确说过。
Lambda 表达式是一个匿名函数(声明)。
引用 Scott 的《编程语言语用学》的结尾解释如下:
...创建引用环境的显式表示(通常是子例程在当前调用时将执行的那个环境),并将其与对子例程的引用捆绑在一起......称为闭包。
也就是说,它就像我们所说的“函数+放弃上下文”的捆绑包一样。
评论
在这个问题的各种现有答案中,有许多技术上模糊或“甚至没有错误”的人造珍珠的声音,所以我最终要添加一个新的......
对术语的澄清
最好知道,术语“闭包”和“lambda”都可以表示不同的事物,具体取决于上下文。
这是一个形式问题,因为正在讨论的 PL(编程语言)规范可能会明确定义此类术语。
例如,通过 ISO C++(从 C++ 11 开始):
lambda-expression 的类型(也是闭包对象的类型)是一种唯一的、未命名的非联合类类型,称为闭包类型,其属性如下所述。
由于类C语言的用户每天都会与“指针”(类型)混淆“指针”(类型)或“指针对象”(类型的居民),因此这里也有混淆的风险:大多数C++用户实际上是通过使用术语“闭包”来谈论“闭包对象”。要小心模棱两可。
注意为了使事情更清晰、更精确,我很少故意使用一些语言中立的术语(通常是特定于 PL 理论的,而不是语言定义的术语。例如,上面使用的类型 inhabitant 涵盖了更广泛意义上的特定于语言的“(r)值”和“lvalues”。(由于 C++ 值类别定义的句法本质无关紧要,因此避免“(l/r)值”可能会减少混淆)。(免责声明:左值和右值在许多其他上下文中很常见。不同 PL 之间未正式定义的术语可能用引号引起来。参考资料的逐字副本也可以用引号括起来,错别字保持不变。
这与“lambda”更相关。(小写)字母 lambda (λ) 是希腊字母表的一个元素。与“lambda”和“closure”相比,人们当然不是在谈论字母本身,而是在使用“lambda”派生概念的语法背后的东西。
现代 PL 中的相关结构通常称为“lambda 表达式”。它源自下面讨论的“lambda 抽象”。
在详细讨论之前,我建议阅读问题本身的一些评论。我觉得它们比这里的大多数答案更安全、更有帮助,因为混淆的风险更小。(可悲的是,这是我决定在这里提供答案的最重要原因......
Lambdas:简史
PL中名为“lambda”的结构,无论是“lambda表达式”还是其他结构,都是语法的。换句话说,这些语言的用户可以找到这样的源语言结构,这些结构用于构建其他语言。粗略地说,“其他”在实践中只是“匿名函数”。
这种结构源自 lambda 抽象,这是 A. Church 开发的(非类型化)lambda 演算的三个语法类别(“表达式种类”)之一。
Lambda 演算是一种演绎系统(更准确地说,是 TRS(术语重写系统)),用于对计算进行普遍建模。减少 lambda 项就像计算普通 PL 中的表达式一样。使用内置的约简规则,定义各种计算方法就足够了。(如你所知,它是图灵完备的。因此,它可以用作 PL。
注意一般而言,计算 PL 中的表达式与减少 TRS 中的项不可互换。然而,lambda 演算是一种语言,其所有约简结果都可以在源语言中表达(即作为 lambda 术语),因此它们巧合地具有相同的含义。在实践中,几乎所有的 PL 都没有这种属性;描述其语义的演算可能包含不是源语言表达式的术语,并且简化可能比评估具有更详细的影响。
lambda 演算中的每个项(“表达式”)(lambda 项)要么是变量,要么是抽象的,要么是应用程序。这里的“变量”是符号的语法(只是变量的名称),它可以指前面介绍的现有“变量”(语义上,一个可以简化为其他 lambda 术语的实体)。引入变量的能力由抽象语法提供,该语法具有前导字母 λ,后跟绑定变量、点和 lambda 项。在许多语言中,绑定变量在语法和语义上都类似于形式参数名称,lambda 抽象中后面的 lambda 术语就像函数体一样。应用程序语法将 lambda 术语(“实际参数”)与某种抽象相结合,例如许多 PL 中的函数调用表达式。
注意lambda 抽象只能引入一个参数。要克服微积分内部的局限性,请参阅咖喱。
引入变量的能力使 lambda 演算成为一种典型的高级语言(尽管很简单)。另一方面,通过从 lambda 演算中删除变量和抽象特征,可以将组合逻辑视为 PL。从这个意义上说,组合逻辑是低级的:它们就像普通的旧汇编语言,不允许引入用户命名的变量(尽管有宏,这需要额外的预处理)。(...如果不是更低级......通常,汇编语言至少可以引入用户命名的标签。
注意到 lambda 抽象可以在任何其他 lambda 术语中就地构建,而无需指定名称来表示抽象。因此,整个 lambda 抽象形成了匿名函数(可能是嵌套的)。这是一个相当高级的功能(与.ISO C相比,它不允许匿名或嵌套函数)。
非类型化 lambda 演算的后继者包括各种类型化 lambda 演算(如 lambda 立方体)。这些更像是静态类型语言,它需要对函数的形式参数进行类型注释。尽管如此,lambda 抽象在这里仍然具有相同的作用。
尽管 lambda 演算不打算直接用作计算机中实现的 PL,但它们在实践中确实影响了 PL。值得注意的是,J. McCarthy 在 LISP 中引入了运算符,以提供完全遵循 Church 的非类型 lambda 演算思想的函数。显然,这个名字来自字母λ。 LISP(后来)具有不同的语法(S-expression),但是表达式中的所有可编程元素都可以通过简单的语法转换直接映射到非类型化lambda演算中的lambda抽象。LAMBDA
LAMBDA
LAMBDA
另一方面,许多其他 PL 通过其他方式表达类似的功能。引入可重用计算的一种略有不同的方法是命名函数(或者更准确地说,命名子程序),早期的 PL(如 FORTRAN)和源自 ALGOL 的语言都支持这些函数。它们由同时指定命名实体作为函数的语法引入。从某种意义上说,与LISP方言相比,这更简单(特别是在实现方面),而且几十年来它似乎比LISP方言更受欢迎。命名函数还可能允许匿名函数未共享的扩展,例如函数重载。
然而,越来越多的工业程序员终于发现了一等函数的用处,并且对就地引入函数定义的能力(在任意上下文中的表达式中,例如,作为其他函数的参数)的要求正在增加。避免命名一个不需要的事物是自然和合法的,根据定义,任何命名函数在这里都是失败的。(你可能知道,正确命名是计算机科学中众所周知的难题之一。为了解决这个问题,匿名函数被引入传统上只提供命名函数(或类似函数的结构,如“方法”等)的语言中,如C++和Java。他们中的许多人将该特征命名为“lambda 表达式”或类似的 lambda 事物,因为它们基本上反映了 lambda 演算中基本相同的思想。复兴。
有点消歧义:在 lambda 演算中,所有术语(变量、抽象和应用程序)都是 PL 中的有效表达式;从这个意义上说,它们都是“lambda 表达式”。但是,添加 lambda 抽象以丰富其功能的 PL 可能会将抽象的语法命名为“lambda 表达式”,以区别于现有的其他类型的表达式。
关闭:历史
在后一种情况下,该术语是由 P. J. Landin 于 1964 年创造的,用于在实现“以 Church 的 λ 符号建模”的 PL 时提供一流函数的支持。
具体到Landin(SECD机器)提出的模型,闭包包括λ表达式和相对于它被评估的环境,或者更准确地说:
环境部分,它是一个列表,其两个项目是 (1) 环境 (2) 标识符列表的标识符
以及一个控制部分,该部分由一个列表组成,其唯一项目是 AE
注:AE是论文中应用表达的缩写。这是在 lambda 演算中或多或少公开应用程序相同功能的语法。还有一些额外的细节,比如“应用”,在 lambda 演算中并不那么有趣(因为它是纯粹的函数式的)。对于这些微小的差异,SECD 与原始 lambda 演算不一致。例如,SECD 在任意单个 lambda 抽象上停止子项(“body”)是否具有正常形式,因为它不会在未应用抽象(“called”)的情况下减少子项(“evaluate the body”)。然而,这种行为可能更像是今天的 PL,而不是 lambda 演算。SECD 也不是唯一可以评估 lambda 项的抽象机器;尽管大多数用于类似目的的其他抽象机器也可能具有环境。与lambda演算(纯)相比,这些抽象机器可以在一定程度上支持突变。
因此,在这种特定上下文中,闭包是一种内部数据结构,用于实现对具有 AE 的 PL 的特定评估。
访问闭包中变量的纪律反映了词法范围,该范围在 1960 年代初期由命令式语言 ALGOL 60 首次使用。ALGOL 60 支持嵌套过程和将过程传递给参数,但不支持将过程作为结果返回。对于完全支持可由函数返回的一类函数的语言,ALGOL 60 样式实现中的静态链不起作用,因为被返回的函数使用的自由变量可能不再存在于调用堆栈中。这是向上的funarg问题。闭包通过捕获环境部分中的空闲变量并避免在堆栈上分配它们来解决问题。
另一方面,早期的LISP实现都使用动态作用域。这使得引用的变量绑定都可以在全局存储中访问,并且名称隐藏(如果有)是以每个变量为基础实现的:一旦使用现有名称创建变量,旧变量将由后进先出结构支持;换句话说,每个变量的名称都可以访问相应的全局堆栈。这有效地消除了对每个函数环境的需求,因为函数中从未捕获过任何自由变量(它们已经被堆栈“捕获”了)。
尽管一开始模仿了 lambda 符号,但 LISP 与这里的 lambda 演算非常不同。lambda 演算是静态作用域的。也就是说,每个变量都表示由最接近的 lambda 抽象的相同命名形式参数限定的实例,该参数在变量约简之前包含该变量。在 lambda 演算的语义中,简化应用程序将术语(“参数”)替换为抽象中的绑定变量(“形式参数”)。由于所有值都可以在 lambda 演算中表示为 lambda 项,因此可以通过在减少的每个步骤中替换特定子项来直接重写来完成。
注意因此,环境对于减少 lambda 项并不是必不可少的。然而,扩展 lambda 演算的演算可以在语法中显式引入环境,即使它只对纯计算(没有突变)进行建模。通过显式添加环境,可以对环境有专门的约束规则来强制环境归一化,从而加强微积分的方程理论。(参见 [Shu10] §9.1。
LISP则完全不同,因为它的底层语义规则既不基于lambda演算,也不基于术语重写。因此,LISP 需要一些不同的机制来维护范围规则。它采用了基于环境数据结构的机制,将变量保存到值的映射(即变量绑定)。在LISP的新变体中,环境中可能存在更复杂的结构(例如,词法范围的Lisp允许突变),但最简单的结构在概念上等同于Landin论文定义的环境,下面将讨论。
LISP实现在很早的时候确实支持一流的函数,但是使用纯粹的动态范围,没有真正的funargs问题:它们可以避免堆栈上的分配,并让全局所有者(GC,垃圾收集器)管理引用变量的环境(和激活记录)中的资源。那时不需要关闭。这是闭包发明之前的早期实现。
1962 年左右,LISP 1.5 中通过该设备引入了近似于静态(词法)绑定的深度绑定。这最终使该问题以“funarg问题”的名义广为人知。FUNARG
注意:AIM-199指出,这本质上是关于环境的。
Scheme 是第一个默认支持词法范围的 Lisp 方言(动态范围可以在现代版本的 Scheme 中由 / forms 模拟)。在后来的十年中,有一些争论,但最终大多数Lisp方言都采用了默认词法范围的想法,就像许多其他语言一样。从那时起,闭包作为一种实现技术,在不同风格的 PL 中传播得更广泛,也更受欢迎。make-parameter
parameterize
闭合:演变
Landin的原始论文首先将环境定义为将名称(“常量”)映射到命名对象(“基元”)的数学函数。然后,它将环境指定为“由名称/值对组成的列表结构”。后者在早期的 Lisp 实现中也以 alists(关联列表)的形式实现,但现代语言实现不一定遵循这些细节。特别是,可以链接环境以支持嵌套闭包,而像 SECD 这样的抽象机器不太可能直接支持嵌套闭包。
除了环境之外,Landin论文中“环境部分”的另一个组件用于保存lambda抽象的绑定变量(函数的形式参数)的名称。对于现代实现来说,这也是可选的(并且可能缺少),在现代实现中,当不需要反映源信息时,参数的名称可以静态优化(由 lambda 演算的 alpha 重命名规则在精神上授予)。
同样,现代实现可能不会将语法结构(AE 或 lambda 术语)直接保存为控制部分。相反,他们可能会使用一些内部 IR(中间表示)或“编译”形式(例如,某些 Lisp 方言实现使用的 FASL)。这种 IR 甚至不能保证是从表单生成的(例如,它可以来自某些命名函数的主体)。lambda
此外,环境部分可以保存其他信息,而不是用于 lambda 演算的评估。例如,它可以保留一个额外的标识符,以在调用站点上为环境提供额外的绑定命名。这可以实现基于 lambda 演算扩展的语言。
重新审视 PL 特定术语
此外,一些语言可以在其规范中定义与“闭包”相关的术语,以命名可以通过闭包实现的实体。这是不幸的,因为它导致了许多误解,例如“闭包是一种函数”。但幸运的是,大多数语言似乎都避免将其直接命名为语言中的句法结构。
尽管如此,这仍然比由语言规范任意重载的更成熟的通用概念要好。仅举几例:
“对象”被重定向到“类的实例”(在Java/CLR/“OOP”语言中),而不是传统的“类型化存储”(在C和C++中)或只是“值”(在许多Lisp中);
“变量”被重定向到传统称为“对象”(在 Golang 中)以及可变状态(在许多新语言中)的东西,因此它不再与数学和纯函数式语言兼容;
“多态性”仅限于包含多态性(在C++/“OOP”语言中),即使这些语言也有其他类型的多态性(参数多态性和临时多态性)。
关于资源管理
尽管在现代实现中省略了组件,但 Landin 论文中的定义相当灵活。它不限制如何在 SECD 机器的上下文中存储组件(如环境)。
在实践中,使用了各种策略。最常见和最传统的方法是让所有资源归一个全局所有者所有,该所有者可以收集不再使用的资源,即(全局)GC,首先在LISP中使用。
其他方式可能不需要全局所有者,并且对闭包具有更好的位置,例如:
在 C++ 中,通过指定如何捕获 lambda 表达式的捕获列表中的每个变量(通过值复制、引用甚至显式初始值设定项)以及每个变量的确切类型(智能指针或其他类型),允许用户显式管理闭包中捕获的实体资源。这可能不安全,但如果使用得当,它会获得更大的灵活性。
在 Rust 中,资源是通过不同的捕获模式(通过不可变的 borrow、通过 borrow、通过移动)依次尝试的(通过实现)来捕获的,用户可以指定 explicit .这比 C++ 更保守,但在某种意义上更安全(因为与 C++ 中未经检查的引用捕获相比,借用是静态检查的)。
move
上面所有的策略都可以支持闭包(C++ 和 Rust 确实有“闭包类型”概念的语言特定定义)。管理关闭所使用资源的纪律与关闭的资格无关。
因此,(虽然这里没有看到)托马斯·洛德(Thomas Lord)在LtU的闭包图跟踪的必要性在技术上也是不正确的。闭包可以解决 funarg 问题,因为它允许防止对激活记录(堆栈)的无效访问,但事实并不能神奇地断言对包含闭包的资源的每个操作都是有效的。这种机制取决于外部执行环境。需要明确的是,即使在传统的实现中,隐式所有者(GC)也不是闭包中的一个组件,所有者的存在是SECD机器的实现细节(所以它是用户的“高阶”细节之一)。此类细节是否支持图形跟踪对闭包的限定没有影响。此外,AFAIK,语言结构 let
与 rec
的结合是在 1966 年的 ISWIM 中首次引入的(再次由 P. Landin),它不可能产生强制执行比它更早发明的闭包的原始含义的效果。
关系
因此,总而言之,闭包可以(非正式地)定义为:
(1)PL实现特定的数据结构,包括作为环境部分和类功能实体的控制部分,其中:
(1.1)控制部分派生自一些源语言结构,指定类函数实体的评估结构;
(1.2) 环境部分由环境和其他实现定义的数据组成;
(1.3)中的环境(1.2)是由类函数实体的潜在上下文依赖源语言结构决定的,用于保存捕获的自由变量发生在创建类函数实体的源语言结构的评估结构中。
(2)或者,利用(1)中名为“闭包”的实体的实现技术的总称。
Lambda 表达式(抽象)只是源语言中引入(创建)未命名的类似函数实体的语法结构之一。PL 可以将其作为引入类函数实体的唯一方式。
通常,源程序中的 lambda 表达式与程序执行中闭包的存在之间没有明确的对应关系。由于实现细节对程序的可观察行为没有影响,因此 PL 实现通常被允许在可能的情况下合并分配给闭包的资源,或者在对程序语义无关紧要时完全省略创建它们:
该实现可以检查 lambda 表达式中要捕获的自由变量集,当集合为空时,可以避免引入环境部分,因此类函数实体不需要维护闭包。这种策略通常是在静态语言的规则中强制执行的。
否则,实现可能会也可能不会总是通过计算 lambda 表达式(是否有要捕获的变量)为类似函数的实体创建闭包。
Lambda 表达式的计算结果可以被计算为类似函数的实体。某些 PL 的用户可能会将这种类似函数的实体称为“闭包”。在这种情况下,“匿名函数”应该是这种“闭包”的更中性的名称。
附录:功能:乱七八糟的历史
这与问题没有直接关系,但可能还值得注意的是,“函数”可以在不同的上下文中命名不同的实体。
这在数学上已经是一团糟了。
目前,我懒得在 PL 的上下文中总结它们,但需要注意的是:密切关注上下文,以确保不同 PL 中“函数”的各种定义不会使您的推理偏离主题。
至于一般使用“匿名函数”(在实践中由 PL 共享),我相信它不会在这个主题上引起重大的混淆和误解。
命名函数可能有更多的问题。函数可以表示名称本身的实体(“符号”),以及这些名称的计算值。鉴于大多数 PL 没有未计算的上下文来区分函数与其他一些具有有趣含义的实体(例如 在 C++ 中只是格式错误),用户可能无法观察到未计算的操作数和计算值之间的误解差异。对于一些 Lisp 方言来说,这将是有问题的。即使是经验丰富的 PL 专家也很容易错过一些重要的东西;这也是为什么我强调要区分句法结构与其他实体的原因。sizeof(a_plain_cxx_function)
QUOTE
闭包意味着一个函数返回另一个函数。不是结果,而是像委托一样的可调用函数。 Lambda 是一个匿名函数描述。如果 lambda 返回函数,它也可以是闭包。
评论