ChatGPT解决这个技术问题 Extra ChatGPT

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

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

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

Lambda 是一种语言结构(匿名函数),闭包是一种实现一流函数(无论是否匿名)的实现技术。不幸的是,这常常被许多人混淆。
对于 PHP 闭包,请参阅 php.net/manual/en/class.closure.php 。这不是 JavaScript 程序员所期望的。
SasQ 的回答非常好。恕我直言,如果这个问题能引导观众找到答案,那么这个问题对 SO 用户会更有用。

B
Ben

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()

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

另一个重要点 - func 将继续关闭 anotherfunc 的环境,即使它不再在 anotherfunc 中进行评估。此代码也将起作用:

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

print anotherfunc(10)()

这将打印 10。

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


Claudiu,据我不确定的知识,python 从来没有完全正确的闭包。他们是否在我不注意的时候解决了可变性问题?很有可能...
@AlexanderOrlov:它们都是 lambdas 和闭包。 Java 之前通过匿名内部类进行了闭包。现在,通过 lambda 表达式,该功能在语法上变得更容易了。因此,新功能最相关的方面可能是现在有 lambda。称它们为 lambdas 并没有错,它们确实是 lambdas。我不知道为什么 Java 8 作者可能选择不强调它们是闭包这一事实。
@AlexanderOrlov 因为Java 8 lambdas 不是真正的闭包,它们是闭包的模拟。它们更类似于 Python 2.3 闭包(没有可变性,因此要求引用的变量是“有效最终的”),并且在内部编译为将封闭范围中引用的所有变量作为隐藏参数的非闭包函数。
@Claudiu我认为对特定语言实现(Python)的引用可能会使答案过于复杂。这个问题完全与语言无关(也没有特定于语言的标签)。
@Kevin,但重点不是重新绑定和创建新的阴影局部变量;它确实改变了一个闭包中绑定的值并在另一个引用相同绑定的闭包中看到了新值。当然,正如您所说,改变结构(或“存储”)也可以模仿这一点。
D
Derek Mahar

围绕 lambda 和闭包存在很多混淆,即使在此处对 StackOverflow 问题的回答中也是如此。与其随机询问那些从使用某些编程语言或其他无知的程序员实践中了解闭包的程序员,不如踏上寻找源头的旅程(一切开始的地方)。由于 lambdas 和闭包来自 Alonzo Church 在 30 年代发明的 Lambda Calculus,当时还没有第一台电子计算机,这就是我所说的来源。

Lambda Calculus 是世界上最简单的编程语言。您可以在其中做的唯一事情:►

应用:将一个表达式应用于另一个表达式,表示为 f x。 (把它想象成一个函数调用,其中 f 是函数,x 是它的唯一参数)

抽象:绑定出现在表达式中的符号以标记该符号只是一个“槽”,一个等待填充值的空白框,一个“变量”。它是通过在前面加上一个希腊字母 λ (lambda),然后是符号名称(例如 x),然后是一个点来完成的。在表达式之前。然后,这会将表达式转换为需要一个参数的函数。例如: λx.x+2 采用表达式 x+2 并告诉该表达式中的符号 x 是一个绑定变量——它可以用您作为参数提供的值替换。请注意,以这种方式定义的函数是匿名的——它没有名称,所以你还不能引用它,但你可以通过向它提供它正在等待的参数来立即调用它(还记得应用程序吗?),比如this: (λx.x+2) 7. 然后将表达式(在本例中为文字值)7 替换为应用 lambda 的子表达式 x+2 中的 x,因此得到 7+2,然后减少为 9通过常见的算术规则。

所以我们解决了其中一个谜团:
lambda 是上例 λx.x+2 中的 匿名函数

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 抽象所做的是绑定其子表达式中的一个符号,以便它成为一个可替换的参数。这样的符号称为bound。但是如果表达式中还有其他符号怎么办?例如:λx.x/y+2。在这个表达式中,符号 x 由它前面的 lambda 抽象 λx. 绑定。但是另一个符号 y 没有被绑定——它是自由的。我们不知道它是什么以及它来自哪里,所以我们不知道它意味着什么以及它代表什么,因此我们无法评估该表达式直到我们弄清楚 y 的含义。

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

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

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

