编写嵌套反引号的宏

一个没事找事的例子

当在Common Lisp中定义宏的时候,经常会使用到反引号(`)。比方说,我有这么一个函数git

(defun foobar ()
  (+ 1 1)
  (+ 2 3)
  (+ 5 8))

它被调用后会返回最后一个表达式的结果——13。若是我但愿在第二个表达式计算后就把结果返回给外部的调用者的话,能够用return-fromgithub

(defun foobar ()
  (+ 1 1)
  (return-from foobar (+ 2 3))
  (+ 5 8))

固然了,这属于没事找事,由于彻底能够把最后两个表达式放到一个prog1(这也是没事找事),或者直接点,把最后一个表达式删掉来作到一样的效果——但若是是这样的话这篇东西就写不下去了,因此我偏要用return-from函数

还有一个更加没事找事的办法,就是用macrolet定义一个局部的宏来代替return-from——我很想把这个新的宏叫作return,但这样SBCL会揍我一顿,因此我只好把这个宏叫作bye(叫作exit也会被揍).net

(defun foobar ()
  (macrolet ((bye (&optional value)
               `(return-from foobar ,value)))
    (+ 1 1)
    (bye (+ 2 3))
    (+ 5 8)))

若是我有另外一个叫作foobaz的函数code

(defun foobaz ()
  (+ 1 2)
  (+ 3 4)
  (+ 5 6))

也想要拥有bye这种想来就来想走就走的能力的话,能够依葫芦画瓢地包含一个macroletblog

(defun foobaz ()
  (macrolet ((bye (&optional value)
               `(return-from foobaz ,value)))
    (+ 1 2)
    (bye (+ 3 4))
    (+ 5 6)))

好了,如今我以为每次都须要在函数体内粘贴一份bye的实现代码太麻烦了,想要减小这种重复劳做。因而乎,我打算写一个宏来帮我复制粘贴代码。既然要定义宏,那么首先应当定义这个宏的名字以及用法,姑且是这么用的吧ip

(with-bye foobar
  (+ 1 1)
  (bye (+ 2 3))
  (+ 5 8))

with-bye这个宏须要可以展开成上面的手动编写的foobar中的函数体的代码形式,那么with-bye的定义中,就必定会含有macrolet的代码,同时也就含有了反引号——好了,如今要来处理嵌套的反引号了。get

这篇文章有个不错的讲解,各位不妨先看看。如今,让我来机械化地操做一遍,给出with-bye的定义。首先,要肯定生成的目标代码中,那一些部分是可变的。对于with-bye而言,return-from的第一个参数已经macrolet的函数体是可变的,那么不妨把这两部分先抽象为参数it

(let ((name 'foobar)
      (body '((+ 1 1) (bye (+ 2 3)) (+ 5 8))))
  `(macrolet ((bye (&optional value)
                `(return-from ,name ,value)))
     ,@body))

但这样是不够的,由于name是一个在最外层绑定的,但它被放在了两层的反引号当中,若是它只有一个前缀的逗号,那么它就没法在外层的反引号求值的时候被替换为目标的FOOBAR符号。所以,须要在,name以前再添加一个反引号io

(let ((name 'foobar)
      (body '((+ 1 1) (bye (+ 2 3)) (+ 5 8))))
  `(macrolet ((bye (&optional value)
                `(return-from ,,name ,value)))
     ,@body))

若是你在Emacs中对上述的表达式进行求值,那么它吐出来的结果其实是

(MACROLET ((BYE (&OPTIONAL VALUE)
             `(RETURN-FROM ,FOOBAR ,VALUE)))
  (+ 1 1)
  (BYE (+ 2 3))
  (+ 5 8))

显然,这仍是不对。若是生成了上面这样的代码,那么对于bye而言FOOBAR就是一个未绑定的符号了。之因此会这样,是由于

  1. name在绑定的时候输入的是一个符号,而且
  2. name被用在了嵌套的反引号内,它会被求值两次——第一次求值获得符号foobar,第二次则是foobar会被求值

所以,为了对抗第二次的求值,须要给,name加上一个前缀的引号(‘),最终效果以下

(let ((name 'foobar)
      (body '((+ 1 1) (bye (+ 2 3)) (+ 5 8))))
  `(macrolet ((bye (&optional value)
                `(return-from ,',name ,value)))
     ,@body))

因此with-bye的定义是这样的

(defmacro with-bye (name &body body)
  `(macrolet ((bye (&optional value)
                `(return-from ,',name ,value)))
     ,@body))

机械化的操做方法

我大言不惭地总结一下,刚才的操做步骤是这样的。首先,找出一段有规律的、须要被用宏来实现的目标代码;而后,识别其中的可变的代码,给这些可变的代码的位置起一个名字(例如上文中的namebody),将它们做为let表达式的绑定,把目标代码装进同一个let表达式中。此时,目标代码被加上了一层反引号,而根据每一个名字出现的位置的不一样,为它们适当地补充一个前缀的逗号;最后,若是在嵌套的反引号中出现的名字没法被求值屡次——好比符号或者列表,那么还须要给它们在第一个逗号后面插入一个引号,避免被求值两次招致未绑定的错误。

一个例子

就用上面所引用的文章里的例子好了。有一天我以为Common Lisp中一些经常使用的宏的名字实在是太长了想要精简一下——毕竟敲键盘也是会累的——伪装没有自动补全的功能。我可能会定义下面这两个宏

(defmacro d-bind (&body body)
  `(destructuring-bind ,@body))
(defmacro mv-bind (&body body)
  `(multiple-value-bind ,@body))

显然,这里的代码的写法出现了重复模式,不妨试用按照机械化的操做手法来提炼出一个宏。第一步,先识别出其中可变的内容。对于上面这个例子而言,变化的地方其实只有两个名字——新宏的名字(d-bindmv-bind),以及旧宏的名字(destructuring-bindmultiple-value-bind)。第二步,给它们命名并剥离成let表达式的绑定,获得以下的代码

(let ((new-name 'd-bind)
      (old-name 'destructuring-bind))
  `(defmacro ,new-name (&body body)
     `(,old-name ,@body)))

由于old-name处于嵌套的反引号中,可是它是由最外层的let定义的,因此应当添上一个前缀的逗号,获得

(let ((new-name 'd-bind)
      (old-name 'destructuring-bind))
  `(defmacro ,new-name (&body body)
     `(,,old-name ,@body)))

最后,由于old-name绑定的是一个符号,不能被两次求值(第二次是在defmacro定义的新宏中展开,此时old-name已经被替换为了destructuring-bind,而它对于新宏而言是一个自由变量,并无被绑定),因此须要有一个单引号来阻止第二次的求值——由于须要的就是符号destructuring-bind自己。因此,最终的代码为

(defmacro define-abbreviation (new-name old-name)
  `(defmacro ,new-name (&body body)
     `(,',old-name ,@body)))

试一下就能够确认这个define-abbreviation是能用的(笑

后记

可以指导编写宏的、万能的、机械化的操做方法,我想应该是不存在的

阅读原文

相关文章
相关标签/搜索