“λ 演算是函数式语言与操作语义的理论原型。纯演算以无捕获替换解释应用,带环境的解释器则把函数表示为代码与定义环境组成的闭包;两种机制都必须保持同一词法绑定。”
形式陈述 ​
在词法作用域语言中,函数表达式
其中代码部分给出参数
语义上只需保存
直觉 ​
函数代码写在某个位置时,会引用参数之外的名字。若函数立即执行,当前环境足以解释这些名字;一旦函数被返回、存入数据结构或传给别处,定义位置的局部作用域可能已经退出。闭包就是让代码随身携带那一小块词法上下文,使函数值真正能够脱离出生地继续运行。
“捕获”并不必然意味着复制变量当时的数值。不可变绑定看起来像保存值,可变绑定通常保存位置或引用,于是多个闭包能看到同一单元的后续更新。闭包保存的是解析自由变量所需的绑定关系,具体被保存对象取决于语言的环境与存储模型。
例子与边界 ​
考虑:
let x = 1 in
let f = (fun y -> x + y) in
let x = 100 in
f 2
定义 f 时,闭包保存了外层 x 绑定到 f 2 时,在该环境上加入 y 绑定到 x = 100 不会改变函数体中自由变量 x 的指向。若语言采用动态作用域,函数会沿调用链找到
若 x 是一个可变引用,闭包通常捕获该引用而非创建私有副本;定义后通过别名把单元改为
本页的 closure 是运行时函数值,不是“没有自由变量的 closed term”,也不是拓扑空间中的闭包或某类运算下的封闭性。纯 λ 演算常通过无捕获替换定义归约,无须显式环境;抽象机器与解释器引入闭包,是把词法绑定实现为可执行状态的一种方式。
推论与应用 ​
高阶函数、回调、迭代器、异步任务和模块私有状态都依赖闭包把代码与上下文一并传递。编译器的 closure conversion 会把自由变量组织成显式环境记录,并把函数改写为同时接收环境与普通参数的代码指针;这一步把源语言的词法作用域降到更低级的调用约定。
闭包与变量绑定共同解释为什么 α-改名不应改变程序行为:只要绑定关系保持,表面名称不是运行时含义。带可变状态时,环境映到位置、store 映到值的分层还能准确描述多个闭包共享状态,以及对象方法和消息处理器如何保持私有数据。
参考资料
- Michael R. Clarkson et al., OCaml Programming: Correct + Efficient + Beautiful, Cornell CS 3110 textbook,lexical scope, environments, and closures。
- Benjamin C. Pierce, Types and Programming Languages, MIT Press, 2002,Chs. 5 and 7,lambda-calculus evaluation and implementation perspectives。