ske*_*ker 774 lambda closures functional-programming function
有人能解释一下吗 我理解它们背后的基本概念,但我经常看到它们互换使用,我感到困惑.
现在我们在这里,它们与常规功能有什么不同?
Cla*_*diu 676
一个拉姆达只是一个匿名函数-没有名字定义的函数.在某些语言中,例如Scheme,它们等同于命名函数.实际上,函数定义被重写为在内部将lambda绑定到变量.在其他语言中,如Python,它们之间存在一些(相当不必要的)区别,但它们的行为方式相同.
甲闭合是任何功能关闭以上的环境中,它被定义.这意味着它可以访问不在其参数列表中的变量.例子:
def func(): return h
def anotherfunc(h):
return func()
Run Code Online (Sandbox Code Playgroud)
这将导致错误,因为func
未关闭环境anotherfunc
- h
未定义.func
只关闭全球环境.这将有效:
def anotherfunc(h):
def func(): return h
return func()
Run Code Online (Sandbox Code Playgroud)
因为在这里func
定义anotherfunc
,并且在python 2.3和更高版本(或者像这样的一些数字)中,当它们几乎使闭包正确时(变异仍然不起作用),这意味着它关闭了 anotherfunc
环境并且可以访问内部的变量.它.在Python 3.1+,突变使用时也工作的nonlocal
关键词.
另一个重要的观点 - 即使不再被评估,func
也将继续关闭anotherfunc
环境anotherfunc
.此代码也适用:
def anotherfunc(h):
def func(): return h
return func
print anotherfunc(10)()
Run Code Online (Sandbox Code Playgroud)
这将打印10.
正如您所注意到的,这与lambda无关- 它们是两个不同的(虽然相关)概念.
Sas*_*asQ 262
关于lambdas和闭包有很多混淆,即使在这个StackOverflow问题的答案中也是如此.而不是要求随机程序员通过某些编程语言或其他无知程序员学习关于实践的闭包,而是前往源头(这一切都开始了).由于lambdas和闭包来自Alonzo Church发明的Lambda微积分,早在30年代,在第一台电子计算机出现之前,这就是我所说的源头.
Lambda Calculus是世界上最简单的编程语言.您可以做的唯一事情:►
f x
.f
是函数和x
是其唯一的参数)?
(lambda)之前,然后是符号名称(例如x
),然后是.
表达式之前的点来完成的.然后,将表达式转换为期望一个参数的函数.例如:获取表达式并告诉该表达式中的符号是绑定变量 - 它可以替换为您提供的值作为参数.
请注意,以这种方式定义的函数是匿名的 - 它没有名称,所以你还不能引用它,但是你可以通过提供它正在等待的参数来立即调用它(记住应用程序吗?),就像这个:.然后表达式(在这种情况下是一个文字值)被替换为应用的lambda 的子表达式,所以你得到,然后通过常见的算术规则减少.?x.x+2
x+2
x
(?x.x+2) 7
7
x
x+2
7+2
9
所以我们已经解决了一个谜团:
lambda是上面例子中的匿名函数,?x.x+2
.
function(x) { return x+2; }
Run Code Online (Sandbox Code Playgroud)
你可以立即将它应用于这样的参数:
(function(x) { return x+2; })(7)
Run Code Online (Sandbox Code Playgroud)
或者您可以将此匿名函数(lambda)存储到某个变量中:
var f = function(x) { return x+2; }
Run Code Online (Sandbox Code Playgroud)
它有效地赋予它一个名称f
,允许你引用它并在以后多次调用它,例如:
alert( f(7) + f(10) ); // should print 21 in the message box
Run Code Online (Sandbox Code Playgroud)
但你不必为此命名.你可以马上叫它:
alert( function(x) { return x+2; } (7) ); // should print 9 in the message box
Run Code Online (Sandbox Code Playgroud)
在LISP中,lambdas是这样的:
(lambda (x) (+ x 2))
Run Code Online (Sandbox Code Playgroud)
你可以通过立即将它应用于参数来调用这样的lambda:
( (lambda (x) (+ x 2)) 7 )
Run Code Online (Sandbox Code Playgroud)
正如我所说,lambda抽象所做的是在其子表达式中绑定一个符号,以便它成为一个可替代的参数.这样的符号称为绑定.但是如果表达式中还有其他符号呢?例如:?x.x/y+2
.在此表达式中,符号x
由其?x.
前面的lambda抽象绑定.但另一个符号,y
没有约束 - 它是免费的.我们不知道它是什么以及它来自何处,因此我们不知道它意味着什么以及它代表什么价值,因此在我们弄清楚它的y
含义之前我们无法评估该表达式.
事实上,其他两个符号也一样,2
并且+
.只是我们对这两个符号非常熟悉,我们通常会忘记计算机不知道它们,我们需要通过在某个地方定义它们来告诉它它们的含义,例如在库或语言本身.
您可以将在表达之外的其他位置定义的自由符号视为其"周围环境",称为环境.环境可能是一个更大的表达,这个表达是其中的一部分(正如Qui-Gon Jinn所说:"总是有更大的鱼";)),或者在某些库中,或者在语言本身(作为原始).
这让我们将lambda表达式分为两类:
您可以通过提供环境来关闭一个开放的 lambda表达式,该环境通过将它们绑定到某些值(可能是数字,字符串,匿名函数,也就是lambdas,等等......)来定义所有这些自由符号.
和这里来的闭合部分:
该封闭件上lambda表达式是这个特定的一组给值到在外部环境(环境)中所定义的符号的分类符号在该表达式中,使得它们非自由了.它将一个开放的 lambda表达式(仍然包含一些"未定义的"自由符号)转换为一个封闭的符号,它不再具有任何自由符号.
例如,如果您有以下lambda表达式:?x.x/y+2
,符号x
的束缚,而符号y
是免费的,因此表达的是open
,不能进行评估,除非你说的什么y
意思(与同+
和2
,这也是免费的).但是假设你也有这样的环境:
{ y: 3,
+: [built-in addition],
2: [built-in number],
q: 42,
w: 5 }
Run Code Online (Sandbox Code Playgroud)
这个环境从我们的lambda表达式提供的所有"未定义"(免费)符号定义(y
,+
,2
),和一些额外的符号(q
,w
).我们需要定义的符号是这个环境的子集:
{ y: 3,
+: [built-in addition],
2: [built-in number] }
Run Code Online (Sandbox Code Playgroud)
这正是我们lambda表达式的闭包:>
换句话说,它会关闭一个开放的lambda表达式.这就是名字封闭起源的地方,这就是为什么很多人在这个帖子中的答案不太正确的原因:P
好吧,Sun/Oracle,微软,谷歌等公司的市场主体应该受到指责,因为这就是他们所谓的语言结构(Java,C#,Go等).他们经常把"封闭"称为"lambdas".或者他们将"闭包"称为他们用于实现词法作用域的特定技术,即,函数可以访问在定义时在其外部作用域中定义的变量.他们经常说这个函数"包含"这些变量,即将它们捕获到一些数据结构中,以防止它们在外部函数完成执行后被销毁.但这只是后事实上的 "民俗词源学"和市场营销,只会让事情变得更加混乱,因为每个语言供应商都使用自己的术语.
更糟糕的是,因为他们所说的内容总是有一些真相,这不允许你轻易将其视为假:P让我解释一下:
如果要实现使用lambdas作为一等公民的语言,则需要允许它们使用在其周围上下文中定义的符号(即,在lambda中使用自由变量).即使周围的函数返回,这些符号也必须存在.问题是这些符号绑定到函数的某些本地存储(通常在调用堆栈上),当函数返回时,它将不再存在.因此,为了使lambda以您期望的方式工作,您需要以某种方式从其外部上下文中"捕获"所有这些自由变量,并将其保存以供日后使用,即使外部上下文将消失.也就是说,你需要找到你的lambda 的闭包(它使用的所有这些外部变量)并将它存储在其他地方(通过制作副本,或者为它们预先准备空间,除了堆栈之外的其他地方).用于实现此目标的实际方法是您的语言的"实现细节".这里重要的是闭包,它是来自lambda 环境的一组自由变量,需要在某处保存.
人们花了很长时间才开始调用他们在语言实现中使用的实际数据结构来实现闭包作为"闭包"本身.结构通常看起来像这样:
Closure {
[pointer to the lambda function's machine code],
[pointer to the lambda function's environment]
}
Run Code Online (Sandbox Code Playgroud)
并且这些数据结构作为参数传递给其他函数,从函数返回并存储在变量中,以表示lambda,并允许它们访问其封闭环境以及在该上下文中运行的机器代码.但它只是一种方式(许多之一)来实现闭合,不能在封闭自己.
正如我在上面解释的那样,lambda表达式的闭包是其环境中定义的子集,它为lambda表达式中包含的自由变量赋值,有效地关闭表达式(将一个无法计算的开放 lambda表达式转换为一个闭合的 lambda表达式,然后可以对其进行求值,因为现在定义了包含在其中的所有符号).
其他任何东西只是程序员和语言供应商的"货物崇拜"和"voo-doo魔术",并不知道这些概念的真正根源.
我希望能回答你的问题.但如果您有任何后续问题,请随时在评论中询问他们,我会尝试更好地解释它.
Mar*_*ade 174
当大多数人想到函数时,他们会想到命名函数:
function foo() { return "This string is returned from the 'foo' function"; }
Run Code Online (Sandbox Code Playgroud)
这些是按名称调用的,当然:
foo(); //returns the string above
Run Code Online (Sandbox Code Playgroud)
使用lambda表达式,您可以拥有匿名函数:
@foo = lambda() {return "This is returned from a function without a name";}
Run Code Online (Sandbox Code Playgroud)
通过上面的示例,您可以通过分配给它的变量调用lambda:
foo();
Run Code Online (Sandbox Code Playgroud)
但是,将匿名函数分配给变量比将它们传递给高阶函数或从高阶函数传递更有用,即接受/返回其他函数的函数.在很多这些情况下,命名函数是不必要的:
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)});
Run Code Online (Sandbox Code Playgroud)
甲闭合可以命名或匿名的功能,但是是公知的,当它"关闭在...之上"的范围的变量,其中所述函数被定义,即,闭合将仍然参考环境与在所使用的任何外变量封闭本身.这是一个命名的闭包:
@x = 0;
function incrementX() { x = x + 1;}
incrementX(); // x now equals 1
Run Code Online (Sandbox Code Playgroud)
这似乎不是很多,但如果这是所有在另一个函数,你传递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)
Run Code Online (Sandbox Code Playgroud)
这就是在函数式编程中获取有状态对象的方法.由于不需要命名"incrementX",因此在这种情况下可以使用lambda:
function foo()
{ @x = 0;
return lambda()
{ x = x + 1;
return x;
};
}
Run Code Online (Sandbox Code Playgroud)
Mic*_*own 54
并非所有的闭包都是lambda,并非所有的lambd都是闭包.两者都是功能,但不一定是我们习惯了解的方式.
lambda本质上是一个内联定义的函数,而不是声明函数的标准方法.Lambdas经常可以作为对象传递.
闭包是一种通过引用其主体外部的字段来包围其周围状态的函数.封闭状态保持在闭包的调用之间.
在面向对象的语言中,闭包通常通过对象提供.但是,某些OO语言(例如C#)实现的特殊功能更接近于纯函数语言(如lisp)提供的闭包定义,这些闭包没有包含状态的对象.
有趣的是,在C#中引入Lambdas和Closures使函数式编程更接近主流使用.
Wei*_*Qiu 12
从编程语言的角度来看,它们完全是两回事.
基本上对于图灵完整语言我们只需要非常有限的元素,例如抽象,应用和缩减.抽象和应用程序提供了构建lamdba表达式的方法,并且减少了lambda表达式的含义.
Lambda提供了一种可以抽象出计算过程的方法.例如,为了计算两个数的和,可以抽象出取两个参数x,y和返回x + y的过程.在方案中,您可以将其编写为
(lambda (x y) (+ x y))
Run Code Online (Sandbox Code Playgroud)
您可以重命名参数,但它完成的任务不会更改.在几乎所有编程语言中,您都可以为lambda表达式指定名称,这些名称是命名函数.但是没有太大的区别,它们在概念上可以被认为只是语法糖.
好的,现在想象一下如何实现这一点.每当我们将lambda表达式应用于某些表达式时,例如
((lambda (x y) (+ x y)) 2 3)
Run Code Online (Sandbox Code Playgroud)
我们可以简单地用要评估的表达式替换参数.这个模型已经非常强大了.但是这个模型不能让我们改变符号的值,例如我们无法模仿状态的变化.因此,我们需要一个更复杂的模型.为了简化,每当我们想要计算lambda表达式的含义时,我们将符号对和相应的值放入环境(或表)中.然后通过查找表中的相应符号来评估其余(+ xy).现在,如果我们提供一些原语来直接对环境进行操作,我们可以对状态的变化进行建模!
在此背景下,检查此功能:
(lambda (x y) (+ x y z))
Run Code Online (Sandbox Code Playgroud)
我们知道,当我们评估lambda表达式时,xy将被绑定在一个新表中.但是我们如何以及在哪里可以看到z?实际上,z被称为自由变量.必须有一个包含z的外部环境.否则,只能通过绑定x和y来确定表达式的含义.为清楚起见,您可以在方案中编写如下内容:
((lambda (z) (lambda (x y) (+ x y z))) 1)
Run Code Online (Sandbox Code Playgroud)
所以z将在外表中绑定为1.我们仍然得到一个接受两个参数的函数,但它的真正含义还取决于外部环境.换句话说,外部环境关闭了自由变量.在set!的帮助下,我们可以使函数有状态,即它不是数学意义上的函数.它返回的不仅取决于输入,还取决于z.
这是你已经非常了解的东西,对象的方法几乎总是依赖于对象的状态.这就是为什么有些人说"封闭是穷人的对象."但我们也可以将对象视为穷人的封闭,因为我们真的喜欢一流的功能.
我使用方案来说明由于该方案是最早的具有真正闭包的语言之一的想法.这里的所有材料都更好地呈现在SICP第3章中.
总而言之,lambda和闭包是非常不同的概念.lambda是一个函数.闭包是一对lambda和关闭lambda的相应环境.
概念与上面描述的相同,但如果您来自PHP背景,这将进一步解释使用PHP代码.
$input = array(1, 2, 3, 4, 5);
$output = array_filter($input, function ($v) { return $v > 2; });
Run Code Online (Sandbox Code Playgroud)
function($ v){return $ v> 2; 是lambda函数定义.我们甚至可以将它存储在变量中,因此它可以重复使用:
$max = function ($v) { return $v > 2; };
$input = array(1, 2, 3, 4, 5);
$output = array_filter($input, $max);
Run Code Online (Sandbox Code Playgroud)
现在,如果您想更改过滤数组中允许的最大数量,该怎么办?你必须编写另一个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));
Run Code Online (Sandbox Code Playgroud)
闭包是一个在自己的环境中计算的函数,它有一个或多个绑定变量,可以在调用函数时访问它们.它们来自功能编程世界,其中有许多概念在起作用.闭包类似于lambda函数,但在某种意义上它们更聪明,它们能够与定义闭包的外部环境中的变量进行交互.
这是一个更简单的PHP闭包示例:
$string = "Hello World!";
$closure = function() use ($string) { echo $string; };
$closure();
Run Code Online (Sandbox Code Playgroud)
这个问题很老,得到了很多答案.现在Java 8和Official Lambda是非正式的关闭项目,它重新提出了问题.
Java环境中的答案(通过Lambdas和闭包 - 有什么区别?):
"闭包是一个lambda表达式,它与一个将每个自由变量绑定到一个值的环境配对.在Java中,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";});
}
}
Run Code Online (Sandbox Code Playgroud)
斯威夫特[关闭]
class MyClass {
func foo(r:() -> Void) {}
func lambdaExample() {
foo(r: {})
}
var s = "hello"
func closureExample() {
foo(r: {s = "world"})
}
}
Run Code Online (Sandbox Code Playgroud)
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;
}
};
Run Code Online (Sandbox Code Playgroud)
其中 Function 类只是用 java 代码构建的。现在你可以调用mapPersonToJob.apply(person)
某个地方来使用它。这只是一个例子。那是在有语法之前的 lambda。Lambda 是一个捷径。
关闭:
当 Lambda 可以访问此范围之外的变量时,它就成为闭包。我想你可以说它的魔力,它神奇地可以环绕它创建的环境并使用其作用域之外的变量(外部作用域)。所以需要明确的是,闭包意味着 lambda 可以访问其外部作用域。
在 Kotlin 中,lambda 始终可以访问其闭包(位于其外部作用域中的变量)
在这个问题的各种现有答案中,有很多技术上模糊或“甚至没有错误”的人造珍珠的噪音,所以我最终会添加一个新的......
\n最好知道,术语“闭包”和“lambda”都可以表示不同的事物,具体取决于上下文。
\n这是一个正式问题,因为正在讨论的 PL(编程语言)规范可能会明确定义这些术语。
\n例如,通过 ISO C++ (C++11 起):
\n\n\nlambda 表达式的类型(也是闭包对象的类型)是唯一的、未命名的非联合类类型,称为闭包类型,其属性如下所述。
\n
由于类 C 语言的用户每天都会混淆指向“指针值”或“指针对象”(类型的居民)的“指针”(类型),因此这里也存在混淆的风险:大多数 C++ 用户实际上正在谈论“闭包对象”使用术语“闭包”。对含糊之处要小心。
\n注意:为了使事情总体上更清晰、更精确,我很少故意使用一些语言中立的术语(通常特定于PL 理论而不是语言定义的术语。例如,上面使用的类型 inhabitant涵盖了特定于语言的“更广泛意义上的“(r)值”和“左值”。(由于 C++ 值类别定义的语法本质无关,避免“(l/r)值”可能会减少混淆)。(免责声明:左值和右值在许多其他上下文中很常见。)不同 PL 之间未正式定义的术语可能会在引号中。引用材料的逐字副本也可能在引号中,拼写错误不变。
\n这与“lambda”更相关。(小写)字母 lambda (\xce\xbb) 是希腊字母表的一个元素。与“lambda”和“closure”相比,我们当然不是在谈论字母本身,而是在使用“lambda”派生概念的语法背后的某些东西。
\n现代 PL 中的相关构造通常被命名为“lambda 表达式”。它源自“lambda 抽象”,如下所述。
\n在详细讨论之前,我建议阅读问题本身的一些评论。我觉得它们比这里问题的大多数答案更安全、更有帮助,因为混淆的风险更小。(可悲的是,这是我决定在这里提供答案的最重要原因......)
\nPL 中名为“lambda”的构造,无论是“lambda 表达式”还是其他东西,都是语法上的。换句话说,这些语言的用户可以找到用于构建其他语言的源语言结构。粗略地说,“其他”实际上只是“匿名函数”。
\n此类构造源自lambda 抽象,它是 A. Church 开发的(无类型)lambda 演算的三个语法类别(“表达式种类”)之一。
\nLambda 演算是一个对通用计算进行建模的推导系统(更准确地说,TRS(术语重写系统))。减少 lambda 项就像计算普通 PL 中的表达式一样。使用内置的归约规则,定义各种计算方式就足够了。(您可能知道,它是图灵完备的。)因此,它可以用作 PL。
\n注:一般而言,评估 PL 中的表达式与减少 TRS 中的术语不可互换。然而,lambda演算是一种所有归约结果都可以在源语言中表达的语言(即作为lambda项),因此它们恰好具有相同的含义。实践中几乎所有 PL 都不具备此属性;描述其语义的演算可能包含不是源语言表达式的术语,并且缩减可能比评估具有更详细的效果。
\nlambda 演算(lambda 项)中的每个项(“表达式”)都是变量、抽象或应用。这里的“变量”是符号的语法(只是变量的名称),它可以引用前面介绍的现有“变量”(语义上,可以简化为其他某个 lambda 术语的实体)。引入变量的能力是由抽象语法提供的,它有一个前导字母 \xce\xbb,后跟一个绑定变量、一个点和一个 lambda 项。在许多语言中,绑定变量在语法和语义上都类似于形式参数名称,而 lambda 抽象中后面跟随的 lambda 术语就像函数体。应用程序语法将 lambda 项(“实际参数”)与某种抽象结合起来,例如许多 PL 中的函数调用表达式。
\n注意lambda 抽象只能引入一个参数。要克服微积分内部的限制,请参阅柯里化。
\n引入变量的能力使 lambda 演算成为一种典型的高级语言(尽管很简单)。另一方面,通过从 lambda 演算中删除变量和抽象特征,组合逻辑可以被视为 PL。正是在这个意义上,组合逻辑是低级的:它们就像普通的旧汇编语言,不允许引入用户命名的变量(尽管有宏,这需要额外的预处理)。(...如果不是更底层的话...通常汇编语言至少可以引入用户命名的标签。)
\n请注意,lambda 抽象可以就地构建在任何其他 lambda 术语内,无需指定名称来表示抽象。因此,lambda 抽象整体形成了匿名函数(可能是嵌套的)。这是一个相当高级的功能(与 ISO C 相比,ISO C 不允许匿名或嵌套函数)。
\n无类型 lambda 演算的后继者包括各种类型的 lambda 演算(如lambda 立方体))。这些更像是静态类型语言,需要对函数的形式参数进行类型注释。尽管如此,lambda 抽象在这里仍然具有相同的作用。
\n尽管 lambda 演算无意直接用作计算机中实现的 PL,但它们在实践中确实影响了 PL。值得注意的是,J. McCarthyLAMBDA
在 LISP 中引入了运算符来提供完全遵循 Church 无类型 lambda 演算思想的函数。显然,这个名字LAMBDA
来自字母\xce\xbb。LISP(稍后)具有不同的语法(S-表达式),但LAMBDA
表达式中的所有可编程元素都可以通过简单的语法转换直接映射到无类型 lambda 演算中的 lambda 抽象。
另一方面,许多其他 PL 通过其他方式表达类似的功能。引入可重用计算的一种稍微不同的方法是命名函数(或更准确地说,命名子例程),早期的 PL(如 FORTRAN)和从 ALGOL 派生的语言都支持这种方式。它们是通过指定命名实体同时作为函数的语法引入的。与 LISP 方言相比,这在某种意义上更简单(尤其是在实现方面),而且几十年来它似乎比 LISP 方言更流行。命名函数还可能允许匿名函数不共享的扩展,例如函数重载。
\n尽管如此,越来越多的工业程序员最终发现了一流函数的用处,并且对就地引入函数定义的能力(在任意上下文中的表达式中,例如,作为某些其他函数的参数)的需求正在增加。避免命名不需要的东西是自然且合法的,并且根据定义,任何命名函数在这里都会失败。(您可能知道,正确命名事物是计算机科学中众所周知的难题之一。)为了解决这个问题,传统上仅提供命名函数(或类似函数的结构,如“方法”、无论如何),如 C++ 和 Java。他们中的许多人将该功能命名为“lambda 表达式”或类似的 lambda 事物,因为它们基本上反映了 lambda 演算中本质上相同的思想。再生。
\n有点歧义:在 lambda 演算中,所有术语(变量、抽象和应用)都是 PL 中的有效表达式;从这个意义上来说,它们都是“lambda 表达式”。然而,添加 lambda 抽象以丰富其功能的 PL 可能会将该抽象的语法专门命名为“lambda 表达式”,以与现有的其他类型表达式相区别。
\n在后一种情况下,该术语由 PJ Landin 于 1964 年创造,旨在为“以 Church\xce\xbb-notation 建模”的 PL 评估实施提供一流函数的支持。
\n具体到 Landin( SECD 机器)提出的模型,闭包由 \xce\xbb 表达式和评估它的相对环境组成,或更准确地说:
\n\n\n环境部分,是一个列表,其中两项为 (1) 环境 (2) 标识符列表的标识符
\n
\n\n以及一个控制部分,由一个列表组成,该列表的唯一项目是 AE
\n
注: 本文中AE为applicative expression的缩写。这种语法或多或少地暴露了 lambda 演算中应用程序的相同功能。不过,还有一些额外的细节,例如“应用”,在 lambda 演算中并不那么有趣(因为它是纯函数式的)。由于这些细微的差异,SECD 与原始 lambda 演算不一致。例如,无论子项(“body”)是否具有范式,SECD 都会在任意单个 lambda 抽象上停止,因为在没有应用抽象(“被调用”)的情况下,它不会减少子项(“评估主体”)。然而,这种行为可能更像今天的 PL,而不是 lambda 演算。SECD 也不是唯一可以计算 lambda 项的抽象机;尽管大多数其他用于类似目的的抽象机器也可能有环境。与 lambda 演算(纯粹的)相比,这些抽象机器可以在某种程度上支持变异。
\n因此,在这个特定的上下文中,闭包是一种内部数据结构,用于实现 PL 与 AE 的特定评估。
\n访问闭包中变量的规则反映了词法作用域,该规则首先由命令式语言 ALGOL 60 在 20 世纪 60 年代初使用。ALGOL 60 支持嵌套过程并将过程传递给参数,但不将过程作为结果返回。对于完全支持可由函数返回的一等函数的语言,ALGOL 60 样式实现中的静态链不起作用,因为返回的函数使用的自由变量可能不再存在于调用堆栈上。这是向上 funarg 问题。闭包通过捕获环境部分中的自由变量并避免在堆栈上分配它们来解决该问题。
\n另一方面,早期的 LISP 实现都使用动态作用域。这使得引用的变量绑定在全局存储中都可访问,并且名称隐藏(如果有)是作为每个变量的基础实现的:一旦使用现有名称创建变量,旧变量将由 LIFO 结构支持;换句话说,每个变量的名称都可以访问相应的全局堆栈。这有效地消除了对每个函数环境的需求,因为函数中没有捕获任何自由变量(它们已经被堆栈“捕获”)。
\n尽管一开始模仿了 lambda 表示法,但 LISP 与这里的 lambda 演算有很大不同。lambda 演算的作用域是静态的。也就是说,每个变量表示由 lambda 抽象的最近的相同命名形式参数界定的实例,该抽象在约简之前包含该变量。在 lambda 演算的语义中,减少应用程序将术语(“参数”)替换为抽象中的绑定变量(“形式参数”)。由于所有值都可以表示为 lambda 演算中的 lambda 项,因此可以通过在约简的每个步骤中替换特定子项来直接重写来完成。
\n注意因此,环境对于减少 lambda 项来说并不是必需的。然而,扩展 lambda 演算的演算可以在语法中显式地引入环境,即使它仅模拟纯计算(无突变)。通过显式添加环境,可以对环境有专用的约束规则来强制环境标准化,从而加强微积分的方程理论。(参见[Shu10] \xc2\xa79.1。)
\nLISP 则截然不同,因为它的底层语义规则既不基于 lambda 演算,也不基于术语重写。因此,LISP 需要一些不同的机制来维护范围界定规则。它采用基于环境数据结构的机制来保存变量到值的映射(即变量绑定)。LISP 新变体的环境中可能有更复杂的结构(例如,词法范围的 Lisp 允许突变),但最简单的结构在概念上等同于 Landin 论文定义的环境,如下所述。
\nLISP 实现在很早的时候确实支持一流的函数,但是使用纯动态作用域,不存在真正的 funargs 问题:它们可以避免堆栈上的分配并让全局所有者(GC,垃圾收集器)来管理环境中的资源(和激活记录)引用变量。那么就不需要闭包了。这是闭包发明之前的早期实现。
\n大约在 1962 年,通过该设备在 LISP 1.5 中引入了近似静态(词汇)绑定的深度绑定FUNARG
。这最终使得这个问题以“funarg问题”的名字而广为人知。
注意 AIM-199指出这本质上与环境有关。
\nScheme 是第一个默认支持词法作用域的Lisp 方言(动态作用域可以通过现代版本的 Scheme 中的make-parameter
/parameterize
形式来模拟)。在后来的十年里出现了一些争论,但最终大多数 Lisp 方言都采用了默认词法作用域的想法,就像许多其他语言一样。自此,闭包作为一种实现技术,在不同风格的 PL 中得到了更广泛的传播和流行。
Landin 的原始论文首先将环境定义为将名称(“常量”)映射到命名对象(“原始”)的数学函数。然后,它将环境指定为“由名称/值对组成的列表结构”。后者在早期的 Lisp 实现中也被实现为alist(关联列表),但现代语言实现不一定遵循这样的细节。特别是,环境可以链接起来以支持嵌套闭包,这不太可能被像 SECD 这样的抽象机直接支持。
\n除了环境之外,Landin 论文中“环境部分”的另一个组件用于保存 lambda 抽象的绑定变量的名称(函数的形式参数)。对于现代实现来说,这也是可选的(并且可能缺失),当不需要反映源信息时,参数的名称可以静态优化(精神上由 lambda 演算的 alpha 重命名规则授予)。
\n类似地,现代实现可能不会直接将句法结构(AE 或 lambda 项)保存为控制部分。相反,它们可能使用一些内部 IR(中间表示)或“编译”形式(例如 Lisp 方言的某些实现所使用的 FASL)。这样的 IR 甚至不能保证是从lambda
(例如,它可以来自某些命名函数的主体)。
此外,环境部分可以保存不用于lambda演算评估的其他信息。例如,它可以保留额外的标识符以提供额外的绑定,命名调用站点的环境。这可以实现基于 lambda 演算扩展的语言。
\n此外,一些语言可以在其规范中定义与“闭包”相关的术语,以命名可以由闭包实现的实体。这是不幸的,因为它会导致许多误解,例如“闭包是一个函数”。但幸运的是,大多数语言似乎都避免将其直接命名为语言中的语法结构。
\n尽管如此,这仍然比通过语言规范任意重载更完善的通用概念要好。仅举几例:
\n“对象”被重定向到“类的实例”(在Java /CLR/“OOP”语言中),而不是传统的“类型存储”(在 C 和C++中)或只是“值”(在许多 Lisp 中);
\n“变量”被重定向到传统的“对象”(在Golang中)以及可变状态(在许多新语言中),因此它不再与数学和纯函数语言兼容;
\n“多态性”仅限于包含多态性(在 C++/“OOP”语言中),即使这些语言确实具有其他类型的多态性(参数多态性和临时多态性)。
\n尽管现代实现中省略了这些组件,但 Landin 论文中的定义相当灵活。它不限制如何存储 SECD 机器上下文之外的组件(例如环境)。
\n在实践中,使用了各种策略。最常见和传统的方式是让所有资源归一个全局所有者所有,该全局所有者可以收集不再使用的资源,即(全局)GC,首先在LISP中使用。
\n其他方式可能不需要全局所有者并且对闭包具有更好的局部性,例如:
\n在 C++ 中,通过指定如何捕获 lambda 表达式的捕获列表中的每个变量(通过值复制、通过引用,甚至通过显式初始化程序),允许用户显式管理闭包中捕获的实体的资源,并且每个变量的确切类型(智能指针或其他类型)。这可能不安全,但如果使用正确,它会获得更大的灵活性。
\n在 Rust 中,资源通过依次尝试(通过实现)的不同捕获模式(通过不可变借用、借用、移动)来捕获,并且用户可以指定显式move
. 这比 C++ 更保守,但在某种意义上更安全(因为与 C++ 中未经检查的按引用捕获相比,借用是静态检查的)。
上述所有策略都可以支持闭包(C++ 和 Rust 确实具有“闭包类型”概念的特定于语言的定义)。管理关闭所使用的资源的规则与关闭的资格无关。
\n因此,(虽然这里没有看到)LtU 的 Thomas Lord 提出的闭包图追踪必要性的主张在技术上也是不正确的。闭包可以解决 funarg 问题,因为它允许防止对激活记录(堆栈)的无效访问,但事实上并不能神奇地断言对构成闭包的资源的每个操作都是有效的。这种机制依赖于外部执行环境。应该清楚的是,即使在传统实现中,隐式所有者(GC)也不是闭包中的组件,所有者的存在是 SECD 机器的实现细节(因此它是“高阶”细节之一)给用户)。这些细节是否支持图跟踪对于闭包的资格没有影响。此外,据我所知,结合的语言结构于 1966 年首次在 ISWIM 中引入(再次由 P. Landin 引入)let
rec
,它无法强制执行早于它本身发明的闭包的原始含义。
因此,总而言之,闭包可以(非正式地)定义为:
\n(1) PL 实现特定的数据结构,包括类似功能实体的环境部分和控制部分,其中:
\n(1.1) 控制部分源自某些指定类函数实体的求值结构的源语言结构;
\n(1.2) 环境部分由环境和可选的其他实现定义的数据组成;
\n(1.3) (1.2) 中的环境由类函数实体的潜在上下文相关源语言结构决定,用于保存捕获的自由变量,出现在创建类函数实体的源语言结构的求值结构中。
\n(2) 或者,是利用(1) 中名为“闭包”的实体的实现技术的总称。
\nLambda 表达式(抽象)只是源语言中引入(创建)未命名的类函数实体的语法结构之一。PL 可以将其作为引入类功能实体的唯一方式来提供。
\n一般来说,源程序中的 lambda 表达式与程序执行中是否存在闭包之间没有明确的对应关系。由于实现细节对程序的可观察行为没有影响,因此通常允许 PL 实现在可能的情况下合并为闭包分配的资源,或者在与程序语义无关时完全省略创建它们:
\n实现可以检查 lambda 表达式中要捕获的自由变量的集合,当该集合为空时,可以避免引入环境部分,因此类函数实体将不需要维护闭包。这种策略通常在静态语言的规则中强制执行。
\n否则,实现可能会也可能不会总是为通过评估 lambda 表达式是否有要捕获的变量而产生的类似函数的实体创建闭包。
\nLambda 表达式可以计算为类似函数的实体。某些 PL 的用户可能将这种类似函数的实体称为“闭包”。在这种情况下,“匿名函数”应该是这种“闭包”的更中性的名称。
\n这与问题没有直接关系,但也值得注意的是“函数”可以在不同的上下文中命名不同的实体。
\n数学已经是一团乱麻了。
\n目前,我懒得在 PL 的上下文中总结它们,但需要注意的是:请注意上下文,以确保不同 PL 中“函数”的各种定义不会使您的推理偏离主题。
\n不过,就一般使用“匿名函数”(在实践中由 PL 共享)而言,我相信它不会在这个主题上带来重大的混乱和误解。
\n命名函数可能会有稍微多一点的问题。函数可以表示名称本身的实体(“符号”),以及这些名称的评估值。鉴于大多数 PL 没有未评估的上下文来区分函数与其他一些具有有趣含义的实体(例如,sizeof(a_plain_cxx_function)
在 C++ 中只是格式错误),用户可能不会观察到未评估的操作数和评估值之间的误解差异。对于某些具有QUOTE
. 即使是经验丰富的 PL 专家也很容易错过一些重要的事情;这也是为什么我强调将句法结构与其他实体区分开来。