CLOSED 表达式:出现在这些表达式中的每个符号都受某种 lambda 抽象约束。换句话说,它们是独立的;它们不需要评估任何周围的上下文。它们也被称为组合子。

OPEN 表达式:这些表达式中的一些符号是不受约束的——也就是说,其中出现的一些符号是自由的,它们需要一些外部信息,因此在您提供这些符号的定义之前,它们无法被计算。

您可以通过提供环境来关闭一个开放的 lambda 表达式,该环境通过将所有这些自由符号绑定到某些值(可能是数字、字符串、匿名函数又名 lambda 等)来定义所有这些自由符号。

这里是闭包部分: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)中的所有“未定义”(自由)符号和几个额外符号(q、{5 })。我们需要定义的符号是环境的这个子集:

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

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

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

好吧,应该归咎于 Sun/Oracle、Microsoft、Google 等企业市场,因为这就是他们在他们的语言(Java、C#、Go 等)中所说的这些结构。他们经常称“闭包”应该只是 lambdas。或者他们将“闭包”称为他们用来实现词法作用域的一种特殊技术,也就是说,一个函数可以访问在其定义时在其外部作用域中定义的变量这一事实。他们常说,函数“封装”了这些变量,也就是将它们捕获到某种数据结构中,以防止它们在外部函数执行完毕后被破坏。但这只是事后虚构的“民间传说词源”和营销,只会让事情变得更加混乱,因为每个语言供应商都使用自己的术语。

更糟糕的是,他们所说的总是有一点道理,这不允许您轻易将其视为错误:P 让我解释一下:

如果你想实现一种使用 lambdas 作为一等公民的语言,你需要允许它们使用在其周围上下文中定义的符号(即,在你的 lambdas 中使用自由变量)。即使周围的函数返回,这些符号也必须存在。问题是这些符号绑定到函数的一些本地存储(通常在调用堆栈上),当函数返回时,这些符号将不再存在。因此,为了让 lambda 以您期望的方式工作,您需要以某种方式从其外部上下文中“捕获”所有这些自由变量并将它们保存以供以后使用,即使外部上下文将消失。也就是说,您需要找到 lambda 的闭包(它使用的所有这些外部变量)并将其存储在其他地方(通过制作副本,或者通过预先为它们准备空间,而不是在堆栈上的其他地方)。你用来实现这个目标的实际方法是你的语言的“实现细节”。这里重要的是闭包,它是来自 lambda 环境的一组自由变量,需要保存在某个地方。

人们很快就开始调用他们在语言实现中使用的实际数据结构来实现闭包作为“闭包”本身。该结构通常看起来像这样:

Closure {
   [pointer to the lambda function's machine code],
   [pointer to the lambda function's environment]
}

这些数据结构作为参数传递给其他函数,从函数返回,并存储在变量中,以表示 lambda,并允许它们访问其封闭环境以及在该上下文中运行的机器代码。但这只是实现闭包的一种方式(其中一种),而不是闭包本身。

正如我上面所解释的,lambda 表达式的闭包是其环境中定义的子集,它为该 lambda 表达式中包含的自由变量提供值,有效地关闭表达式(将尚未计算的开放 lambda 表达式转换为一个封闭的 lambda 表达式,然后可以对其求值,因为其中包含的所有符号现在都已定义)。

其他任何东西都只是程序员和语言供应商的“货物崇拜”和“巫术魔法”,他们不知道这些概念的真正根源。

我希望这能回答你的问题。但是,如果您有任何后续问题,请随时在评论中提出,我会尽力解释得更好。


最佳答案笼统地解释事物而不是特定语言
在解释事情时,我喜欢这种方法。从一开始就解释事情是如何运作的,然后解释当前的误解是如何产生的。这个答案需要到顶部。
虽然 Lambda 演算对我来说感觉像是机器语言,但我必须同意它是一种与“制造”语言相比的“发现”语言。因此更少受任意约定的影响,更适合捕捉现实的底层结构。我们可以在 Linq、JavaScript、F# 中找到更平易近人的细节,但 Lambda 演算可以毫不费力地触及问题的核心。
感谢您多次重申您的观点,每次措辞略有不同。它有助于强化这个概念。我希望更多的人这样做。
你说的。这个答案中有很多错误和误导/令人困惑的陈述,对他们来说一些是真实的。对于初学者来说,Lambda Calculus 中没有闭包,因为 Lambda Calculus (cc @ap-osd) 中没有环境。顺便说一句,恭喜! Google 现在在 this search 上显示您的错误定义。实际上,闭包是 lambda 表达式与其定义环境的配对。没有副本,没有子集,它必须是原始帧本身(its 指针在链上),因为它不是关于值,而是关于 bindings
M
Mark Cidade

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

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;
           };
 }

你在这里用什么语言?
它基本上是伪代码。其中有一些 lisp 和 JavaScript,以及我正在设计的一种语言,称为“@”(“at”),以变量声明运算符命名。
@MarkCidade,那么这种语言在哪里@?有文档和下载吗?
为什么不使用 Javascript 并添加一个以 @ 符号前导声明变量的约束?这样可以节省一点时间:)
@Pacerier:我开始实现语言:github.com/marxidad/At2015
H
Honest Abe

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

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

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

