【闭包】你真的理解闭包和lambda表达式吗


作者:AItsuki
链接:https://www.jianshu.com/p/c22db2a91989

 

1. 前言

在阅读Think in java时,关于内部类的作用中出现了闭包这个词。于是开始百度,了解到了怎么使用代码定义一个闭包,闭包能实现什么妙用。而这种答案是不能让人满意的,因为这样的回答会让人感觉闭包是编程语言设计者设计出来的一个很蠢的东西。

例如这种类型的回答:

  • 闭包能实现私有数据,隐藏变量,减少全局变量
  • 闭包能实现计数器等类似对象的妙用

虽然回答没错,但总感觉回答不到点子上,我觉得没有人会为了上面这两个可有可无的功能而大费周章的设计出闭包这种东西,并且定义一个闭包还需要你通过嵌套函数去引用自由变量这种极其繁琐的方式才能完成?你确定会有这么蠢的编程语言设计者吗?

说那么多,只是因为查了很多英文资料,甚至找到1960年代的文章来看,有点郁闷。我很难用简练的语言将这么多资料组成一篇闭包发展史,因为我不懂编程语言设计,不懂LISP,我尝试了几天想写成一篇文章,但都是在中间卡住了,因为存在很多我不懂而只能臆测的东西。所以我现在分成两部分来讲:

  • 第一部分,是stackoverflow上的一个回答翻译,回答者的想法和我完全一模一样,它抛开历史问题,直接讲闭包的本质上的概念,而不是形式上的定义。
  • 第二部分,我会整理下我查过的资料,用历史资料来描述闭包的诞生,但可能不会太详细。

2. 【译】lambda表达式和闭包的区别

What is the difference between a 'closure' and a 'lambda'?

问题由SasQ回答,silvalli重新编辑

关于lambda和闭包有很多困惑,即使在这个stackoverflow的回答中也一样。
与其询问从某些编程语言上通过实践学习到闭包的程序员,或者从其他无知的程序员,不如自己去寻找源头(闭包开始出现的地方)。

lambda和闭包最早可以追溯到lambda演算,lambda演算是上世纪30年代由Alonzo Church创造的,而我们就从这里开始说起。

lambda演算可以说是一种最简单的编程语言,你只可以用它来做的唯一的事情是:

  • 应用:将一个表达式应用到另一个表达式,表示f x。(把它当作是函数调用,其中f是函数,x是它的唯一参数)
  • 抽象:它可以绑定一个符号,改符号可以看作是一个“插槽”、空白的框、或者说一个“变量”。它是用希腊字母λ(lambda)加上符号名称(例如x)跟着一个点,最后加上一个表达式组成。然后将表达式转换为期望一个参数的函数。
    例如:λx.x + 2表示包含一个x + 2的表达式,并且表示表达式中符号x是一个绑定变量(bound variable),它可以用你提供的值作为参数来替换。
    注意,这种方式定义的函数是匿名的,所以你还不能引用它,但是你可以立即调用它(看应用的定义),方式是提供它正在等待的参数,比如这样:(λx.x+ 2)7。然后,用表达式7(这种情况下是个字面值)被替换到x,作用在lambda子表达式x + 2上,所以你得到 7 + 2,最后你通过简单的算术规则得到9。

所以,我们解决了一个谜题:
lambda本质就是一个匿名函数,例如上面的λx.x + 2


在不同的编程语言中,函数抽象的语法可能不同,在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中,lambdas是这样定义的:

(lambda (x) (x + 2))

然后你可以立即将它应用于一个参数调用:

( (lambda (x) (+ x 2)) 7 )

好的,现在是时候解决另一个谜题了:什么是闭包。为了做到这一点,我们来谈谈lambda表达式中的符号(变量)。

正如我所说,lambda 抽象的的做法是在它的子表达式中绑定一个符号,以便它称为一个可替代的参数。这样的符号称为约束的(bound)。但是如果表达式中还有其他符号呢?例如λx.x/y+2。这这个表达式中,符号x是被lambda抽象λx约束的。但是另一个符号y不受限制,他是自由的。我们不知道它是什么以及它来自哪里,所以我们不知道它代表什么,有什么价值,因此我们不能评估(evaluate)这个表达式直到我们找出y代表的意义。

事实上,与其他两个符号2和+一样。知识我们对这两个符号非常熟悉,通常会忘记计算机并不知道它们,我们需要通过在某处定义它们来告诉计算机它们的含义。例如,在库或在语言本身。

