闭包漫谈(从抽象代数及函数式编程角度)

简介:

如果一个函数返回另一个函数,而被返回函数又需要外层函数的变量时,不会立即释放这个变量,而是允许被返回的函数引用这些变量。支持这种机制的语言称为支持闭包机制,而这个内部函数连同其自由变量就形成了一个闭包。

前言

如果Google一下“闭包”这个词,会发现网上关于闭包的文章已经不计其数,甚至很多人将闭包看做面试JavaScript程序员的必考题(虽然闭包和JavaScript没有什么必然联系)。既然如此,我为什么还要写一篇关于闭包的文章呢?

首先,虽然网上关于闭包的文章甚多,但是很少以较为形式化的角度阐述闭包,而我认为理解闭包的关键之一就是从形式化角度理解其涵义;其次,大多数文章将闭包的概念与JavaScript语言绑定太死,这样容易局限对闭包概念的理解,而难以窥探到其本质。从JavaScript去理解闭包,个人认为这是本末倒置的,应该先理解纯粹意义上的闭包,然后再理解JavaScript中的闭包机制。

基于以上情况,本文将从较为形式化的角度阐述闭包概念,当做对现有网络的资料的一个补充。

一个需要明确的重要事实

在开始阐述闭包之前,我需要特别明确一个非常重要的事实,那就是“闭包(closure)”一词被用于定义两个毫不相关的概念,分别是数学领域抽象代数分支下用于描述集合之于运算的一种性质以及计算机科学程序设计语言分支用于描述函数式语言所支持的一种机制。这种不同大约就如“电影《人猿泰山》”和“五岳之尊泰山”中的“泰山”差不多,两个短语中的“泰山”描述的是两个风马牛不相及的概念,虽然是同一个词。

一般来说,作为程序员我们说的闭包更多是指后者,但是如果你与我一样同时具有一点数学背景,第一次接触“闭包”一词是在抽象数学中的话,那么当刚接触到计算机中的“闭包”时多少会产生困惑,同样,如果你是一个纯粹的程序员,那么当在数学著作中读到“闭包”一词时请小心区分这个“闭包”具体是表述哪一个概念。

我会在下文中分别阐述数学领域和计算机科学领域闭包的概念。

抽象代数中的闭包

抽象代数是一门研究代数结构的数学分支,它的研究对象包括群、环、域和向量空间等等。当然我在这里丝毫没有要大谈特谈这些令人头大的概念的意思,我会尽量以一种易懂的半形式化方式去阐述一些概念。

集合的定义

非正式地,集合是N个对象组成的一个无序、互异且确定的整体。其中N是自然数。这些对象称为集合的元素。

无序性是指集合中的元素不存在序关系(集合上可以定义序关系,但这些序关系不是集合本身的一部分),每个元素的地位是相同的。

互异性是指集合中任意两个元素是不同的,即同一集合中任意两个元素间不存在等价关系。

确定是指对任意一个对象和任意一个集合,这个对象要么属于此集合,要么不属于此集合,二者必居其一,不存在模棱两可的状态(模糊数学中有一种中间隶属状态,本文不考虑模糊数学领域)。

运算的定义

非正式地,集合上的n元运算是一个映射,这个映射将作用于任意n个集合中的元素,并映射为一个结果(注意结果不一定属于这个集合)。

例如,设是正整数集合,那么加法()和减法()都可以看做定义在上的二元运算,因为任意两个正整数都可以进行加减。

封闭的定义

有了集合和运算的概念,就可以定义封闭的概念了。

非正式地,如果定义于集合上的运算的运算结果仍然属于,那么运算对于集合是封闭的。下面给出“封闭”的一个半形式化定义:

如果对于任意,有,那么说二元运算对于集合A是封闭的。

例如“”对于是封闭的,因为任意两个正整数的和结果仍然是正整数;但是“”对于不是封闭的,例如3和5属于,但是:

闭包性质

一个集合满足闭包性质当且仅当这个集合在某个运算或某些运算的搜集下是封闭的,其中“某些运算的搜集下封闭”是指这个集合单独闭合在每个运算之下。

值得一提的是,之前这条定义往往被作为一条公理引入一个代数结构,叫做“闭包公理”。但是现代集合论往往将运算形式化的定义为集合间的运算,所以将其作为公理引入代数结构是多余的(因为可以通过其它公理间接定义闭包公理),但是对于子集是否闭合的问题,闭包公理仍然有意义。

一个例子 – 存在于形式语言与自动机理论中的闭包

上面说了很多东西,我们来看一个抽象代数领域闭包的例子。

我们回想在形式语言与自动机理论中(或者编译原理中也有提到)在定义语言时做的一些推导。

