1. 反引用
上文我们介绍了如何使用defmacro
定义宏,
(defmacro inc (var)
(list 'setq var (list '1+ var)))
我们定义了inc
宏,(inc x)
会被展开为(setq x (1+ x))
,因此,
(defvar x 0)
(inc x)
x ; 1
宏做的是语法对象的变换操作,因此几乎每个宏最后都返回一个列表,
可是,类似上述inc
宏那样,每次都使用list
来创建列表,是一件麻烦的事情,
所以,Lisp提供了反引用(quasiquote/backquote),可以便捷的生成列表。
例如,以上inc
宏使用反引用来生成列表,可以修改为,
(defmacro inc (var)
`(setq ,var (1+ ,var)))
可以看到,反引用``(setq ,var (1+ ,var)))与
(inc x)的展开式
(setq x (1+ x))非常相像, 我们只需要将反引号
` 去掉,然后将反引用表达式中的逗号表达式
,var,替换为
var绑定的值
x`即可。
2. 反引用表达式的求值规则
下面我们通过几个例子来说明反引用的使用方式,其中=>
表示“求值为”。
求值规则:
(1)如果反引用表达式中不包含逗号,
,那么它和引用表达式是一样的,
因此反引用通常被看做是一种特殊的引用(quote)
`(a list of (+ 2 3) elements)
=> (a list of (+ 2 3) elements)
(2)反引用表达式中的逗号表达式会被求值
`(a list of ,(+ 2 3) elements)
=> (a list of 5 elements)
(3)反引用表达式中的,@
表达式,也会被求值,但是要求其结果必须是一个列表,
,@
会去掉列表的括号,将列表中的元素放到,@
表达式出现的位置
(defvar x '(2 3))
`(1 ,@x 4)
=> (1 2 3 4)
`(1 ,@(cdr '(1 2 3)) 4)
=> (1 2 3 4)
3. 生成宏定义的宏
以上,我们定义了宏inc
,
宏调用(inc x)
,会被展开为(setq x (1+ x))
。
在编写宏的时候,一个常用的思路是,
先考虑展开关系,即我们期望将A展开为B,再根据这个线索编写相应的宏。
那么,我们可否编写一个宏,让它展开成(defmacro ...)
呢?
是可以的,这是一种展开为宏定义的宏,它可以作为defmacro
来使用。
考虑展开关系,我们期望将(create-inc)
展开为
(defmacro inc (var)
`(setq ,var (1+ ,var)))
于是,宏create-inc
就应该被这样定义,
(defmacro create-inc ()
`(defmacro inc (var)
`(setq ,var (1+ ,var))))
我们来试验一下,
(create-inc) ; 定义了inc
(defvar x 0)
(inc x) ; 使用inc
x ; 1
我们还可以给create-inc
加上参数。
考虑展开关系,我们将(create-inc-n y)
展开为,
(defmacro inc-n (var)
`(setq ,var (+ y ,var)))
那么create-inc-n
应该怎么定义呢?事实上,
(defmacro create-inc-n (num)
`(defmacro inc-n (var)
`(setq ,var (+ ,',num ,var))))
第一次看到,',num
的时候,我非常惊讶,这到底是什么?
4. 嵌套反引用
嵌套反引用指的是,一个反引用表达式中嵌套出现了另一个反引用表达式。
在生成宏定义的宏中,嵌套反引用经常出现。
嵌套反引用表达式中,经常会出现类似,',num
这样的表达式,
它不能被写成,num
,也不能被写成,,num
,下面我们进行仔细的分析。
(1),num
为什么不正确
先看一下展开关系,我们期望将(create-inc-n y)
展开为,
(defmacro inc-n (var)
`(setq ,var (+ y ,var)))
即,嵌套反引用表达式,应该按下述方式求值,
`(defmacro inc-n (var)
`(setq ,var (+ ,',num ,var))))
=> (defmacro inc-n (var)
`(setq ,var (+ y ,var)))
其中,,var
是不应该被求值的,因为这是内层反引用需要的,
如果我们将,',num
写成,num
,那么它就和,var
一样不会被求值了,
`(defmacro inc-n (var)
`(setq ,var (+ ,num ,var))))
=> (defmacro inc-n (var)
`(setq ,var (+ ,num ,var)))
这和我们期望的展开关系不同。
(2),,num
为什么不正确
写成,,num
在求值最外层反引用表达式的时候,确实会求值num
的值,
但是,在求值内层反引用表达式的时候,这个值还会被再求值一次。
(create-inc-n y)
将被展开为,
`(defmacro inc-n (var)
`(setq ,var (+ ,,num ,var)))
=> (defmacro inc-n (var)
`(setq ,var (+ ,y ,var)))
可是,在进行宏调用(create-inc-n y)
的时候,我们不应该关心y
的值是什么,
因为在宏展开阶段,y
可能还没有值。
而且,该展开式和我们预期的展开结果也不相同。
(3),',num
是怎么来的
综上分析,我们需要在外层反引用表达式被求值的时候,求值num
,
而在内层反引用表达式被求值的时候,不再继续求值num
的值,
因此,我们需要给num
的值加上一个引用来“阻止”求值。
因此,(create-inc-n y)
会被展开为,
`(defmacro inc-n (var)
`(setq ,var (+ ,',num ,var))))
=> (defmacro inc-n (var)
`(setq ,var (+ ,'y ,var)))
而内层反引用表达式被求值的时候,,'y
将求值为y
。
所以,(inc-n x)
将被展开为
`(setq ,var (+ ,'y ,var))
=> (setq x (+ y x))
和我们期望的展开结果相同。
5. 嵌套反引用的求值规则
在生成宏定义的宏中,经常会出现嵌套反引用,
如果我们定义了另一个宏other-macro
来生成create-inc-n
的定义,
(defmacro other-macro ()
`(defmacro create-inc-n (num)
`(defmacro inc-n (var)
`(setq ,var (+ ,',num ,var)))))
那么,将出现三层嵌套反引用。
不过,不用担心,嵌套反引用也是有求值规则的,以下我们用两层嵌套反引用作为例子来说明。
求值规则:
(1)嵌套反引用被求值的时候,一次求值,只去掉一层反引用,内层反引用不受影响,
`(defmacro inc-n (var)
`(setq ,var (+ ,',num ,var))))
=> (defmacro inc-n (var)
`(setq ,var (+ ,'y ,var)))
(2)嵌套反引用表达式中的逗号表达式,是否被求值,要根据情况来定,
如果最外层嵌套反引用总共有n
层,那么一定不会出现包含大于n
个逗号的表达式,
且包含逗号数目小于n
的表达式不会被求值,只有逗号数目等于n
的表达式才会被求值。
`(defmacro inc-n (var)
`(setq ,var (+ ,',num ,var))))
=> (defmacro inc-n (var)
`(setq ,var (+ ,'y ,var)))
最外层嵌套反引用总共有n=2
层,
,var
表达式包含一个逗号,1<n
,不会被求值,
,',num
表达式包含两个逗号,2=n
,会被求值。
(3)被求值的逗号表达式,其求值方式是,
去掉最右边的一个逗号,然后将表达式替换成它的值。
`(defmacro inc-n (var)
`(setq ,var (+ ,',num ,var))))
=> (defmacro inc-n (var)
`(setq ,var (+ ,'y ,var)))
,',num
,去掉最右边的逗号,'num
,然后将num
替换成它的值y
,
于是得到了,'y
。
参考
GNU Emacs Lisp Reference Manual
ANSI Common Lisp
On Lisp
Let Over Lambda