912

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

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

4

15 回答 15

755

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

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

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

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

print anotherfunc(10)()

这将打印 10。

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

于 2008-10-21T03:58:32.583 回答
476

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

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

  • 应用:将一个表达式应用于另一个表达式,表示为f x
    (把它想象成一个函数调用,函数在哪里fx是它唯一的参数)
  • 抽象:绑定出现在表达式中的符号以标记该符号只是一个“槽”,一个等待填充值的空白框,一个“变量”。它是通过在前面加上一个希腊字母λ(lambda),然后是符号名称 (例如),然后在表达式之前加上x一个点来完成的。.然后,这会将表达式转换为需要一个参数的函数。例如:获取表达式并告诉该表达式中的符号是一个绑定变量——它可以用您作为参数提供的值替换。 注意这样定义的函数是匿名的
    λx.x+2x+2x
    – 它没有名称,因此您还不能引用它,但是您可以通过向它提供它正在等待的参数来立即调用(λx.x+2) 7它(还记得应用程序吗?),如下所示: . 然后表达式(在本例中为文字值)7被替换为应用的 lambdax的子表达式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; }

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

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

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

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

  • CLOSED 表达式:出现在这些表达式中的每个符号都受某种 lambda 抽象约束。换句话说,它们是独立的;它们不需要评估任何周围的上下文。它们也被称为组合子。
  • OPEN 表达式:这些表达式中的一些符号是不受约束的——也就是说,其中出现的一些符号是自由的,它们需要一些外部信息,因此在您提供这些符号的定义之前,它们不能被计算。

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

这里是闭包部分:lambda 表达式
闭包是在外部上下文(环境)中定义的一组特殊符号,它们为该表达式中的自由符号赋予值,使它们不再是非自由的。它将一个仍然包含一些“未定义”自由符号的开放lambda 表达式转换为一个不再有任何自由符号的封闭表达式。

例如,如果您有以下 lambda 表达式:λx.x/y+2,则符号x是绑定的,而符号y是自由的,因此除非您说出什么意思,否则该表达式是open并且不能被评估y(与 and 相同+2它们也是自由的)。但是假设你也有这样的环境

{  y: 3,
+: [built-in addition],
2: [built-in number],
q: 42,
w: 5  }

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

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

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

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


那么他们为什么会误会呢?为什么他们中的许多人说闭包是内存中的一些数据结构,或者他们使用的语言的一些特性,或者他们为什么将闭包与 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 表达式,然后可以对其求值,因为其中包含的所有符号现在都已定义)。

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

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

于 2016-04-27T01:18:04.737 回答
181

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

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;
           };
 }
于 2008-10-21T03:46:55.727 回答
57

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

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

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

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

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

于 2008-10-21T03:29:36.743 回答
17

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

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

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

于 2014-03-19T08:31:17.927 回答
13

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

基本上,对于图灵完备的语言,我们只需要非常有限的元素,例如抽象、应用和归约。抽象和应用程序提供了构建 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 的相应环境。

于 2014-03-19T00:11:15.193 回答
11

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

在这篇文章中很好地解释了。

于 2014-07-31T07:41:33.360 回答
8

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

Java 上下文中的答案(通过Lambda 和闭包——有什么区别?):

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

于 2014-11-09T17:46:45.397 回答
5

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

于 2015-05-16T07:03:23.073 回答
4

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 始终可以访问其闭包(位于其外部范围内的变量)

于 2019-04-14T21:57:21.503 回答
3

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

斯威夫特[关闭]

class MyClass {
    func foo(r:() -> Void) {}
    
    func lambdaExample() {
        foo(r: {})
    }
    
    var s = "hello"
    func closureExample() {
        foo(r: {s = "world"})
    }
}
于 2021-03-16T21:17:02.823 回答
1

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

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

  • 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 保存在函数中,以便在传递给其他函数或在任何地方使用时都可以引用它。

于 2018-09-24T09:34:44.743 回答
1

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

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

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

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

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

于 2021-08-22T09:59:53.720 回答
1

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

术语说明

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

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

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

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

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

注意为了使事情更清晰和更精确,我很少故意使用一些与语言无关的术语(通常特定于PL 理论而不是语言定义的术语。例如,上面使用的类型居民涵盖了特定于语言的“( r)values”和更广泛意义上的“lvalues”。(由于 C++值类别定义的句法本质无关紧要,避免“(l/r)values”可能会减少混淆)。(免责声明: lvalues和 rvalues在许多其他上下文。)未在不同 PL 中正式定义的术语可能会加引号。参考材料的逐字副本也可能加引号,但拼写错误保持不变。

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

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

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

Lambdas:简史

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

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

Lambda 演算是一个演绎系统(更准确地说,是一个TRS(术语重写系统)),用于对计算进行普遍建模。减少 lambda 项的 a 就像评估普通 PL 中的表达式一样。使用内置的归约规则,定义各种计算方式就足够了。(您可能知道,它是图灵完备的。)因此,它可以用作 PL。

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

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

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

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

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

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

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

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

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

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

闭包:历史

数学中的闭包与PL 中的闭包不同。

在后一种情况下,该术语由 PJ Landin 于 1964 年创造,以在评估“以 Church 的 λ 符号为模型”的 PL 的实施中提供一流功能的支持。

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

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

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

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

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

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

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

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

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

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

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

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

注意 AIM-199指出这本质上是关于环境的。

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

闭包:演变

Landin 的原始论文首先将环境定义为将名称(“常量”)映射到命名对象(“原始”)的数学函数。然后,它将环境指定为“由名称/值对组成的列表结构”。后者也在早期的 Lisp 实现中作为alist(关联列表)实现,但现代语言实现不一定遵循这样的细节。特别是,可以链接环境以支持嵌套闭包,这不太可能被 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 确实有“闭包类型”概念的特定语言定义)。管理闭包使用的资源的规程与闭包的资格无关。

因此,(虽然这里没有看到)Thomas Lord 在 LtU 的关于闭包的图跟踪必要性的主张在技术上也是不正确的。闭包可以解决 funarg 问题,因为它允许防止对激活记录(堆栈)的无效访问,但事实并没有神奇地断言对包含闭包的资源的每个操作都是有效的。这种机制依赖于外部执行环境。应该清楚的是,即使在传统实现中,隐式所有者(GC)也不闭包,所有者的存在是 SECD 机器的实现细节(因此它是用户的“高阶”细节之一)。这样的细节是否支持图追踪对闭包的限定没有影响。此外,AFAIK,语言结构letrec1966 年首次在 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 共享),我相信它不会在这个主题上引入重大的混淆和误解。

Named functions may have a slightly more problems. Functions may denote the entity of the name themselves (the "symbols"), as well as the evaluated values of these names. Given that the fact that most PLs don't have unevaluated context to differentiate a function with some other entities carrying interesting meaning (e.g. sizeof(a_plain_cxx_function) in C++ just ill-formed), users may not observe the differences of the misinterpretation between unevaluated operand and evaluated values. That will be problematic with some Lisp dialects having QUOTE. Even experienced PL specialists can easily miss something important; this is also why I emphasize to distinguish syntactic constructs with other entities.

于 2022-01-27T13:50:49.387 回答
0

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 和 Closure

于 2021-03-01T16:45:01.433 回答