无所遁形

按语:我任何路边的摄像头下走过的时候为「不懂编程的人」写了这一系列文章的最后一篇,整理于此。它的前一篇是《咒语》,介绍了如何在 Emacs Lisp 程序的世界里登坛作法,呼风唤雨。

还记得

(defun list-map (a f)
  (funcall (lambda (x)
             (if (null x)
                 nil
               (cons (funcall f x) (list-map (cdr a) f))))
           (car a)))

么?

当时,为了表示把手绑起来也能用脚写字,所以故意没用 let,现在可以坦然地用 let 了,这样可以让代码更清晰一些:

(defun list-map (a f)
  (let ((x (car a)))
    (if (null x)
        nil
      (cons (funcall f x) (list-map (cdr a) f)))))

这个函数可以将函数 f 作用于 列表 a 中的每个元素,结果为一个列表。例如:

(list-map '(1 2 3) (lambda (x) (+ x 1)))

结果为 (2 3 4)

匿名函数可以作为参数传递给 list-map,那么有名的函数可不可以?试试看:

(defun ++ (x) (+ x 1))
(list-map '(1 2 3) ++)

不行。Emacs Lisp 解释器抱怨,++ 是无效的变量。它的抱怨没错,++ 是个函数,不是变量。虽然在逻辑上,变量与函数不用分得太清,但是 Emacs Lisp 解释器从形式上分不清什么是函数,什么是变量。不过,其他 Lisp 方言,例如 Scheme 就能够分辨出来。归根结底,还是 Emacs Lisp 的年代过于久远导致。

在 Emacs Lisp 里,需要将上述的 list-map 表达式改成下面这样:

(list-map '(1 2 3) (function ++))

或者简写形式:

(list-map '(1 2 3) #'++)

function#' 告诉 Emacs Lisp 解释器,后面这个符号是函数。这样 Emacs Lisp 就可以正确识别 ++ 了。

以上,只是本文的前奏。下面我们来思考一个更深刻的问题。这个问题可能深到无止境的程度。

现在,假设 list-map 所接受的列表是一个嵌套的列表——列表中有些元素也是列表:

(list-map '(1 2 3 (4 5 6) 7 8 9) #'++)

对这个表达式进行求值,发现 list-map 失灵了,++ 没法作用于列表元素 (4 5 6)++ 只能对一个数进行增 1 运算,却不能对一个列表这样做。倘若我们真的很想让 ++ 能够继续进入 (4 5 6) 内部,将其中每一个元素都增 1,然后再跳出来继续处理 (4 5 6) 后面的元素,该怎么办?

首先,我们需要具有判断列表中的一个元素是不是列表的能力。Emacs Lisp 提供的 listp 函数可以让我们具有这种能力。例如:

(listp 3)
(listp '[1 2 3])
(listp '(1 2 3))
(listp '())
(listp nil)

上面这五个表达式,前两个的求值结果皆为 nil,后面三个的求值结果皆为 t

有了 listp,我们就可以区分一个列表元素是原子还是列表了。能区分,就好办。倘若列表元素依然是列表,那么我们就继续将 list-map 作用于这个元素,而倘若它不是列表,那么就用 ++ 之类的函数伺候之。

试试看:

(defun list-map (a f)
  (let ((x (car a)))
    (if (null x)
        nil
      (if (listp x)
          (cons (list-map x f) (list-map (cdr a) f))
        (cons (funcall f x) (list-map (cdr a) f))))))

试验一下这个新的 list-map 能不能用:

(list-map '(1 2 3 (4 5 6) 7 8 9) #'++)

结果得到 (2 3 4 (5 6 7) 8 9 10),正确。

再拿更多层数的列表试试看:

(list-map '(1 2 3 (4 5 (0 1)) 7 8 9 (3 3 3)) #'++)

结果得到 (2 3 4 (5 6 (1 2)) 8 9 10 (4 4 4)),正确。

就这样,我们只是对 list-map 略动手脚,似乎就可以让无论嵌套有多少层,藏匿有多深的列表,在 list-map 面前都是一览无余的。

在未对列表结构有任何破坏的情况下,可以确定上述的感觉是正确的。因为计算机的运转总是周而复始。倘若程序本身只变动了数据的形状,而未破坏它的拓扑结构,我们就总是能够做到见微而知著。

上面对 list-map 的修改,虽然只考虑了再次使用 list-map 来处理列表元素为列表的情况,结果却让 list-map 能够适用于任何形式的列表嵌套。我们在用宏的形式定义 my-let* 的时候也遇到过这样的情况。为什么会这样?这其实是在周而复始的运动中,出现了类型。listp 能够判断一个值是否是列表类型。在一个 Emacs Lisp 程序里,可以有无数个列表,但它们的类型却是相同的,都是列表类型。

天网恢恢,疏而不漏,靠的不过是递归 + 类型。类型,描述了值的共性。它生活在柏拉图的理想国里,是一种完美的模具,而那些值只不过是从模具里铸出来的东西。类型是比递归一个更大的题目,已经有许多人写了这方面的专著。倘若你对这个感兴趣,可以通过 Haskell 语言了解这方面的一些概念。

下一篇'()

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章