Archive for 2013年9月

Closure 与 Unbound 变量

2013/09/08

大凡了解或编写过简单 Lisp 解释器的人都知道 unbound 变量这个概念。如下面这段代码 [1],对其中用 lambda 创建的匿名函数来说,balance 就是一个 unbound 变量。(对于 make-withdraw 函数来说,balance 是 bound 变量。)

Unbound 变量的求值通过 Environment Model 来定义 [2]。程序运行时的 environment 由 frame 构成。每当一个函数被调用执行时,解释器会创建新 frame,称为当前代码的 execution frame [3];定义新函数时当前 execution frame 作为被定义函数的 associated frame,当前被执行函数的 associated frame 作为被定义函数的 associated frame 的 enclosing frame。所有的 bound 变量(局部变量)都在当前 execution frame 中定义。在当前 execution frame 中未定义的变量的取值要到 enclosing frame 中去查找,被称为 unbound 变量。

Environment Model 是一个很容易理解的简单概念模型,但直接将其对应到具体实现中会导致一些问题。所以基本上只有用于教学练习目的的解释器才会直接采用这种模型。这样的一个结果是,很多从未了解过解释器理论仅是使用动态语言编程的人并不了解这个概念模型,只知道和具体实现比较接近的术语 —— closure 和 lexical scope。但是,因为比较接近底层实现,closure 和 lexical scope 概念体现的实现方面的意义掩盖了其语意本质,成了高级动态语言中很难理解的部分。理解这两个术语需要了解它们对 Enviornment Model 做出了什么取舍。

在原本的 Enviornment Model 中,unbound 变量的求值要根据运行时的 enclosing frame 一路向上查找,性能开销很大。从名称上可以看出,lexical scope 则是一个静态机制,源代码的静态结构 (lexical scope) 完全决定了 unbound 变量的取值来源 [4]。Lexical scope 消除运行开销,代价是语言能力稍许弱化。从概念本身来说,定义一个函数并不等同于定义一个 nested 函数 [5]。完全可以把被定义函数本身的代码和该函数「被定义」的位置分离开,让 lexical-scope 和函数的定义不再有对应关系。一种典型的方法是 load 另一个源文件(或者一个表示代码的字符串),比如 Lisp 的 eval 和 Lua 的 load。目前主流动态语言中的 lexical scope 则完全是静态机制,只支持同一源文件中的 nested 函数定义。如 Lua 的 load 之类的动态机制不支持 unbound 变量。所以,严格地说,大多数动态语言的 closure 并不是和 Enviornment Model 一模一样的。

另一个有趣的问题是 lexical scope 为什么又被称为 closure。直接采用 Environment Model 的简单解释器大多由高级动态语言编写,所以可以直接利用 hosting 语言的高级特性。比如前面提到过的(脚注 [3])execution frame 借助 hosting 语言的 runtime stack 来实现被解释语言的 call stack,而且依靠 hosting 语言的内存管理机制来管理 frame 的生命周期。当一个 frame 不再是当前的 execution frame 时,它的生命周期完全取决于是否还有以其作为 associated frame 的函数存在。这种判断往往直接利用 hosting 语言的 GC 完成。这种 hosting 语言本身高度抽象化的实现中是不必有字面上和「closure」相关的概念的。

工业级别的解释器一般用没有 GC 的静态语言实现,显式的实现被解释执行代码的 call stack,函数没有直接对应的 associated frame,仅在逻辑上将 upval [6] 对应到 call stack 中的变量。如果 nested 函数可以作为返回值从定义它的 nesting 函数中传出,就会出现 upval 的生命周期长于对应的 on-stack 变量的情况。所以,当一个函数返回时 —— 也就是当前的 call stack frame 被销毁时,若其中有 on-stack 变量对应到 nested 函数的 upval,它们(或者它们的 references)必须被拷贝到解释器中维护 nested 函数本身的数据结构中。这种操作称为 upval 的「closing」,也就是「closure」名称的由来。因为一般来说只有可以作为函数返回值的 first-class function 才会需要 closing 操作,所以 closure 对应的全称是 first-class function with lexical scope。

脚注:

  1. 摘自《Structure and Interpretation of Computer Program》(SICP)。
  2. Environment Model 的概念同样来自 SICP,见第 3.2 章。
  3. Execution frame 实际构成了程序运行的 call stack。但是在基本的 Environment Model 中,这些 frame 之间没有显式联系,真正的 call stack 由解释器维护。直接采用 Environment Model 的解释器一般比较简单,近似或者就是一个递归求值器 (recursive evaluator),所以 call stack 由解释器本身的 runtime stack 来维护。
  4. 大多数高级动态语言都编译为 byte code 运行。所以,类似静态语言,可以说这是「compile-time」机制。
  5. 全局函数也是「global scope」中的 nested 函数。
  6. 由于 lexical scope 和 Environment Model 意义并不完全相同,所以这里改用 Lua 的术语「up value (upval) 」而不再使用 Environment Model 的术语「unbound 变量」。