在面向对象的语言中,闭包通常是通过对象提供的。但是,一些 OO 语言(例如 C#)实现的特殊功能更接近于由纯粹的 functional languages(例如 lisp)提供的没有对象来包围状态的闭包的定义。

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


那么,我们可以说闭包是 lambdas 的一个子集,而 lambdas 是函数的一个子集吗?
闭包是 lambdas 的一个子集……但 lambdas 比普通函数更特殊。就像我说的,lambda 是内联定义的。基本上没有办法引用它们,除非它们被传递给另一个函数或作为返回值返回。
Lambda 和闭包都是所有函数的子集,但 lambda 和闭包之间只有一个交集,其中闭包的不相交部分将被命名为闭包函数,而不相交的 lamdas 是具有完全-绑定变量。
在我看来,lambdas 是比函数更基本的概念。这真的取决于编程语言。
Roarrr... 一些事实: (1) 闭包不一定是函数。 (2) Lisp 不是纯函数式的。 (3) Lisp 确实有对象;它只是传统上将“对象”视为“值”的同义词,然后将“对象”的定义覆盖为其他事物(例如,通过 CLOS)。
D
Developer

就这么简单: lambda 是一种语言结构,即匿名函数的简单语法;闭包是一种实现它的技术——或者任何一流的函数,就此而言,命名的或匿名的。

更准确地说,闭包是 first-class function 在运行时的表示方式,作为它的一对“代码”和一个“关闭”该代码中使用的所有非局部变量的环境。这样,即使它们起源的外部范围已经退出,这些变量仍然可以访问。

不幸的是,有许多语言不支持函数作为一等值,或者只支持残缺的形式。所以人们经常用“闭包”这个词来区分“真货”。


W
Wei Qiu

从编程语言的角度来看,它们是完全不同的两种东西。

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

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

(lambda (x y) (+ x y))

您可以重命名参数,但它完成的任务不会改变。在几乎所有的编程语言中,您都可以为 lambda 表达式命名,即命名函数。但是没有太大区别,它们在概念上可以被认为只是语法糖。

好的,现在想象一下如何实现。每当我们将 lambda 表达式应用于某些表达式时,例如

((lambda (x y) (+ x y)) 2 3)

我们可以简单地将参数替换为要评估的表达式。这个模型已经很强大了。但是这个模型不能让我们改变符号的值,例如我们不能模仿状态的变化。因此我们需要一个更复杂的模型。简而言之,每当我们想要计算 lambda 表达式的含义时,我们都会将这对符号和对应的值放入一个环境(或表)中。然后通过查找表中的相应符号来评估其余的 (+ xy)。现在,如果我们提供一些原语来直接对环境进行操作,我们就可以对状态的变化进行建模!

在此背景下,检查此功能:

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

我们知道,当我们计算 lambda 表达式时,xy 将被绑定到一个新表中。但是我们如何以及在哪里查找 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 替换所有闭包,直到不再存在自由变量?在这种情况下,我会说闭包可以看作是一种特殊的 lambda。
一些问题。 (1) 这里的“减少”似乎含糊不清。在术语重写系统中,lambda 抽象也是 redex 的实例,它们将根据 Scheme 的规则重写为过程的值。你的意思是“变量引用”吗? (2) 抽象不是使语言图灵完备所必需的,例如组合逻辑没有抽象。 (3) 许多现代语言中的命名函数是独立于 lambda 表达式构建的。其中一些具有 lambda 表达式不共享的特殊功能,例如重载。
(4) 在 Scheme 中,对象就是值。最好避免将模棱两可的术语混在一起。 (5) 闭包不需要存储抽象的句法元素(另外还有其他运算符可以是抽象),因此闭包不是包含任何“lambda”内容的对。 (不过,仍然比断言“闭包是函数”的答案更正确。)
D
Developer

概念与上面描述的相同,但如果您是 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();

Nicely explained in this article.


A
Arsen Khachaturyan

这个问题很老,得到了很多答案。现在有了作为非官方闭包项目的 Java 8 和 Official Lambda,它重新提出了这个问题。

Java 上下文中的答案(通过 Lambdas and closures — what’s the difference?):

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


Java中的闭包如何实现Lamdas?这是否意味着 Lamdas 表达式被转换为旧式匿名类?
f
feilengcui008

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


j
j2emanue

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,在它没有语法之前。 Lambdas 是一个捷径。

关闭:

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

在 Kotlin 中,lambda 始终可以访问其闭包(位于其外部范围内的变量)


y
yoAlex5

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";});
    }
}

