长长的望远镜

按语:我在送孩子去幼儿园的路上为「不懂编程的人」写了这一系列文章的第九篇,整理于此。它的前一篇是《无名》,讲述了如何一步一步「推演」出 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-bindingt 绑定了起来。下面的例子展示了如何将符号与匿名函数绑定起来:

(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)

要试验上述代码,记得开启词法域模式。

下一篇:从混乱到有序

你可能感兴趣的:(elisp,emacs)