一般我们会先定义一个有限集合,叫做字母表,的n阶幂运算定义为形成一个新的集合,一个对象属于当且仅当它是中任意n个字母连接成的串,其中。而

此时集合满足闭包性质,因为这个集合对于幂运算是封闭的。有兴趣的读者可以自行证明一下。

函数式编程中的闭包

在这一章节开始之前,我需要再和大家明确一个比较纠结的事实,就是在函数式编程领域中当说到“闭包”时,也有可能是指数学领域中闭包的概念,这是因为函数式编程在基础理论与抽象代数有一定亲缘性,所以当在函数式语言著作中讨论“闭包”时,有可能是在抽象数学的上下文中讨论的。然而,在表述上可能会有微妙变化。在函数式语言领域对于数学闭包常用的表述是“如果一个运算的结果仍然能被此运算作用,则这个运算是封闭的”,要注意这只不过是上文提到的“闭包”概念的另一种等价表述而已,如果我们将这个运算的所有结果看做一个集合,那么就可以等价表述说这个运算在这个集合上是封闭的。

而我下面所要阐述的闭包是一种截然不同的概念。所以,当在函数式语言的著作中看到“闭包”时,需要根据上下文环境小心区分其表述哪种概念。

Lambda演算与自由变量

函数式编程语言的基础是lambda演算,这是一套用于研究函数定义、应用和递归的形式系统,由数学家丘奇在20世纪30年代引入。如果您不太熟悉lambda演算,那么维基百科相关页面是很好的快速入门资料,请原谅我不会完整描述lambda演算(因为已经有很多可以参考的资料)。

简单来说lambda演算将计算过程看过一系列的函数代换,例如,下面是加运算的lambda函数(假设+运算已经定义):

lambda演算就是反复将函数应用于实际值,并用实际值代替参数,最终得出结果。例如下面是7+2的计算过程:

首先用第一个参数(7)代替最外层函数的参数(x),然后用第二个参数(2)代替第二层函数的参数(y),最终得到计算结果。

鉴于如果下面大量使用lambda演算描述问题大家可能会崩溃(我也会崩溃),我将改用函数式语言schemeLisp的一个方言)来进行问题描述。注意其实scheme在本质上与lambda演算是等价的,只是看起来更好懂,例如不需要遵循lambda演算一个变态的规定:每个函数只允许有一个参数(虽然任何多参数函数式程序都可以通过Currying过程化归为等价的lambda演算)。

下面是用scheme程序对上述lambda演算的等价表示:

(define (f x y) (+ x y))

可以这样计算7+2:

(f 7 2)
;Value: 9

下面看一个稍微复杂点的例子:

(define (f x) (lambda (y) (+ x y)))

这里定义了函数f,接受一个参数x,特别要注意它的返回值:不是一个值而是一个匿名函数。如果我们把这个函数单独拿出来:

(lambda (y) (+ x y))

可以看到,这个匿名函数接收一个参数y,但是却没有参数x!也就是说,如果脱离上下文执行这个函数,则x处于未指定状态,我们说对于这个函数,y是绑定的,而x是自由的。

一般地:x是一个函数的函数体中的变量,如果x被这个函数的参数指定,则x是绑定于这个函数的,否则说x对于此函数是自由的。

下面可以看到,变量的绑定和自由概念是理解闭包本质的一把钥匙。

程序语言的闭包性质

继续上面的scheme程序,我们已经定义了函数f:

(define (f x) (lambda (y) (+ x y)))

如果我们运行下面程序:

(f 7)
;Value 13: #[compound-procedure 13]

可以看到,f返回了一个过程(匿名函数),按照函数演算规则,这个函数应该是:

(lambda (y) (+ 7 y))

那么下面的运算就很直观了:

((f 7) 2)
;Value: 9

注意这里有一个非常重要的地方(也是闭包性质的关键),那就是这个运算执行了两个函数:f和匿名函数。而f的作用域为(f 7),这就是说,其实在(f 7)之后,f这个函数就结束了,而x(这里被赋值为7)是f的私有变量(绑定于f),那么程序设计语言的设计者就有两种选择:第一,在函数超出其作用域后立即销毁其绑定变量,如果是这样的话,((f 7) 2) 是无法得出结果的,因为在外层的f运算结束后,存放数值“7”的变量就被释放了,所以匿名函数无法得到其自由变量x的值;显然scheme的设计者做了第二种选择:如果一个函数返回另一个函数,而被返回函数又需要外层函数的变量时,不会立即释放这个变量,而是允许被返回的函数引用这些变量。支持这种机制的语言称为支持闭包机制,而这个内部函数连同其自由变量就形成了一个闭包。