斯威夫特[Closure]

class MyClass {
    func foo(r:() -> Void) {}
    
    func lambdaExample() {
        foo(r: {})
    }
    
    var s = "hello"
    func closureExample() {
        foo(r: {s = "world"})
    }
}

F
FrankHB

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

术语说明

最好知道,术语“闭包”和“lambda”都可以表示不同的事物,具体取决于上下文。

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

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

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

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

注意 为了使事情更清晰、更准确,我很少故意使用一些与语言无关的术语(通常特定于 PL theory 而不是语言定义的术语。例如,type inhabitant上面使用的涵盖了语言特定的“(r)values”和更广泛意义上的“lvalues”。(由于 C++ 的 value category 定义的句法本质是无关紧要的,避免使用“(l/r)values” " 可能会减少混淆)。(免责声明:在许多其他情况下,左值和右值 common 就足够了。)在不同的 PL 中未正式定义的术语可能会用引号引起来。来自参考材料的逐字复制也可以用引号引起来,但拼写错误没有改变。

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

现代 PL 中的相关结构通常称为“lambda 表达式”。它源自下面讨论的“lambda 抽象”。

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

Lambdas:简史

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

此类构造源自 lambda 抽象,它是 A. Church 开发的 (untyped) lambda calculus 的三个语法类别(“表达式种类”)之一。

Lambda 演算是一个推导系统(更准确地说,是一个 TRS (term rewrite system)),用于对计算进行普遍建模。减少 lambda 项的 a 就像评估普通 PL 中的表达式一样。使用内置的归约规则,定义各种计算方式就足够了。 (您可能知道,it is Turing-complete。)因此,它可以用作 PL。

注:评估 PL 中的表达式通常不能与减少 TRS 中的项互换。然而,lambda 演算是一种语言,所有归约结果都可以在源语言中表达(即作为 lambda 项),因此它们具有相同的含义。几乎所有实践中的 PL 都不具备此属性;描述其语义的演算可能包含不是源语言表达式的术语,并且归约可能比评估具有更详细的效果。

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

注意 lambda 抽象只能引入一个参数。要克服微积分内部的限制,请参见Currying

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

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

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

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

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

尽管如此,越来越多的工业程序员最终发现 first-class functions 的用处,并且对能够就地引入函数定义(在任意上下文中的表达式中,例如,作为某个其他函数的参数)的需求正在增加。避免命名不需要的事物是自然且合法的,并且根据定义,任何命名函数在此都会失败。 (您可能知道,naming things correctly is one of the well-known hard problems in the computer science。)为了解决这个问题,匿名函数被引入到传统上只提供命名函数(或类似函数的构造,如“方法”)的语言中,如 C++ 和 Java。他们中的许多人将该功能命名为“lambda 表达式”或类似的 lambda 事物,因为它们基本上反映了 lambda 演算中基本相同的想法。 文艺复兴。

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

闭包:历史

Closures in mathematicsit in PLs 不同。

在后一种情况下,术语 is coined by P. J. Landin in 1964,在“以 Church 的 λ-notation 为模型”评估 PL 的实现中提供一流函数的支持。

特定于 Landin(SECD machine)、a closure is comprising the λ-expression and the environment relative to which it was evaluated 或更准确地说:

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

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

NOTE AE在论文中是applicative expression的缩写。这是在 lambda 演算中暴露或多或少相同的应用程序功能的语法。不过,还有一些额外的细节,如 "applicative",在 lambda 演算中并不那么有趣(因为它纯粹是函数式的)。 SECD 与这些微小差异的原始 lambda 演算不一致。例如,无论子项(“body”)是否具有范式,SECD 都会在任意单个 lambda 抽象上停止,因为它不会在没有应用抽象(“called”)的情况下减少子项(“evaluate the body”)。然而,这种行为可能更像今天的 PL,而不是 lambda 演算。 SECD 也不是唯一可以评估 lambda 项的抽象机器;尽管用于类似目的的大多数其他抽象机器也可能有环境。与 lambda 演算(纯粹的)相比,这些抽象机器可以在一定程度上支持变异。

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

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

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

尽管一开始模仿了 lambda 表示法,但 LISP 与这里的 lambda 演算非常不同。 lambda 演算是静态作用域的。也就是说,每个变量都表示由 lambda 抽象的最接近的相同命名形式参数限定的实例,该 lambda 抽象包含在其归约之前的变量。在 lambda 演算的语义中,减少应用程序将术语(“参数”)替换为抽象中的绑定变量(“形式参数”)。由于在 lambda 演算中所有值都可以表示为 lambda 项,因此这可以通过在归约的每个步骤中替换特定子项来直接重写来完成。

注意 因此,环境对于减少 lambda 项并不是必不可少的。然而,扩展 lambda 演算的演算可以在文法中显式地引入环境,即使它只对纯计算(没有变异)进行建模。通过显式添加环境,可以对环境有专门的约束规则来强制执行环境归一化,从而加强微积分的方程理论。 (见 [Shu10] §9.1。)

LISP 完全不同,因为它的基本语义规则既不是基于 lambda 演算,也不是基于术语重写。因此,LISP 需要一些不同的机制来维护范围规则。它采用了基于环境数据结构保存变量到值映射(即变量绑定)的机制。在 LISP 的新变体中的环境中可能存在更复杂的结构(例如,词法范围的 Lisp 允许突变),但最简单的结构在概念上等同于 Landin 论文定义的环境,如下所述。

LISP 实现在早期确实支持一流的函数,但是对于纯动态范围,没有真正的函数参数问题:它们可以避免堆栈上的分配并让全局所有者(GC、垃圾收集器)来管理引用变量的环境(和激活记录)中的资源。那时不需要闭包。这是闭包发明之前的早期实现。

近似于静态(词法)绑定的深度绑定是在 1962 年左右通过 FUNARG 设备在 LISP 1.5 中引入的。这最终使这个问题以“Funarg 问题”的名义广为人知。

注意 AIM-199指出这主要是关于环境的。

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

闭包:演变

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

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

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

此外,环境部分可以保存其他信息,而不是用于 lambda 演算的评估。例如,it can keep an extra identifier to provide additional binding naming the environment at the call site。这可以实现基于 lambda 演算扩展的语言。

重新审视特定于 PL 的术语

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

尽管如此,这仍然比语言规范任意重载更完善的通用概念要好。仅举几例:

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

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

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

关于资源管理

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

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

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

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

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

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

所以,(虽然这里没有看到)the claim of the necessity of graph tracing for closures by Thomas Lord at LtU 在技术上也是不正确的。闭包可以解决 funarg 问题,因为它可以防止对激活记录(堆栈)的无效访问,但事实并不能神奇地断言对包含闭包的资源的每个操作都是有效的。这种机制依赖于外部执行环境。应该清楚,即使在传统的实现中,隐式所有者(GC)也不是闭包的一个组件,而所有者的存在是 SECD 机器的实现细节(因此它是给用户的“高阶”细节)。这样的细节是否支持图追踪对闭包的限定没有影响。此外,AFAIK,the language constructs let combined with rec is first introduced (again by P. Landin) in ISWIM in 1966,它无法强制执行比它自己更早发明的闭包的原始含义。

关系

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

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

(1.1) 控制部分源自一些源语言结构,指定了类函数实体的求值结构;

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

(1.3) (1.2) 中的环境由类函数实体的潜在上下文相关源语言构造确定,用于保存捕获的自由变量,出现在创建类函数实体的源语言构造的评估构造中.

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

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

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

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

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

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

附录:函数:凌乱的历史

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

它已经是 a mess in mathematics

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

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

命名函数可能会有更多的问题。函数可以表示名称本身的实体(“符号”),以及这些名称的评估值。鉴于大多数 PL 没有未评估的上下文来区分函数与其他一些带有有趣含义的实体(例如,C++ 中的 sizeof(a_plain_cxx_function) 格式错误),用户可能不会观察到未评估操作数和评估值。对于某些具有 QUOTE 的 Lisp 方言,这将是一个问题。 Even experienced PL specialists can easily miss something important;这也是我强调区分句法结构与其他实体的原因。


A
Adeel

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

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

Lambda 表达式是无状态的,因为它依赖于参数、内部变量或常量来执行操作。函数<整数,整数> lambda = t -> { int n = 2 return t * n }

闭包保持状态是因为它使用外部变量(即定义在函数体范围之外的变量)以及参数和常量来执行操作。 int n = 2 Function 闭包 = t -> { return t * n }

当 Java 创建闭包时,它会将变量 n 保存在函数中,以便在传递给其他函数或在任何地方使用时都可以引用它。


(1)“外部变量”有一个更好的规范名称:“自由变量”。 (2) 闭包可以保持状态,但并非总是如此。如果除了绑定的变量(当本地环境满足空间安全属性时,对于典型的静态语言通常如此),它们甚至不需要保存对变量的引用。
m
mtmrv

这个问题已经有 12 年历史了,我们仍然将它作为 Google 中“closures vs lambda”的第一个链接。所以我不得不说,因为没有人明确表示。

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

引用 Scott 的 Programming Language Pragmatics 的闭包解释为:

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

也就是我们所说的“函数+放弃上下文”的捆绑。


通过强调“显式表示”,此处的闭包定义在技术上比该问题的其他一些答案更精确,尽管在许多方面仍然存在微妙的问题(例如,实际上可以捆绑多个引用环境,并且子例程不是必须通过参考捆绑)。
a
ap-osd

Lambda 是一个匿名函数定义,未(必然)绑定到标识符。

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

闭包是 lambda 函数的实现。

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

Lambda 和 Closure 的一般解释在其他响应中进行了介绍。

对于那些具有 C++ 背景的人,Lambda 表达式是在 C++11 中引入的。将 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*/ } );
}

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


