王垠在一篇叫做 Lisp 已死,Lisp 万岁!的文章里介绍了「上古时期」Lisp的动态作用域,并用Emacs Lisp做了示范。我想这一定会让很多程序员感到新鲜(或困惑),因为目前被使用的大部分编程语言要么使用词法作用域,要么干脆不允许函数中有自由变量。
要了解自由变量,我们首先需要知道哪些变量是「不自由」的。在一个函数里面,我们使用一个变量之前,它应该是已经被绑定了的,可能是来自函数内的局部变量、函数的参数变量,以及全局变量。向C这样的语言,只允许存在局部变量、参数变量和全局变量。一些函数式语言允许你在任意位置生成函数,也就必须允许你在更多的位置绑定变量。
假设你已经跟着那篇文章在Emacs中体验过了动态作用域,现在,请试着在Common Lisp试一下类似的代码:
;;; 定义一个函数f,函数定义的时候外面一层存在一个变量x,这个变量在函数f中被使用了
(let ((x 1))(defun f (y)(* x y)));;; 正常调用,结果符合我们的直觉
(f 2) ; 2;;; style-warning: The variable x is defined but never used
(let ((x 2))(f 2)) ; 2 and style-warning
第三块代码报了一个style-warning: The variable x is defined but never used,这里的x没有被函数f使用。这在99.44%(这个数字是我瞎掰的)的情况下都是合理的:我们看到函数f,它接受一个参数y,我们不需要也不应该知道函数内部到底使用了哪些其它变量。
但是,如果我们不甘心函数只是一个黑盒子,想要在调用一个函数前改变函数体内的某个变量的值呢?如果可以这样,那么一些特定的任务将会变得非常容易和自然。通过使用动态作用域的变量,IO重定向可以非常简单,自定义异常处理的handler也可以非常简单。
在Common Lisp中,默认使用词法作用域,动态作用域的变量叫做「特殊变量」,让我们对刚才的示例代码稍作修改:
(let ((x 1))(defun f (y)(declare (special x))(* x y)))
函数f里面的x被声明为「特殊变量」,即使用动态作用域。函数f定义的地方有一个同名的变量x,但是函数f却「不认它」,而是在被调用的时候动态查找x的值。
(f 2) ; error: x not defined(defvar x 2)
(f 2) ; 4
Common Lisp中「特殊变量」的价值就在于值在运行时才确定,这一任务最好交给函数,而不是变量。
Common Lisp的姐妹Scheme,在最新的标准R7RS中引入了类似的特性,但是没有采用「特殊变量」,而是使用普通的函数。因为函数的本质就是动态的。Scheme保持了语言的简单性,同时不破坏程序员大脑中的模型,只是给编译器作者增加了难度。因为函数调用的开销问题,所有用来实现这种动态特性的函数必须被内联优化。
在Scheme中,这种东西被叫做parameter,由make-parameter创建。make-parameter接受1个或2个参数。第一个参数是parameter的初始值,第二个(可选的)参数是一个函数,用来约束parameter的值。make-parameter返回一个函数,这个函数接受0个或1个参数。如果不带参数调用它则返回parameter的值,带一个参数调用,则将它设为parameter的值。
还可以在做某件事情的时候使用parameterize临时改变parameter的值,做完之后自动恢复原样。
make-parameter和parameterize可以被定义成这样:
;; Taken from:
;; https://cisco.github.io/ChezScheme/csug9.5/system.html#./system:h13(define make-parameter(case-lambda[(init guard)(let ([v (guard init)])(case-lambda[() v][(u) (set! v (guard u))]))][(init)(make-parameter init (lambda (x) x))]))(define-syntax parameterize(lambda (x)(syntax-case x ()[(_ () b1 b2 ...) #'(begin b1 b2 ...)][(_ ((x e) ...) b1 b2 ...)(with-syntax ([(p ...) (generate-temporaries #'(x ...))][(y ...) (generate-temporaries #'(x ...))])#'(let ([p x] ... [y e] ...)(let ([swap (lambda ()(let ([t (p)]) (p y) (set! y t))...)])(dynamic-wind swap (lambda () b1 b2 ...) swap))))])))
(实际的编译器中为了让parameter总是被内联优化,应该不是这样写的。)
一些被证明了是坏想法的东西,如果能找到一种可靠的方法让它们变成可控的,也许能让某些特定的任务变得非常简单和自然。更重要的是,很多东西都是可以被重新审视的,作为程序员,不应该「谈动态作用域色变」,「谈白盒色变」,「谈goto色变」 ;-)