长长的望远镜

按语:我在送孩子去幼儿园的路上为「不懂编程的人」写了这一系列文章的第九篇,整理于此。它的前一篇是《无名》,讲述了如何一步一步「推演」出 Y 组合子。编程

我有个长长的望远镜,能一直伸到你的家里面,你说什么作什么,我都能看到。segmentfault

怎样用 Emacs Lisp 语言描述这样的事?闭包

(defun bar () x)
(defun foo (x) (bar))
(foo '三原色)

(foo '三原色) 进行求值,会在 Emacs 微缓冲区显现 三原色dom

foo 函数会对 bar 函数进行求值。bar 函数不接受任何参数的函数,可是它的内部却凭空出现了一个变量 x。令 bar 不寒而栗的是,当 foo 对它求值时,这个 x 居然有意义的,它的值是符号原子 三原色函数

foo 的内部,bar 函数以为本身见了鬼。code

这实际上是 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 抓住了这个伸到本身家里的望远镜,把它的镜头涂黑了。get

动态域,是很古老的变量做用域模型。现代的变量做用域叫词法域,也叫静态域。

在词法域里,每一个函数都有本身的环境。当函数中出现自由变量时,它就在本身的环境里搜索变量的值,搜到了就做为自由变量的求值结果,不然就报错——变量无效。

词法域的好处是,没有人可以将长长的望远镜伸到你家里。看下面的例子:

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

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

下一篇从混乱到有序

相关文章
相关标签/搜索