按语:我任何路边的摄像头下走过的时候为「不懂编程的人」写了这一系列文章的最后一篇,整理于此。它的前一篇是《咒语》,介绍了如何在 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 语言了解这方面的一些概念。
下一篇:
'()
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。