“闭包”和“lambda”有什么区别?

有人能解释一下吗?我理解它们背后的基本概念,但我经常看到它们可以互换使用,我感到困惑。

现在我们在这里,它们与常规函数有何不同?

166892 次浏览

并非所有闭包都是lambda,也并非所有lambda都是闭包。两者都是函数,但不一定是我们习惯知道的方式。

lambda本质上是一个内联定义的函数,而不是声明函数的标准方法。Lambda可以经常作为对象传递。

闭包是一种通过引用其主体外部的字段来封闭其周围状态的函数。封闭状态在调用闭包时保持不变。

在面向对象语言中,闭包通常是通过对象提供的。然而,一些面向对象语言(例如C#)实现了更接近纯粹函数式语言(例如lisp)提供的闭包定义的特殊功能,这些闭包没有对象来封闭状态。

有趣的是,C#中Lambdas和闭包的引入使函数式编程更接近主流使用。

当大多数人想到功能时,他们会想到命名函数

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 numbersfilter([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.xy(); //returns 1 (y.x == 0 + 1)y(); //returns 2 (y.x == 1 + 1)

这就是函数式编程中获取有状态对象的方式。由于不需要命名“增量X”,因此在这种情况下可以使用lambda:

function foo(){ @x = 0;
return lambda(){ x = x + 1;return x;};}

lambda只是一个匿名函数——一个没有名称定义的函数。在某些语言中,如Schem,它们等价于命名函数。事实上,函数定义被重写为在内部将lambda绑定到变量。在其他语言中,如Python,它们之间有一些(相当不必要的)区别,但它们的行为方式相同。

关闭是定义它的结束环境的任何函数。这意味着它可以访问不在其参数列表中的变量。例子:

def func(): return hdef anotherfunc(h):return func()

这将导致错误,因为func关闭超过anotherfunc-h中的环境未定义。func仅关闭全局环境。这将起作用:

def anotherfunc(h):def func(): return hreturn func()

因为在这里,func是在anotherfunc中定义的,在python 2.3及更高版本(或类似的数字)中,当几乎正确地获得闭包时(突变仍然不起作用),这意味着它结束anotherfunc的环境可以访问其中的变量。在Python 3.1+中,使用#3关键字时突变也有效。

另一个要点-即使anotherfunc不再对func的环境进行评估,func也将继续关闭anotherfunc的环境。这段代码也可以工作:

def anotherfunc(h):def func(): return hreturn func
print anotherfunc(10)()

这将打印10。

正如你所注意到的,这与lambda无关-它们是两个不同的(尽管相关)概念。

从编程语言的角度来看,它们完全是两码事。

基本上,对于图灵完备语言,我们只需要非常有限的元素,例如抽象、应用和约简。抽象和应用提供了构建lamdba表达式的方式,约简终止了lambda表达式的含义。

Lambda提供了一种可以抽象计算过程的方法。例如,要计算两个数的和,可以抽象出一个接受两个参数x, y并返回x+y的过程。在方案中,您可以将其编写为

(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来确定。为了清楚这一点,您可以在方案中编写如下内容:

((lambda (z) (lambda (x y) (+ x y z))) 1)

所以z在外表中绑定到1。我们仍然得到一个接受两个参数的函数,但它的真正含义也取决于外部环境。换句话说,外部环境关闭了自由变量。在set!的帮助下,我们可以使函数有状态,即它不是数学意义上的函数。它返回的内容不仅取决于输入,还取决于z。

这是你已经非常清楚的事情,对象的方法几乎总是依赖于对象的状态。这就是为什么有些人说“闭包是穷人的对象”。但是我们也可以把对象视为穷人的闭包,因为我们真的很喜欢第一类函数。

我使用方案来说明这些想法,因为方案是最早的具有真正闭包的语言之一。这里的所有材料在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; });

函数($v){返回$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();

这篇文章很好地解释了。

这个问题很老,有很多答案。
现在,Java8和官方Lambda是非官方的关闭项目,它重新提出了这个问题。

答案在Java上下文(通过Lambdas和闭包-有什么区别?):

"闭包是一个lambda表达式与一个将其每个自由变量绑定到一个值的环境配对。在Java,lambda表达式将通过闭包实现,因此这两个术语在社区中可以互换使用。"

简单来说,闭包是一个关于作用域的技巧,lambda是一个匿名函数。我们可以更优雅地实现lambda的闭包,lambda通常用作传递给更高函数的参数

关于lambdas和闭包有很多困惑,即使在这里StackOverflow问题的答案中也是如此。与其问随机的程序员,他们从实践中学习了某些编程语言或其他无知的程序员,不如去来源(一切开始的地方)。由于lambdas和闭包来自于Alonzo Church在30年代发明的lambda演算,当时第一台电子计算机甚至还没有出现,这就是我正在谈论的来源

Lambda演算是世界上最简单的编程语言。你只能用它做的事情:

  • 应用:将一个表达式应用于另一个表达式,表示为f x
    (将其视为函数调用,其中f是函数,x是其唯一参数)
  • 摘要:绑定表达式中出现的符号以标记该符号只是一个“槽”,一个等待填充值的空白框,一个“变量”。它是通过在希腊字母λ(lambda)之前加上前缀来完成的,然后是符号名称(例如x),然后是表达式前的点.。然后将表达式转换为函数,期待一个参数
    例如:λx.x+2接受表达式x+2并告诉此表达式中的符号x绑定变量-它可以用您提供的参数值替换。
    请注意,以这种方式定义的函数是匿名——它没有名称,所以你还不能引用它,但是你可以通过提供它正在等待的参数来立即打电话它(还记得应用程序吗?),如下所示:(λx.x+2) 7。然后表达式(在这种情况下是文字值)7在应用的lambda的子表达式x+2中被替换为x,所以你得到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抽象的作用是绑定在其子表达式中成为一个符号,因此它成为可替换的参数。这样的符号称为绑定。但是如果表达式中还有其他符号怎么办?例如:λx.x/y+2。在这个表达式中,符号x被它前面的lambda抽象λx.绑定。但是另一个符号y没有绑定——它是免费。我们不知道它是什么,它来自哪里,所以我们不知道它意味着x0代表什么,因此在弄清楚y的含义之前,我们无法评估该表达式。

事实上,其他两个符号2+也是如此。只是我们太熟悉这两个符号了,以至于我们经常忘记计算机不知道它们,我们需要通过在某个地方定义它们来告诉它它们的意思,例如在图书馆或语言本身。

你可以把免费符号想象成在表达式之外的其他地方定义的,在它的“周围上下文”中,这被称为它的环境。环境可能是一个更大的表达式,这个表达式是它的一部分(正如Qui-Gon Jinn所说:“总有一条更大的鱼" ;) ), 或在某个库中,或在语言本身(作为原始)中。

这让我们将lambda表达式分为两类:

  • 关闭表达式:在这些表达式中出现的每个符号通过一些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  }

这个环境提供了我们lambda表达式(y+2)中所有“未定义”(自由)符号的定义,以及几个额外的符号(qw)。我们需要定义的符号是环境的这个子集:

{  y: 3,+: [built-in addition],2: [built-in number]  }

这正是我们的lambda表达式的关闭:>

换句话说,它关闭是一个开放的lambda表达式。这就是关闭这个名字最初的来源,这就是为什么这个线程中这么多人的答案不太正确的原因:P


为什么他们中的很多人说闭包是内存中的一些数据结构,或者他们使用的语言的一些特性,或者为什么他们把闭包和lambda混淆了?: P

这应该归咎于Sun/Oracle、微软、谷歌等公司,因为他们用自己的语言(Java、C#、Go等)这样称呼这些结构。他们通常把本应只是lambda的东西称为“闭包”。或者他们把“闭包”称为他们用来实现词法作用域的一种特殊技术,即一个函数可以访问在其定义时在其外部作用域中定义的变量。他们经常说该函数“封闭”这些变量,也就是说,将它们捕获到某种数据结构中,以防止它们在外部函数执行完成后被销毁。但这只是编造的postfactum“民俗词源”和营销,这只会让事情变得更加混乱,因为每个语言供应商都使用自己的术语。

更糟糕的是,因为他们所说的总是有一点真理,这不允许你轻易地将其视为虚假: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魔术”,他们不知道这些概念的真正根源。

我希望这回答了你的问题。如果你还有什么后续问题,欢迎在评论区提出来,我会尽量解释清楚的。

它取决于函数是否使用外部变量来执行操作。

外部变量-定义在函数范围之外的变量。

  • Lambda表达式是无国籍,因为它依赖于参数、内部变量或常量来执行操作。

    Function<Integer,Integer> lambda = t -> {int n = 2return t * n}
  • Closures hold state because it uses external variables (i.e. variable defined outside the scope of the function body) along with parameters and constants to perform operations.

    int n = 2
    Function<Integer,Integer> closure = t -> {return t * n}

When Java creates closure, it keeps the variable n with the function so it can be referenced when passed to other functions or used anywhere.

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代码中构建的。现在您可以在某个地方调用mapPersonToJob.apply(person)来使用它。这只是一个例子。在有语法之前这是一个lambda。Lambda是这方面的捷径。

结束:

当Lambda可以访问这个范围之外的变量时,它就变成了闭包。我猜你可以说它的魔力,它神奇地可以环绕它创建的环境,并使用它的范围(外部范围)之外的变量。所以要清楚,闭包意味着lambda可以访问它的OUTER SCOPE。

在静态编程语言中,lambda总是可以访问它的闭包(在其外部作用域中的变量)。

Lambda是一个匿名函数定义,它(不一定)绑定到标识符。

“匿名函数起源于Alonzo Church的工作,他发明了lambda演算,其中所有函数都是匿名的”-维基百科

闭包是lambda函数的实现。

“Peter J. Landin在1964年将闭包定义为具有环境部分和控制部分,他的SECD机器用于评估表达式”-维基百科

Lambda和闭包的一般解释包含在其他的回答中。

对于C++背景的人来说,Lambda表达式是在C++11中引入的。将Lambdas视为创建匿名函数和函数对象的便捷方式。

lambda和相应闭包之间的区别恰恰等同于类和类实例之间的区别。类只存在于源代码中;它在运行时不存在。运行时存在的是类类型的对象。闭包之于lambda就像对象之于类一样。这不应该令人惊讶,因为每个lambda表达式都会导致一个唯一的类生成(在编译过程中),也会导致该类类型的对象、闭包创建(在运行时)。-Scott Myers

C++允许我们检查Lambda和闭包的细微差别,因为您必须显式指定要捕获的自由变量。

在下面的示例中,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*/ } );}

一旦在捕获列表([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)

请参阅lambda和闭包以获得C++示例的详细说明。

lambda vs闭包

Lambda匿名函数(方法)

Closure结束(从其封闭范围(例如非局部变量)捕获变量的函数

Java

interface Runnable {void run();}
class MyClass {void foo(Runnable r) {
}
//Lambdavoid lambdaExample() {foo(() -> {});}
//ClosureString 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中“闭包vs lambda”的第一个链接。所以我不得不说,因为没有人明确地说过。

Lambda表达式是一个匿名函数(声明)。

引用斯科特的编程语言语用学的闭包解释为:

…创建引用环境的显式表示(通常是当前调用时子例程将执行的环境)并将其与对子例程的引用捆绑在一起…被称为关闭

也就是说,它就像我们称之为“函数+交出上下文”的捆绑一样。

在这个问题的各种现有答案中,有许多技术上模糊或“甚至没有错误”的人造珍珠的噪音,所以我最后添加一个新的。

对术语的澄清

最好知道,术语“闭包”和“lambda”都可以根据上下文表示不同的东西。

这是一个形式问题,因为正在讨论的PL(编程语言)的规范可能会明确定义这些术语。

例如,通过ISOC++(自C++11):

lambda表达式的类型(也是闭包对象的类型)是一个唯一的、未命名的非联合类类型,称为闭包类型,其属性如下所述。

由于类C语言的用户每天都在混淆指向“指针值”或“指针对象”(类型的居民)的“指针”(类型),因此在这里也存在混淆的风险:大多数C++用户实际上通过使用术语“闭包”来谈论“闭包对象”。小心歧义。

为了让事情更清晰、更精确,我很少刻意使用一些语言中性的术语(通常是特定于PL理论而不是语言定义的术语。例如,上面使用的类型居民涵盖了更广泛意义上的语言特定的“(r)值”和“l值”。(由于C++值类别定义的语法本质无关紧要,避免“(l/r)值”可能会减少混乱)。(免责声明:左值和右值在许多其他上下文中已经足够了。)不同的PL之间没有正式定义的术语可能用引号。引用材料的逐字复印也可能用引号,错别字不变。

这与“lambda”更相关。(小写)字母lambda(λ)是希腊字母表的一个元素。与“lambda”和“闭包”相比,人们当然不是在谈论字母本身,而是使用“lambda”衍生概念的语法背后的东西。

现代PLs中的相关构造通常被命名为“lambda表达式”。它来源于“lambda抽象”,下面讨论。

在详细讨论之前,我建议阅读一些对问题本身的评论。我觉得它们比这里的大多数答案更安全、更有帮助,因为混淆的风险更小。(可悲的是,这是我决定在这里提供答案的最重要原因…)

Lambdas:一个简短的历史

PLs中以“lambda”命名的构造,无论“lambda表达式”或其他什么,都是句法。换句话说,语言的用户可以找到这样的源语言结构,用于构建其他东西。粗略地说,“其他”在实践中只是“匿名函数”。

这种结构起源于lambda抽象,这是A. Church开发的(无类型)lambda演算的三个语法类别(“表达式种类”)之一。

Lambda演算是一个通用计算建模的推导系统(更准确地说,是一个术语重写系统)。减少lambda项就像计算普通PLs中的表达式一样。使用内置的减少规则,定义各种计算方式就足够了。(正如你可能知道的,它是图灵完备的。)因此,它可以用作PL。

一般来说,计算PL中的表达式和约简TRS中的术语是不可互换的。然而,lambda演算是一种语言,所有约简结果都可以在源语言中表达(即作为lambda项),所以它们巧合地具有相同的含义。实践中几乎所有的PL都没有这个属性;描述它们语义学的演算可能包含不是源语言表达式的术语,约简可能比评估具有更详细的效果。

lambda演算中的每个术语(“表达式”)要么是变量,要么是抽象的,要么是应用程序。这里的“变量”是符号的语法(只是变量的名称),它可以引用之前引入的现有“变量”(语义上,一个可以简化为其他lambda项的实体)。引入变量的能力由抽象语法提供,抽象语法有一个前导字母λ,后面跟着一个绑定变量、一个点和一个lambda项。在许多语言中,绑定变量在语法和语义上都类似于形式参数名称,在lambda抽象中跟着的lambda项就像函数体一样。应用程序语法将lambda项(“实际参数”)与一些抽象相结合,例如许多PL中的函数调用表达式。

lambda抽象只能引入一个参数。要克服演算内部的限制,请参阅咖喱

引入变量的能力使lambda演算成为典型的高级语言(尽管简单)。另一方面,通过从lambda演算中删除变量和抽象特征,组合逻辑可以被视为PLs。组合逻辑正是在这个意义上是低级的:它们就像普通的汇编语言,不允许引入用户命名的变量(尽管有宏,这需要额外的预处理)。(…如果不是更低级的话…典型的汇编语言至少可以引入用户命名的标签。)

注意lambda抽象可以在任何其他lambda术语中就地构建,而不需要指定一个名称来表示抽象。因此,lambda抽象总体上形成了匿名函数(可能是嵌套的)。这是一个相当高级的功能(与例如ISO C相比,它不允许匿名或嵌套函数)。

无类型lambda演算的后继者包括各种类型lambda演算(如lambda立方体)。这些更像是静态类型语言,需要对函数的形式参数进行类型注释。尽管如此,lambda抽象在这里仍然具有相同的角色。

尽管lambda演算并不打算直接用作计算机中实现的PL,但它们在实践中确实影响了PL。值得注意的是,J. McCarthy在LISP中引入了LAMBDA运算符,以提供完全遵循Church无类型lambda演算思想的函数。显然,LAMBDA这个名字来自字母λ。LISP(后来)有不同的语法(S表达),但是LAMBDA表达式中的所有可编程元素都可以通过简单的语法转换直接映射到无类型lambda演算中的lambda抽象。

另一方面,许多其他PLs通过其他方式表达类似的功能。引入可重用计算的略有不同的方式是命名函数(或更准确地说,命名子程序),它们由早期PLs(如FORTRAN)和从ALGOL派生的语言支持。它们通过指定命名实体同时是一个函数的语法引入。与LISP方言相比,这在某种意义上更简单(尤其是在实现方面),而且几十年来似乎比LISP方言更流行。命名函数还可能允许匿名函数不共享的扩展,如函数重载。

尽管如此,越来越多的工业程序员终于发现了一级函数的有用性,并且对引入函数定义的能力(在任意上下文中的表达式中,例如,作为其他函数的参数)的需求正在增加。避免命名一个不需要的东西是自然和合理的,任何命名函数在这里都无法定义。(你可能知道,正确命名事物是计算机科学中众所周知的难题之一。)为了解决这个问题,匿名函数被引入到传统上只提供命名函数(或类似函数的构造,如“方法”,无论如何)的语言中,如C++和Java。他们中的许多人将该特性命名为“lambda表达式”或类似的lambda事物,因为它们基本上反映了lambda演算中基本相同的想法。

有点不歧义:在lambda演算中,所有术语(变量、抽象和应用程序)实际上都是PL中的表达式;从这个意义上说,它们都是“lambda表达式”。然而,添加lambda抽象以丰富其特征的PLs可能会将抽象的语法指定为“lambda表达式”,以与现有的其他类型的表达式区分开来。

关闭:历史

数学中的闭包不等于它在pls

在后一种情况下,术语是P. J. Landin在1964年创造的,在实现评估“以Church的λ符号建模”的PLs时提供一流函数的支持。

具体到Landin提出的模型(SECD机器),闭包包含λ表达式和评估它的环境,或者更准确地说:

环境部分是一个列表,其两个项目是(1)环境(2)标识符列表的标识符

和一个控制部分,它由一个列表组成,其唯一的项目是AE

本文中AE缩写为应用表达式。这是在lambda演算中暴露了应用程序或多或少相同的功能的语法。然而,还有一些额外的细节,如“适用”在lambda演算中没有那么有趣(因为它是纯函数的)。由于这些微小的差异,SECD与原始的lambda演算不一致。例如,SECD停止在任意单个lambda抽象上,无论子项(“body”)是否具有正常形式,因为如果没有应用抽象(“调用”),它不会减少子项(“评估body”)。然而,这种行为可能更像今天的PLs,而不是lambda演算。SECD也不是唯一可以评估lambda项的抽象机器;尽管大多数其他用于类似目的的抽象机器也可能有环境。与lambda演算(纯)相反,这些抽象机器可以在一定程度上支持突变。

因此,在这个特定的上下文中,闭包是一种内部数据结构,用于实现对具有AE的PLs的特定评估。

在闭包中访问变量的规则反映了词汇范围,该规则于20世纪60年代初由命令式语言ALGOL 60首次使用。ALGOL 60确实支持嵌套过程和将过程传递给参数,但不支持将过程作为结果返回。对于完全支持可以由函数返回的一级函数的语言,ALGOL 60风格实现中的静态链不起作用,因为被返回函数使用的自由变量可能不再存在于调用堆栈上。这是向上函数问题。闭包通过捕获环境部分中的自由变量并避免在堆栈上分配它们来解决问题。

另一方面,早期的LISP实现都使用动态作用域。这使得所有引用的变量绑定都可在全局存储中访问,并且名称隐藏(如果有)是作为每个变量的基础实现的:一旦用现有名称创建变量,旧变量就会得到LIFO结构的支持;换句话说,每个变量的名称可以访问相应的全局堆栈。这有效地取消了每个函数环境的需要,因为没有自由变量在函数中被捕获(它们已经被堆栈“捕获”了)。

尽管一开始模仿了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指出,这本质上是关于环境的。

方案默认为第一 Lisp方言支持词法范围(在现代版本的方案中,动态范围可以用make-parameter/parameterize形式模拟)。在后来的十年中有一些争论,但最终大多数Lisp方言采用默认为词法范围的想法,就像许多其他语言一样。从那时起,闭包作为一种实现技术,在不同风格的PLs中得到了更广泛的传播和流行。

闭包:演变

Landin的原始论文首先将环境定义为将名称(“常量”)映射到命名对象(“原始”)的数学函数。然后,它将环境定义为“由名称/值对组成的列表结构”。后者在早期的Lisp实现中也实现为主义者(关联列表),但现代语言实现不一定遵循这些细节。特别是,环境可以是链接来支持嵌套闭包,这不太可能由SECD等抽象机器直接支持。

除了环境之外,Landin论文中“环境部分”的另一个组件用于保留lambda抽象的绑定变量的名称(函数的形式参数)。这对于现代实现来说也是可选的(并且可能缺失),其中参数的名称可以静态优化掉(精神上由lambda演算的alpha重命名规则授予),当不需要反映源信息时。

类似地,现代实现可能不会直接将语法结构(AE或lambda术语)保存为控制部分。相反,它们可能使用一些内部IR(中间表示)或“编译”形式(例如Lisp方言的一些实现使用的FASL)。这样的IR甚至不能保证从lambda形式生成(例如它可以来自一些命名函数的主体)。

此外,环境部分可以保存不用于lambda演算评估的其他信息。例如,它可以保留一个额外的标识符来提供调用站点环境的附加绑定命名。这可以实现基于lambda演算扩展的语言。

重新审视PL特定术语

此外,一些语言可能会在其规范中定义“闭包”相关的术语来命名可能由闭包实现的实体。这是不幸的,因为它会导致许多误解,例如“闭包是一个函数”。但幸运的是,大多数语言似乎避免直接将其命名为语言中的句法结构。

尽管如此,这仍然比重载由语言规范任意设置的更成熟的通用概念要好。举几个例子:

  • “对象”被重定向到“类的实例”(在Java/CLR/“OOP”语言中),而不是传统“类型存储”(在C和C++中)或只是“值”(在许多Lisps中);

  • “变量”被重定向到传统的“对象”(在Golang中)以及可变状态(在许多新语言中),因此它不再兼容数学和纯函数式语言;

  • “多态性”仅限于包含多态(在C++/“OOP”语言中),即使这些语言确实有其他类型的多态性(参数多态性和即席多态性)。

关于资源管理

尽管现代实现中省略了组件,但Landin论文中的定义相当灵活。它不限制如何在SECD机器的上下文之外存储组件,如环境。

在实践中,使用了各种策略。最常见和传统的方法是使所有资源归全局所有者所有,该所有者可以收集不再使用的资源,即(全局)GC,首先在LISP中使用。

其他方法可能不需要全局所有者,并且对闭包具有更好的局部性,例如:

  • 在C++中,用户可以显式管理闭包中捕获的实体资源,方法是指定如何捕获lambda表达式捕获列表中的每个变量(通过值复制、引用,甚至通过显式初始化器)以及每个变量的确切类型(智能指针或其他类型)。这可能不安全,但正确使用时会获得更大的灵活性。

  • 在Rust中,资源以不同的捕获模式(通过不可变借用、借用、移动)依次(通过实现)捕获,用户可以指定显式的move。这比C++更保守,但在某种意义上更安全(因为借用是静态检查的,与C++中未经检查的引用捕获相比)。

上面的所有策略都可以支持闭包(C++和Rust确实对概念“闭包类型”有特定于语言的定义)。管理闭包使用的资源的规程与闭包的资格无关。

所以,(尽管这里没有看到,)中大托马斯·洛德关于闭包图追踪必要性的主张在技术上也是不正确的。闭包可以解决funarg问题,因为它允许防止对激活记录(堆栈)的无效访问,但事实并不能神奇地断言对组成闭包的资源的每个操作都是有效的。这种机制取决于外部执行环境。应该清楚的是,即使在传统的实现中,隐式所有者(GC)也不是闭包的组件,所有者的存在是SECD机器的实现细节(所以它对用户来说是“高阶”细节之一)。这些细节是否支持图跟踪对闭包的资格没有影响。此外,AFAIK,语言结构#0与#1的结合于1966年在ISWIM中首次引入(再次由P. Landin引入),它不能强制执行早于它自己发明的闭包的原始含义。

的关系

因此,总结起来,闭包可以(非正式地)定义为:

(1)PL实现特定的数据结构,包括作为环境部分和功能类实体的控制部分,其中:

(1.1)控制部分是从一些源语言结构派生出来的,这些结构指定了类函数实体的求值结构;

(1.2)环境部分由环境和可选的其他实现定义的数据组成;

(1.3)(1.2)中的环境由潜在的依赖于上下文的源语言构造的类函数实体确定,用于保持捕获的自由变量发生在创建类函数实体的源语言构造的评估构造中。

(2)或者,使用(1)中名为“闭包”的实体的实现技术的总括术语。

Lambda表达式(抽象)只是源语言中用于引入(创建)未命名类函数实体的语法构造。PL可能提供它作为引入类函数实体的唯一方法。

一般来说,源程序中的lambda表达式和程序执行中闭包的存在之间没有明确的对应关系。由于实现细节对程序的可观察行为没有影响,PL实现通常被允许在可能的情况下合并分配给闭包的资源,或者在程序语义学上无关紧要时完全省略创建闭包:

  • 实现可以检查lambda表达式中要捕获的自由变量的集合,当集合为空时,可以避免引入环境部分,因此类函数实体不需要维护闭包。这种策略通常是静态语言规则中强制要求的。

  • 否则,通过评估lambda表达式是否有要捕获的变量,实现可能会也可能不会总是为类似函数的实体创建闭包。

Lambda表达式可以被计算为类函数实体。一些PL的用户可能会将这种类函数实体称为“闭包”。在这种情况下,“匿名函数”应该是这种“闭包”的更中性的名称。

附录:功能:杂乱的历史

这与问题没有直接关系,但也值得注意的是,“函数”可以在不同的上下文中命名不同的实体。

已经是数学上的混乱了。

目前我懒得在PLs的上下文中总结它们,但作为一个警告:密切关注上下文,以确保不同PLs中“函数”的各种定义不会使你的推理偏离主题。

至于一般使用“匿名函数”(在实践中由PLs共享),我相信它不会在这个主题上引入重大的混淆和误解。

命名函数可能有更多的问题。函数可能表示名称本身的实体(“符号”),以及这些名称的评估值。鉴于大多数PLs没有未经评估的上下文来区分一个函数和其他一些承载有趣含义的实体(例如C++中的sizeof(a_plain_cxx_function)只是格式不正确),用户可能不会观察到未评估操作数和评估值之间误解的差异。这将是有问题的,一些Lisp方言有QUOTE.即使是经验丰富的PL专家也很容易错过一些重要的东西;这也是我强调区分句法结构和其他实体的原因。