按语:我在围观茅山道士跳大神的时候为「不懂编程的人」写了这一系列文章的第十一篇,整理于此。它的前一篇是《从混乱到有序》,介绍了如何用 Emacs Lisp 语言写一个快速排序程序。html
咒语,或许是存在的,并且也是有用的。说咒语彻底是骗人的鬼话,不科学,这种心态自己就不够科学。node
下面的 Emacs Lisp 代码可让变量 x
增 1:编程
(setq x (+ x 1))
假设存在 ++
这样的咒语,那么对 x
念这个咒语:segmentfault
(++ x)
是否也可以让 x
增 1 呢?函数
若基于咱们如今的 Emacs Lisp 水平,能够定义一个 ++
函数:测试
(defun ++ (x) (+ x 1))
这个函数能够做为上述问题的答案。没错,Emacs Lisp 的函数命名很自由。只要你高兴,中文也行,例如:code
(defun 自增 (x) (+ x 1))
大多数 Lisp 方言对变量、函数的命名都很自由。htm
++
或 自增
,是咒语吗?排序
不是。它们是咱们司空见惯的形式,也就是所谓的客观存在的东西。在 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 的世界里,能驾驭宏的人,他们就像大法师同样,吞云吐雾,上天入地,无所不能。
下一篇:无所遁形