一个没事找事的例子
当在Common Lisp中定义宏的时候,常常会使用到反引号(`)。比方说,我有这么一个函数
1 | (defun foobar () |
它被调用后会返回最后一个表达式的结果——13。如果我希望在第二个表达式计算后就把结果返回给外部的调用者的话,可以用return-from
1 | (defun foobar () |
当然了,这属于没事找事,因为完全可以把最后两个表达式放到一个prog1
(这也是没事找事),或者直接点,把最后一个表达式删掉来做到同样的效果——但如果是这样的话这篇东西就写不下去了,所以我偏要用return-from
。
还有一个更加没事找事的办法,就是用macrolet
定义一个局部的宏来代替return-from
——我很想把这个新的宏叫做return
,但这样SBCL会揍我一顿,所以我只好把这个宏叫做bye
(叫做exit
也会被揍)
1 | (defun foobar () |
如果我有另一个叫做foobaz
的函数
1 | (defun foobaz () |
也想要拥有bye
这种想来就来想走就走的能力的话,可以依葫芦画瓢地包含一个macrolet
1 | (defun foobaz () |
好了,现在我觉得每次都需要在函数体内粘贴一份bye
的实现代码太麻烦了,想要减少这种重复劳作。于是乎,我打算写一个宏来帮我复制粘贴代码。既然要定义宏,那么首先应当定义这个宏的名字以及用法,姑且是这么用的吧
1 | (with-bye foobar |
with-bye
这个宏需要能够展开成上面的手动编写的foobar
中的函数体的代码形式,那么with-bye
的定义中,就一定会含有macrolet
的代码,同时也就含有了反引号——好了,现在要来处理嵌套的反引号了。
这篇文章有个不错的讲解,各位不妨先看看。现在,让我来机械化地操作一遍,给出with-bye
的定义。首先,要确定生成的目标代码中,那一些部分是可变的。对于with-bye
而言,return-from
的第一个参数已经macrolet
的函数体是可变的,那么不妨把这两部分先抽象为参数
1 | (let ((name 'foobar) |
但这样是不够的,因为name
是一个在最外层绑定的,但它被放在了两层的反引号当中,如果它只有一个前缀的逗号,那么它就无法在外层的反引号求值的时候被替换为目标的FOOBAR
符号。因此,需要在,name
之前再添加一个反引号
1 | (let ((name 'foobar) |
如果你在Emacs中对上述的表达式进行求值,那么它吐出来的结果实际上是
1 | (MACROLET ((BYE (&OPTIONAL VALUE) |
显然,这还是不对。如果生成了上面这样的代码,那么对于bye
而言FOOBAR
就是一个未绑定的符号了。之所以会这样,是因为
name
在绑定的时候输入的是一个符号,并且name
被用在了嵌套的反引号内,它会被求值两次——第一次求值得到符号foobar
,第二次则是foobar
会被求值
因此,为了对抗第二次的求值,需要给,name
加上一个前缀的引号(‘),最终效果如下
1 | (let ((name 'foobar) |
所以with-bye
的定义是这样的
1 | (defmacro with-bye (name &body body) |
机械化的操作方法
我大言不惭地总结一下,刚才的操作步骤是这样的。首先,找出一段有规律的、需要被用宏来实现的目标代码;然后,识别其中的可变的代码,给这些可变的代码的位置起一个名字(例如上文中的name
和body
),将它们作为let
表达式的绑定,把目标代码装进同一个let
表达式中。此时,目标代码被加上了一层反引号,而根据每个名字出现的位置的不同,为它们适当地补充一个前缀的逗号;最后,如果在嵌套的反引号中出现的名字无法被求值多次——比如符号或者列表,那么还需要给它们在第一个逗号后面插入一个引号,避免被求值两次招致未绑定的错误。
一个例子
就用上面所引用的文章里的例子好了。有一天我觉得Common Lisp中一些常用的宏的名字实在是太长了想要精简一下——毕竟敲键盘也是会累的——假装没有自动补全的功能。我可能会定义下面这两个宏
1 | (defmacro d-bind (&body body) |
显然,这里的代码的写法出现了重复模式,不妨试用按照机械化的操作手法来提炼出一个宏。第一步,先识别出其中可变的内容。对于上面这个例子而言,变化的地方其实只有两个名字——新宏的名字(d-bind
和mv-bind
),以及旧宏的名字(destructuring-bind
和multiple-value-bind
)。第二步,给它们命名并剥离成let
表达式的绑定,得到如下的代码
1 | (let ((new-name 'd-bind) |
因为old-name
处于嵌套的反引号中,但是它是由最外层的let
定义的,所以应当添上一个前缀的逗号,得到
1 | (let ((new-name 'd-bind) |
最后,因为old-name
绑定的是一个符号,不能被两次求值(第二次是在defmacro
定义的新宏中展开,此时old-name
已经被替换为了destructuring-bind
,而它对于新宏而言是一个自由变量,并没有被绑定),所以需要有一个单引号来阻止第二次的求值——因为需要的就是符号destructuring-bind
本身。所以,最终的代码为
1 | (defmacro define-abbreviation (new-name old-name) |
试一下就可以确认这个define-abbreviation
是能用的(笑
后记
能够指导编写宏的、万能的、机械化的操作方法,我想应该是不存在的