闭包是 lambda 函数(即函数定义)及其定义环境的配对。故事结局。
@WillNess 这在技术上是不正确的,正如 Wei Qiu 的回答中的评论所解释的那样。这里关注的一个更直接的原因是 C++ 的“闭包”根据定义命名了一些 C++ 对象。
@FrankHB 你的评论毫无意义。你提到的答案重复了我让你反对的同一点。该答案以:“闭包是一对 lambda 和相应的环境”结尾。您可能会从现代 C++ POV 中了解到这一点,但这些概念在 Lisp 及其衍生语言中已经确立了半个世纪。
@WillNess 通过淡出 funargs problem 背景,您的“毫无意义”的评论完全没有意义。这个问题最早是由古老的 LISP 实现发现并流行起来的,而(词法)闭包正是该问题的解决方案。具有讽刺意味的是,lexical 闭包不是由 Lisps 发明的,而是由 ALGOL 发明的。第一个采用该解决方案的 Lisp 方言是 Scheme。最初的 LISP 使用动态范围,在接下来的十年中,大多数 Lisp 社区都接受了这种变化。这就是你学到的历史,与 C++ 无关。
@WillNess 如果您坚持更广泛的范围,“关闭”一词是由 P. Landing 在他的 SECD 机器中创造的。诚然,这一次它不一定与“词法”有关,但在此处的上下文中它也是无用的,因为除了强制词法闭包之外,这样的闭包没有任何必要。换句话说,如果没有词法作用域的意义,“闭包”只是一个历史实现细节,无法与“lambdas”进行比较,“lambdas”始终是可用的源语言设备。