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

以上,我們定義了宏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) ; 使用incx ; 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


下一篇:Emacs之魂(九):讀取器宏

推薦閱讀:

TAG:宏編程語言 | 引用 | Lisp |