Skip to content

程序语言闭包

Programming-language closure · Function closure · Lexical closure

将函数代码与其定义位置的词法环境配对而成的运行时函数值。

形式陈述

在词法作用域语言中,函数表达式 λx.e 求值为闭包

λx.e,ρdef,

其中代码部分给出参数 x 与函数体 eρdef 是函数定义时的环境。将该闭包应用于实参值 v 时,在扩展环境 ρdef[xv] 中求值 e,而不是改用调用点的环境。相应的大步规则可写成

ρe1λx.e,ρcρe2v2ρc[xv2]evρe1 e2v.

语义上只需保存 e 的自由变量可达的绑定;保存整个环境是正确但可能多占空间的实现,编译器可通过自由变量分析进行环境裁剪。

直觉

函数代码写在某个位置时,会引用参数之外的名字。若函数立即执行,当前环境足以解释这些名字;一旦函数被返回、存入数据结构或传给别处,定义位置的局部作用域可能已经退出。闭包就是让代码随身携带那一小块词法上下文,使函数值真正能够脱离出生地继续运行。

“捕获”并不必然意味着复制变量当时的数值。不可变绑定看起来像保存值,可变绑定通常保存位置或引用,于是多个闭包能看到同一单元的后续更新。闭包保存的是解析自由变量所需的绑定关系,具体被保存对象取决于语言的环境与存储模型。

例子与边界

考虑:

let x = 1 in
let f = (fun y -> x + y) in
let x = 100 in
f 2

定义 f 时,闭包保存了外层 x 绑定到 1 的环境。调用 f 2 时,在该环境上加入 y 绑定到 2,结果是 3;调用点后来出现的 x = 100 不会改变函数体中自由变量 x 的指向。若语言采用动态作用域,函数会沿调用链找到 100,结果变为 102,这正好标出两种作用域规则的分界。

x 是一个可变引用,闭包通常捕获该引用而非创建私有副本;定义后通过别名把单元改为 100,再次调用便会观察新值。另一方面,函数体没有自由变量时仍可用统一的闭包表示,只是保存的环境为空。

本页的 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。