jiangliu 2019-06-26
按语:我在围观茅山道士跳大神的时候为「不懂编程的人」写了这一系列文章的第十一篇,整理于此。它的前一篇是《从混乱到有序》,介绍了如何用 Emacs Lisp 语言写一个快速排序程序。
咒语,或许是存在的,而且也是有用的。说咒语完全是骗人的鬼话,不科学,这种心态本身就不够科学。
下面的 Emacs Lisp 代码可以让变量 x
增 1:
(setq x (+ x 1))
假设存在 ++
这样的咒语,那么对 x
念这个咒语:
(++ x)
是否也能够让 x
增 1 呢?
若基于我们现在的 Emacs Lisp 水平,可以定义一个 ++
函数:
(defun ++ (x) (+ x 1))
这个函数可以作为上述问题的答案。没错,Emacs Lisp 的函数命名很自由。只要你高兴,中文也行,例如:
(defun 自增 (x) (+ x 1))
大多数 Lisp 方言对变量、函数的命名都很自由。
++
或 自增
,是咒语吗?
不是。它们是我们司空见惯的形式,也就是所谓的客观存在的东西。在 Emacs Lisp 里,凡是能用函数描述的东西,都不能称为咒语。只有一些具有不寻常的能力的语言,才称得上咒语。
那么,
(let ((a 1) (b 2) (c 3)) (+ a b c))
是咒语吗?
是。因为你没办法通过函数实现一个这样的 let
。也许有办法,只是我的功力不够,实现不出来。
let
不是 Emacs Lisp 语法的一部分吗?我认为不是。因为即使没有 let
,我们也能通过函数完成等价的功能,即
(funcall (lambda (a b c) (+ a b c)) 1 2 3)
let
只是让这个功能用起来更方便了而已。
在 Emacs Lisp 语言中,的确有一种像咒语一样的东西,它叫宏。我们可以通过宏,自己造一个像 let
这样的东西来用。实际上,在其他一些 Lisp 方言里,let
与 let*
都是通过宏的方式定义出来的。
我们将要造的这个东西称为 my-let
,制造它的目的不是要用它来取代 let
,而是了解如何念 Emacs Lisp 的咒语,哦,不对,是了解如何使用 Emacs Lisp 的宏。
在制造 my-let
之前,需要先解决序对列表的拆分问题,即如何将下面这样的列表
((a 1) (b 2) (c 3))
拆成两个列表 (a b c)
与 (1 2 3)
。
先尝试拆出 (a b c)
:
(defun take-var-names (var-list) (let ((var-name (car (car var-list)))) (if (null var-name) nil (cons var-name (take-var-names (cdr var-list)))))) (take-var-names '(('a 1) ('b 2) ('c 3)))
这个函数要比以前访问列表每个元素的函数稍微复杂了点。在这个函数里,不仅访问了列表的每个元素,而且还从所访问的元素中提取了信息——每个序对的首元素,并将所提取的信息保存到另一个列表里。简而言之,就是在访问一个列表的过程中,构造了一个列表。
不够,有点不对。我们的目的是要制造一个 像 let
的 my-let
,所以就不好意思再在某个中间环节使用 let
了。因此,需要用匿名函数来替代 take-var-names
里的 let
表达式,结果为:
(defun take-var-names (var-list) (funcall (lambda (var-name) (if (null var-name) nil (cons var-name (take-var-names (cdr var-list))))) (car (car var-list)))) (take-var-names '(('a 1) ('b 2) ('c 3)))
用类似的方式,可以抽取出 (1 2 3)
:
(defun take-var-values (var-list) (funcall (lambda (value) (if (null value) nil (cons value (take-var-values (cdr var-list))))) (car (cdr (car var-list))))) (take-var-values '(('a 1) ('b 2) ('c 3)))
从形式上看,take-var-names
与 take-var-values
的定义只有一个地方不一样,其他都一样。倘若我们能将这些不一样的地方弄成一样,那么就可以将这两个函数就可以合并成一个了。
怎么将不一样的地方弄成一样呢?还记得我们以前是怎样将一个不想看到的东西变成没有的么?方法是将它提升为函数的参数。这个方法在这里依然管用。不一样的地方,提升为函数的参数,它们就都一样了。用这个办法,去定义一个名字叫 take-some-thing
的函数:
(defun take-something (var-list f) (funcall (lambda (x) (if (null x) nil (cons (funcall f x) (take-something (cdr var-list) f)))) (car var-list)))
像下面这样使用 take-something
函数,就可以起到与 take-var-names
同样的效果:
(take-something '(('a 1) ('b 2) ('c 3)) (lambda (x) (car x)))
下面的表达式则起到与 take-var-values
同样的效果:
(take-something '(('a 1) ('b 2) ('c 3)) (lambda (x) (car (cdr x))))
倘若再认真思考一下,不难发现,现在 take-something
的能力似乎已经远远超越了从一个序对列表中提取部分信息的功能,它能够将一个列表映射为另一个列表,而且这种映射还很广义。例如:
(take-something '(1 2 3) (lambda (x) (+ x 1)))
结果可以得到 (2 3 4)
,即让列表中每个元素增 1,而这种运算显然与提取什么信息似乎没有关系,因此 take-something
这个函数名需要修改一下,让它名副其实。就叫它 list-map
吧,即:
(defun list-map (a f) (funcall (lambda (x) (if (null x) nil (cons (funcall f x) (list-map (cdr a) f)))) (car a)))
它的功能将一个列表 a
映射为另一个列表,映射规则是 f
。f
可以将 a
中的一个元素映射为另一个元素。
有了 list-map
,就可以制造 my-let
了,不就是将一个序对列表拆成两部分,一部分扔给匿名函数作为参数列表,另一部分扔给匿名函数作为参数值吗?假设序对列表为 bindings
,下面的代码似乎就能够轻松解决这个的问题:
(funcall (lambda (list-map bindings (lambda (x) (car x)))) (list-map bindings (lambda (x) (car (cdr x)))))
应当注意,我们是在制作一条咒语。这条咒语里的文字是不能以它们在现实世界里的含义进行解释的,也就是说,我们要禁止 Emacs Lisp 解释器对这条咒语中的任何一部分有所解读。有一个符号能够起到这种效果,即反引号(很抱歉,我用了 Markdown 标记语言写的这份文档,在 Markdown 的普通文本里是没法给你看反引号的样子),倘若你的键盘很大众化,反引号与 ~
符号位于同一个键位。现在,将反引号作用于上述代码:
`(funcall (lambda (list-map bindings (lambda (x) (car x)))) (list-map bindings (lambda (x) (car (cdr x)))))
现在,上述表达式实际上是一个列表,你可以尝试在 Emacs 里对它试着进行求值,结果可以得到这个列表的字面形式。
实际上,反引号与 '
的功能相似,就是告诉 Emacs Lisp 解释器不要对列表本身以及列表中的任何一个元素进行求值,只不过 '
太过于武断,它彻底屏蔽了 Emacs Lisp 解释器对列表的影响,而反引号允许开后门,让 Emacs Lisp 解释器能够对列表中的部分元素进行求值。要开这个后门,也需要一个符号,即 ,
。
对于 (list-map bindings (lambda (x) (car x)))
与 (list-map bindings (lambda (x) (car (cdr x))))
,一定是要开后门的,否则它们就会在字面上变成匿名函数的参数名与参数值,这不是我们想要的结果。现在为上述代码加上 ,
:
`(funcall (lambda ,(list-map bindings (lambda (x) (car x)))) ,(list-map bindings (lambda (x) (car (cdr x)))))
不过,这个匿名函数所接受的参数,形式上不正确。因为 (list-map bindings (lambda (x) (car (cdr x))))
的求值结果是一个列表,而匿名函数需要的不是列表,而是脱去列表括号的一组值。不要担心,Emacs Lisp 提供了 @
符号,它可以将列表里的元素取出并平铺开来:
`(funcall (lambda ,(list-map bindings (lambda (x) (car x)))) ,@(list-map bindings (lambda (x) (car (cdr x)))))
现在,my-let
的匿名函数的参数问题算是得到很好的解决,现在,补上它的身体:
`(funcall (lambda ,(list-map bindings (lambda (x) (car x))) ,body) ,@(list-map bindings (lambda (x) (car (cdr x)))))
没错,也得为 body
开个后门,否则 Emacs Lisp 解释器会认为 body
是个符号原子,而不是一个表达式,而匿名函数的身体必须得是表达式才可以。
最后,告诉 Emacs Lisp 解释器,这个东西是咒语,哦,宏:
(defmacro my-let (bindings body) `(funcall (lambda ,(list-map bindings (lambda (x) (car x))) ,body) ,@(list-map bindings (lambda (x) (car (cdr x))))))
大功告成!试着用一下:
(my-let ((a 1) (b 2) (c 3)) (+ a b c))
结果等于 6。一切都没毛病,咒语很管用。
Emacs Lisp 解释器对宏表达式进行求值时,发生了什么呢?首先,它将宏按字面展开,不过在这个过程中,它也会对留出后门的表达式进行求值;然后对宏的展开结果进行求值。
使用 macroexpand
函数,可以看到宏的展开结果。例如:
(macroexpand '(my-let ((a 1) (b 2) (c 3)) (+ a b c)))
对上述表达式求值,结果会在微缓冲区或 *Messages
缓冲区里显示宏的实际展开结果,即:
(funcall (lambda (a b c) (+ a b c)) 1 2 3)
这个结果,与我们在前面为 let
表达式构造的等价匿名函数表达式丝毫不差。
真的没毛病吗?倘若咒语念得不够好,经常会失灵。my-let
看上去念的还行。但是,一些复杂的咒语,能念好的人不太多。常见的念错咒语的方式可参考 [1]。
接下来,要不要再制作一个 my-let*
去挑战一下 let*
?
身为勤劳勇敢的中国人,在日益增长的美好生活需要和不平衡不充分的发展之间的矛盾面前,当然要响应党和国家的号召,继续前进。不知道这样肉麻,人民日报会不会刊登这篇文章啊。
先回顾一下 let*
的特点,它的特点是在变量绑定列表中允许一个变量的值是前面的变量构成的表达式。例如:
(let* ((a 1) (b 2) (c (+ a b))) (+ a b c))
与这个表达式等价的匿名函数表达式可写为:
(funcall (lambda (a) (funcall (lambda (b) (funcall (lambda (c) (+ a b c)) (+ a b))) 2)) 1)
看到这样壁垒森严的匿名函数表达式,双腿难免有点乏力。不过,把这个表达式的形状略微调整一下,会更清楚:
(funcall (lambda (a) (funcall (lambda (b) (funcall (lambda (c) (+ a b c)) (+ a b))) 2)) 1)
看到了吧,不过是将 list-map
从 bindings
里拆分出来的两个列表分别扔到三重台阶上。
试着先往第一层与最后一层上扔第一个参数与它的值:
(defmacro my-let* (bindings) (my-let ((names (list-map bindings (lambda (x) (car x)))) (values (list-map bindings (lambda (x) (car (cdr x)))))) `(funcall (lambda (,(car names))) ,(car values))))
如何知道这个宏是不是正确呢?试着将宏表达式代入 macroexpand
函数:
(macroexpand '(my-let* ((a 1) (b 2) (c (+ a b)))))
对上述表达式求值,得到的展开结果为:
(funcall (lambda (a)) 1)
正确无误。
接下来,试着继续试着往第二层与倒数第二层上扔第二个参数与它的值:
(defmacro my-let* (bindings) (my-let ((names (list-map bindings (lambda (x) (car x)))) (values (list-map bindings (lambda (x) (car (cdr x)))))) `(funcall (lambda (,(car names)) (funcall (lambda (,(car (cdr names)))) ,(car (cdr values)))) ,(car values))))
再次用 macroexpand
对宏进行展开,结果得到:
(funcall (lambda (a) (funcall (lambda ...) 2)) 1)
结果似乎依然正确,由于 macroexpand
在第二层匿名函数里输出了省略号,所以也不确定省略号是不是包含了参数名 b
。先不管了,继续处理第三层与倒数第三层,不过,这次我们需要增加 body
——匿名函数的终点:
(defmacro my-let* (bindings body) (let ((names (list-map bindings (lambda (x) (car x)))) (values (list-map bindings (lambda (x) (car (cdr x)))))) `(funcall (lambda (,(car names)) (funcall (lambda (,(car (cdr names))) (funcall (lambda (,(car (cdr (cdr names)))) ,body) ,(car (cdr (cdr values))))) ,(car (cdr values)))) ,(car values))))
现在可以测试 my-let*
的定义是否正确,下面是测试代码:
(my-let* ((a 1) (b 2) (c (+ a b))) (+ a b c))
结果为 6,正确。
不过,这个正确是以大量的重复代码来保证的。在示例中,仅仅三个参数构成的 bindings
就已经产生这么臃肿的宏定义了,若是参数更多一些,岂不会把定义宏的人累死吗?
一定是思路出现了问题。我们需要从头再来。从最简单的情况开始。大部分时候,当我们的思路实在很难进展下去的时候,往往是在思路的源头就出现了偏差。
最简单的情况是什么?是 my-let*
的第一个参数为 nil
(空表)的时候,即:
(defmacro my-let* (bindings body) (if (null bindings) body (...)))
上述代码意味着,倘若 bindings
为 nil
时,my-let*
的展开结果是 body。省略号部分表示
my-let* 第一个参数不为
nil` 的情况,然而现在我们还不知道怎么去写。
再来看 my-let*
第一个参数可能为 nil
也可能为只包含一个序对的情况,对于这种情况可以像下面这样处理:
(defmacro my-let* (bindings body) (if (null bindings) body (my-let ((x (car bindings))) `(funcall (lambda (,(car x)) ,body) ,(car (cdr x))))))
当 bindings
只包含一个序对时,匿名函数必须出现 body
,而这正是 bindings
为 nil
时的结果。因此,上述代码可以修改为:
(defmacro my-let* (bindings body) (if (null bindings) body (my-let ((x (car bindings))) `(funcall (lambda (,(car x)) (my-let* ,(cdr bindings) ,body)) , (car (cdr x))))))
于是,奇迹就出现了,我们已经成功的完成了 my-let*
宏的定义!天下难事,必作于易。天下大事,必作于细。
试试看:
(my-let* ((a 1) (b 2) (c (+ a b))) (+ a b c))
结果为 6,正确!
我们是怎么成功的呢?不妨看看 macroexpand
对 my-let*
的展开结果:
(macroexpand '(my-let* ((a 1) (b 2) (c (+ a b))) (+ a b c)))
结果为
(funcall (lambda (a) (my-let* (... ...) (+ a b c))) 1)
看到了吧,在 my-let*
的展开结果中又出现了 my-let*
,接下来 Emacs Lisp 解释器不得不继续对它继续进行展开,但是这次 my-let
的参数变成了 (cdr bindings)
。依此类推,结果就形成了宏的递归,直至 bindings
为 nil
,最后一次的 my-let*
展开结果就是 body
。
没错,Emacs Lisp 宏是可以递归的。也就是说,宏也能构成周而复始的发动机。
现在,想必你已经大致上对 Emacs Lisp 宏有一定的认识了。它的行为与函数有些相似,但是二者有着本质的不同。函数操纵的是表达式的值,而宏操纵的是表达式。
在 Emacs Lisp 的世界里,能驾驭宏的人,他们就像大法师一样,吞云吐雾,上天入地,无所不能。
下一篇:无所遁形