上面的这段话不太好理解,但是请务必多读几遍,因为,这就是闭包的全部。

从Scheme到JavaScript

好的,现在开始讨论JavaScript中的闭包。

上文说过,闭包是函数式语言才有的机制,或者说支持函数式编程泛型的语言才有的性质。一个语言支持函数式编程泛型,如果它同时具有下列特性:

可以将一个函数赋值给一个变量。

函数可以作为参数传递给另一个函数。

函数的返回值可以是一个函数。

结合上面关于闭包性质的定义,就很清楚为什么只有支持函数编程泛型的语言才可以谈闭包性质。

很显然,JavaScript是具有上述三条性质的,所以可以说JavaScript拥有函数式编程泛型。当然,一般我们还是习惯于用命令式的写JavaScript,但是其闭包本质是一样的。为了说明这一点,我将会首先用JavaScript按照函数式泛型重写上面的scheme程序,然后转为命令式。

上文用scheme所写的函数f,可以用JavaScript等价实现如下:

1
function  f(x){ return  function (y) { return  x + y; }; }

可以执行与上述scheme类似的计算(因为脱离了浏览器,我是用nodejs执行这段JavaScript):

1
2
console.log( (f(7)) (2) );
//9

其中f返回的匿名函数与其自由变量x组成了一个闭包系统。

如果用命令式重写上面的程序,就得到了我们熟悉的闭包:

1
2
3
4
5
6
7
8
function  f(x){
     return  function (y) {
         return  x + y;
     };
}
 
var  lam = f(7);
console.log(lam(2));

总结

本文分别讨论了抽象代数和函数式编程中两个截然不同的闭包概念,当然,作为程序员我们更关心的是后者(但前者也不是对程序员一点用也没有,如果学习函数式语言的构造原理,抽象代数中的闭包也是必须理解的概念)。实际上,闭包远没有很多人想象的复杂和神秘,只不过是函数式编程中一个普通的概念,但是可能因为我们大多数人是从命令式编程语言学习编程,很少去写函数式程序,而要理解闭包却是需要结合函数式编程泛型,因此很难看清闭包的本质。希望本文能对您有所帮助,同时我个人也建议可以学习一门函数式语言,这样对很多概念的理解非常有好处。

原贴地址:http://www.codinglabs.org/html/closure-perspective-of-abstract-mathematic-and-functional-language.html

目录
相关文章
|
6月前
|
存储 JavaScript 前端开发
技术经验解读:什么是闭包?闭包的优缺点?
技术经验解读:什么是闭包?闭包的优缺点?
54 1
|
7月前
|
存储 缓存 自然语言处理
高阶函数离不开闭包
高阶函数离不开闭包
|
存储 关系型数据库 Python
函数式编程:一等对象、作用域和高阶函数的综合指南
函数式编程:一等对象、作用域和高阶函数的综合指南
52 0
|
Web App开发 JavaScript 前端开发
应该如何理解闭包
应该如何理解闭包
应该如何理解闭包
|
Java API
函数式编程概念和应用
函数式编程的核心要素:传入参数,执行逻辑,返回值,也可以没有返回值。函数式的编程风格侧重描述程序的执行逻辑,不是执行过程。
227 0
函数式编程概念和应用
|
Scala 开发者
偏函数的必要性 | 学习笔记
快速学习偏函数的必要性
|
开发者 Python
闭包的概念 | 学习笔记
快速学习闭包的概念
|
自然语言处理 JavaScript 前端开发
【闭包概念】关于闭包概念不同解读——你可以自己理解。
写在前面: 闭包是被讲烂的内容,但是当我不了解的情况下,看过很多教程,听过很多道理,还是无法完全理解闭包这个东西。所以想要写一篇比较详细,前端小白也能够真正理解闭包概念的干货文章,本文参考很多闭包资料,希望能真正把闭包这个东西讲清楚,喜欢的朋友可以点个赞,或者点波关注。 首先需要了解一波js变量的作用域以及变量的生存周期。 前几天我发在掘金上面了,下面是一个内容概要,墙裂推荐所有小伙伴,还是点进去学习一波,作用域这个问题可以说是闭包最为重要的内容。 里面的内容包括: 1.变量的作用域(变量的有效范围)。2.变量作用域,js高级程序设计(红宝书)中的解释。3.红宝书中的作用域链栗子(还有一个例子
196 0
【闭包概念】关于闭包概念不同解读——你可以自己理解。
|
JavaScript 前端开发 索引
【重温基础】21.高阶函数
【重温基础】21.高阶函数
136 0
|
自然语言处理 JavaScript 前端开发
【重温基础】19.闭包
【重温基础】19.闭包
152 0