你可以想象自由符号是定义在表达式外面的地方,在它“周围的语境”(“surrounding context”)中,这称为环境(environment)。环境可能是一个更大的表达式,这是表达式的一部分。或者是在某个库,或者在语言本身(作为原生的)。

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

  • CLOSED expressions:这些表达式中出现的每一个符号都受到一些lambda抽象的约束。换句话说,它们是自己自足的,不需要评估任何周边语境。它们也被称为Combinators。
  • OPEN expressions:这些表达式中的某些符号没有约束,也就是说,它们中的一些符号是自由的,它们需要一些外部信息,因此只有在提供这些符号的定义后才能对它们进行评估。

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

然后这里是闭包的部分:
lambda表达式的闭包是定义在外部上下文(环境)中特定的符号集,它们给这个表达式中的自由符号赋值。它将一个开放的、仍然包含一些未定义的符号lambda表达式变为一个关闭的lambda表达式,使这个lambda表达式不再具有任何自由符号。

例如:你有一下这个lambda表达式:λx.x/y+2,符号x是受约束的,而y是自由的,因此这个表达式是开放的并且是不能被评估的,除非你说出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表达式,这就是closure这个术语一开始出现的地方。还有这就是为什么关于这个问题很多人的回答都不是很正确的原因。


那么,他们为什么是错误的?为什么很多人说闭包是内存中的一些数据结构,或者是他们使用的语言的一些功能?或者为什么他们将闭包与lambdas混淆?

那么,Sun / Oracle,微软,Google等企业( corporate marketoids)就应该收到指责,因为他们说这些是他们语言的结构(Java,C#,GO 等)。他们经常叫“闭包”,这应该只是lambda表达式(译者:没错,什么ruby,groovy就让我以为lambda表达式就是闭包,真的好气)。或者称“闭包”是他们用来实现词法作用域的一种特定技术,即一个函数可以访问它作用域外定义的变量。他们经常说函数“封闭(encloses)”这些变量,即捕获他们到某些数据结构去存储它们,防止它们在外部函数执行完后被销毁。但是这些只是他们民俗语言和营销,这只会让事情更加混乱,因为每一个语言供应商都使用它们自己的术语。

而且更糟糕的是,因为它们所说的话总是有一点是真实的,这让你不允许轻易的将其视为假的。让我解释一下:

如果你想实现一种使用lambdas作为一等公民(first-class citizens)的语言,你需要允许它们使用在其上下文中定义的符号(即在你的lambda中使用的那些自由变量)。即使周围的函数已经返回,这些符号也必须存在。问题是这些符号是绑定在函数的本地存储(通常是在调用栈上),当函数返回时将不再存在。因此,为了让lambda按照你期望的方式工作,你需要以某种方式从外部上下文中“捕获”所有这些自由变量并且保存它们,即使外部上下文将消失。也就是说,你需要找到该lambda表达式的闭包(所有使用到的白雾变量)并且将它存储在其他地方(建立一个副本,或者提前准备一个除了栈以外的空间给它们)。你用来实现这个目标的实际方法就是你语言的“实现细节”。这里最重要的时闭包,它是一组从环境中获取的自由变量,你需要在保存在某处。

人们开始调用他们语言中使用的实际的数据结构来实现“闭包”并没有花太长时间。该结构通常看起来是这样的:

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

并且这些数据结构可以作为参数传递给其他函数、作为函数返回、保存到变量、代表lambdas,并允许通们它们访问其封闭环境以及在该上下文中运行的机器码(machine code)。但这只是实现闭包的其中一种方式,而不是闭包本身。(译者:例如groovy就用一个Closure类来实现闭包)

正如我上面所解释的那样,lambda表达式的闭包是其环境中定义的一个子集,它给包含在lambda表达式中的自由变量赋值,从而有效的关闭表达式。(将一个开放的还不能评估的lambda表达式,转换成一个关闭的lambda表达式,然后可以进行评估,因为它包含的所有符号现在都已定义)。

其他任何东西都只是程序员和语言供应商们的“巫术”和“诅咒魔术”,并不知道这些概念的真正根源。(译者:原文"cargo cult" and "voo-doo magic",科学家为了描述那些缺乏科学严谨的研究也创造了这些词汇:巫毒科学(voodoo science)和巫术科(cargocult science))


 

  • 3
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值