2.3 一个带有过程的语言

什么是过程? 这个问题并不像看上去那么简单, 而且John McCarthy在设计Lisp时就掉进了坑里. 实际上, 在很长一段时间内, 动态作用域 (之后我们将实现动态作用域的语言) 一直被视为Lisp的本质特征之一, 而这招致了不少批评.

请看以下Scheme的读取求值输出循环交互.

这有什么可希奇的呢! 不过, 对于一个使用远古Lisp方言的程序员而言, 他很可能认为结果应该是12. 这是由于他会认为double的值是(lambda (x) (* x two)), 一个列表而已.

现代编程语言学家十分清楚这是完全错误的设计和看法, 但这对于当时的人们而言尚不明了. 实际上, 一个过程不仅是定义过程的代码本身, 还包括过程所在的环境. 这个环境确定了过程的自由变量的意义, 使其成为封闭形式. 因此, 人们将过程的代码 (形式参数和过程体) 和过程所在的环境合称为闭包 (closure).

如果一个语言的过程的自由变量的意义由创建过程的环境决定, 那么称这个语言采用了词法作用域. 「词法」应该理解为「可以从程序文本推断出来」, 这在动态的求值过程之前, 因而其也被称为静态作用域.

既然明白了什么是过程, 现在我们呈现一个带有过程的(词法作用域的)语言.

首先仍然是给出语言的句法.

这个句法相较于前一节的语言, <exp>新增了两个产生式, 它们的确也是对偶的. 一个用于创建过程, 另一个用于应用过程.

现在我们给出解释器, 同样只有对于新增的产生式的解释是有趣的.

我们将被解释的语言的闭包表示为Scheme的闭包, 这是一种过程性的表示. 如果我们采取数据抽象的观点, 将闭包视为抽象数据类型, 那么我们将闭包的创建和应用抽象为make-closure和apply-closure.

我们也可以更换闭包的具体表示, 例如以下的表示更加具体直接.

我们想说的是, 没有真正的魔法! 用闭包表示闭包似乎是一种魔法, 但其也可以表示为寻常的数据结构.

注记: 这里的apply-closure使用let*的确没有什么必要, 但鉴于let并不规定求值顺序, 为了获得确定的控制流 (其一个好处在于保证不同的实现产生相同的行为), 我们还是使用了let*. 以后我们也将常常这么做.

让我们以一个有趣的例子结束本节.

它展示了不使用letrec编写递归过程的方法. (显然, 其中隐藏着一个不动点组合子.)

你的回應