按语:我在送孩子去幼儿园的路上为「不懂编程的人」写了这一系列文章的第九篇,整理于此。它的前一篇是《无名》,讲述了如何一步一步「推演」出 Y 组合子。
我有个长长的望远镜,能一直伸到你的家里面,你说什么做什么,我都能看到。
怎样用 Emacs Lisp 语言描述这样的事?
(defun bar () x)
(defun foo (x) (bar))
(foo '三原色)
对 (foo '三原色)
进行求值,会在 Emacs 微缓冲区显现 三原色
。
foo
函数会对 bar
函数进行求值。bar
函数不接受任何参数的函数,但是它的内部却凭空出现了一个变量 x
。令 bar
毛骨悚然的是,当 foo
对它求值时,这个 x
竟然有意义的,它的值是符号原子 三原色
。
在 foo
的内部,bar
函数觉得自己见了鬼。
这其实是 Emacs Lisp 的动态域(Dynamic domain)在搞鬼。Emacs Lisp 解释器对 (foo '三原色)
求值,得到表达式 (bar)
,然后它继续对 (bar)
求值,得到表达式 x
,最后它继续对 x
求值,结果发现这个 x
是个长长的望远镜,从 foo
的窗户一直伸到了 bar
的家里,这个望远镜的品牌叫 三原色
。因此,我们就在微缓冲区看到了匪夷所思的结果。
当 Emacs Lisp 对一个函数表达式求值时,遇到自由变量时,它就会到一个全局的环境中搜索这个自由变量的值,将这个值作为自由变量的求值结果,倘若找不到,就会报错,说变量无效。
动态域的这种特性,对于需要长长的望远镜的机构很有用。不过,对于 bar
函数而言,既然 foo
能把长长的望远镜伸过来,就不要放过它:
(defun bar () (setq x '黑暗))
(defun foo (x) (progn (bar) x))
(foo '三原色)
再次对 (foo '三原色)
进行求值,这次结果为 黑暗
。因为 bar
抓住了这个伸到自己家里的望远镜,把它的镜头涂黑了。
动态域,是很古老的变量作用域模型。现代的变量作用域叫词法域,也叫静态域。
在词法域里,每个函数都有自己的环境。当函数中出现自由变量时,它就在自己的环境里搜索变量的值,搜到了就作为自由变量的求值结果,否则就报错——变量无效。
词法域的好处是,没有人能够将长长的望远镜伸到你家里。看下面的例子:
(setq lexical-binding t)
(defun bar () x)
(defun foo (x) (bar))
(foo '三原色)
再对 (foo '三原色)
求值,就会在微缓冲区报错,说变量 x
无效。这是因为,在 bar
的环境中,x
是未定义的自由变量,所以无效。虽然 foo
中的 x
是有定义的,但它仅仅是与 bar
中的 x
同名而已,它们是两个不同的变量。
下面的代码是一个匿名函数的求值表达式:
(funcall (funcall (lambda (thing)
(lambda (n)
(if (= n 0)
0
(+ n (funcall (funcall thing thing) (- n 1))))))
(lambda (thing)
(lambda (n)
(if (= n 0)
0
(+ n (funcall (funcall thing thing) (- n 1))))))) 100)
在动态域里,这个函数表达式无法求值,因为当 Emacs Lisp 解释器在对这个表达式进行求值时,最终抵达 (funcall thing thing)
的时候,全局环境里面已经没有了 thing
的定义。因为,当一个函数的求值结果是匿名函数时,在这个匿名函数被求值时,全局环境已经不再是它还在母体时的那个样子了。
例如,Emacs Lisp 解释器对
(funcall (lambda (thing)
(lambda (n)
(if (= n 0)
0
(+ n (funcall (funcall thing thing) (- n 1))))))
(lambda (thing)
(lambda (n)
(if (= n 0)
0
(+ n (funcall (funcall thing thing) (- n 1)))))))
的求值结果是
(lambda (n)
(if (= n 0)
0
(+ n (funcall (funcall thing thing) (- n 1))))))
这时,这个匿名函数里的 thing
,在这个匿名函数的母体中是有定义的,它就是作为参数传入的那个匿名函数,但是 Emacs Lisp 对母体求值结束后,thing
的定义也就同时在全局环境中消失了,因此对于这个刚刚从母体中脱胎而出的匿名函数,thing
变成了一个未定义的自由变量,从而导致 Emacs Lisp 解释器报错。
简而言之,在动态域中,你没有办法将匿名函数作为参数传给自身。因此,在动态域中,你看不到世界的本原,这样的世界是一个不确定的世界。这可能就是为什么早期的 Lisp 机器在运行时经常出故障的主要原因。
在词法域里不会有这样的问题,因为每个函数都有自己的环境,并且一个匿名函数从母体脱胎而出的时候,它会对母体的环境有所继承。这种结构称为闭包。
在使用 Emacs Lisp 编程时,用动态域还是词法域呢?倘若你没有长长的望远镜,或者你对这种望远镜深恶痛绝,就用词法域吧,在程序的开头添加:
(setq lexical-binding t)
或
;;; -*- lexical-binding: t -*-
最后,略微介绍一下 setq
。之前,我们只见识过通过函数参数传递的变量。setq
可以将一个符号与一个值或一个匿名函数绑定起来。上面已经见识了它将 lexical-binding
与 t
绑定了起来。下面的例子展示了如何将符号与匿名函数绑定起来:
(setq Y
(lambda (F)
(funcall (lambda (thing)
(funcall F
(lambda (m) (funcall (funcall thing thing) m))))
(lambda (thing)
(funcall F
(lambda (m) (funcall (funcall thing thing) m)))))))
(setq F
(lambda (thing*)
(lambda (n)
(if (= n 0)
0
(+ n (funcall thing* (- n 1)))))))
这样绑定之后,用 Y 组合子构造匿名的递归函数会更加简洁:
(funcall (funcall Y F) 100)
要试验上述代码,记得开启词法域模式。
下一篇:从混乱到有序