[Emacs] Emacs之魂(八):反引用与嵌套反引用

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. 生成宏定义的宏

[Emacs] Emacs之魂(八):反引用与嵌套反引用_第1张图片

以上,我们定义了宏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. 嵌套反引用

[Emacs] Emacs之魂(八):反引用与嵌套反引用_第2张图片

嵌套反引用指的是,一个反引用表达式中嵌套出现了另一个反引用表达式。
在生成宏定义的宏中,嵌套反引用经常出现。

嵌套反引用表达式中,经常会出现类似,',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,不会被求值,
,',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

你可能感兴趣的:([Emacs] Emacs之魂(八):反引用与嵌套反引用)