[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

宏做的是語法對象的變換操作,因此幾乎每個(gè)宏最后都返回一個(gè)列表,
可是,類似上述inc宏那樣,每次都使用list來創(chuàng)建列表,是一件麻煩的事情,
所以,Lisp提供了反引用(quasiquote/backquote),可以便捷的生成列表。

例如,以上inc宏使用反引用來生成列表,可以修改為,

(defmacro inc (var)
    `(setq ,var (1+ ,var)))

可以看到,反引用``(setq ,var (1+ ,var)))(inc x)的展開式(setq x (1+ x))非常相像, 我們只需要將反引號` 去掉,然后將反引用表達(dá)式中的逗號表達(dá)式,var,替換為var綁定的值x`即可。

2. 反引用表達(dá)式的求值規(guī)則

下面我們通過幾個(gè)例子來說明反引用的使用方式,其中=>表示“求值為”。

求值規(guī)則:
(1)如果反引用表達(dá)式中不包含逗號,,那么它和引用表達(dá)式是一樣的,
因此反引用通常被看做是一種特殊的引用(quote)

`(a list of (+ 2 3) elements)
=> (a list of (+ 2 3) elements)

(2)反引用表達(dá)式中的逗號表達(dá)式會被求值

`(a list of ,(+ 2 3) elements)
=> (a list of 5 elements)

(3)反引用表達(dá)式中的,@表達(dá)式,也會被求值,但是要求其結(jié)果必須是一個(gè)列表,
,@會去掉列表的括號,將列表中的元素放到,@表達(dá)式出現(xiàn)的位置

(defvar x '(2 3))

`(1 ,@x 4)
=> (1 2 3 4)

`(1 ,@(cdr '(1 2 3)) 4)
=> (1 2 3 4)

3. 生成宏定義的宏

以上,我們定義了宏inc,
宏調(diào)用(inc x),會被展開為(setq x (1+ x))。

在編寫宏的時(shí)候,一個(gè)常用的思路是,
先考慮展開關(guān)系,即我們期望將A展開為B,再根據(jù)這個(gè)線索編寫相應(yīng)的宏。

那么,我們可否編寫一個(gè)宏,讓它展開成(defmacro ...)呢?
是可以的,這是一種展開為宏定義的宏,它可以作為defmacro來使用。

考慮展開關(guān)系,我們期望將(create-inc)展開為

(defmacro inc (var) 
    `(setq ,var (1+ ,var)))

于是,宏create-inc就應(yīng)該被這樣定義,

(defmacro create-inc ()
    `(defmacro inc (var)
        `(setq ,var (1+ ,var))))

我們來試驗(yàn)一下,

(create-inc)    ; 定義了inc

(defvar x 0)
(inc x)    ; 使用inc

x    ; 1

我們還可以給create-inc加上參數(shù)。
考慮展開關(guān)系,我們將(create-inc-n y)展開為,

(defmacro inc-n (var)
    `(setq ,var (+ y ,var)))

那么create-inc-n應(yīng)該怎么定義呢?事實(shí)上,

(defmacro create-inc-n (num)
    `(defmacro inc-n (var)
        `(setq ,var (+ ,',num ,var))))

第一次看到,',num的時(shí)候,我非常驚訝,這到底是什么?

4. 嵌套反引用

嵌套反引用指的是,一個(gè)反引用表達(dá)式中嵌套出現(xiàn)了另一個(gè)反引用表達(dá)式。
在生成宏定義的宏中,嵌套反引用經(jīng)常出現(xiàn)。

嵌套反引用表達(dá)式中,經(jīng)常會出現(xiàn)類似,',num這樣的表達(dá)式,
它不能被寫成,num,也不能被寫成,,num,下面我們進(jìn)行仔細(xì)的分析。

(1),num為什么不正確

先看一下展開關(guān)系,我們期望將(create-inc-n y)展開為,

(defmacro inc-n (var)
    `(setq ,var (+ y ,var)))

即,嵌套反引用表達(dá)式,應(yīng)該按下述方式求值,

`(defmacro inc-n (var)
    `(setq ,var (+ ,',num ,var))))

=> (defmacro inc-n (var)
    `(setq ,var (+ y ,var)))

其中,,var是不應(yīng)該被求值的,因?yàn)檫@是內(nèi)層反引用需要的,
如果我們將,',num寫成,num,那么它就和,var一樣不會被求值了,

`(defmacro inc-n (var)
    `(setq ,var (+ ,num ,var))))

=> (defmacro inc-n (var)
    `(setq ,var (+ ,num ,var)))

這和我們期望的展開關(guān)系不同。

(2),,num為什么不正確

寫成,,num在求值最外層反引用表達(dá)式的時(shí)候,確實(shí)會求值num的值,
但是,在求值內(nèi)層反引用表達(dá)式的時(shí)候,這個(gè)值還會被再求值一次。

(create-inc-n y)將被展開為,

`(defmacro inc-n (var)
    `(setq ,var (+ ,,num ,var)))

=> (defmacro inc-n (var)
    `(setq ,var (+ ,y ,var)))

可是,在進(jìn)行宏調(diào)用(create-inc-n y)的時(shí)候,我們不應(yīng)該關(guān)心y的值是什么,
因?yàn)樵诤暾归_階段,y可能還沒有值。

而且,該展開式和我們預(yù)期的展開結(jié)果也不相同。

(3),',num是怎么來的

綜上分析,我們需要在外層反引用表達(dá)式被求值的時(shí)候,求值num,
而在內(nèi)層反引用表達(dá)式被求值的時(shí)候,不再繼續(xù)求值num的值,
因此,我們需要給num的值加上一個(gè)引用來“阻止”求值。

因此,(create-inc-n y)會被展開為,

`(defmacro inc-n (var)
    `(setq ,var (+ ,',num ,var))))

=> (defmacro inc-n (var)
    `(setq ,var (+ ,'y ,var)))

而內(nèi)層反引用表達(dá)式被求值的時(shí)候,,'y將求值為y。

所以,(inc-n x)將被展開為

`(setq ,var (+ ,'y ,var))
=> (setq x (+ y x))

和我們期望的展開結(jié)果相同。

5. 嵌套反引用的求值規(guī)則

在生成宏定義的宏中,經(jīng)常會出現(xiàn)嵌套反引用,
如果我們定義了另一個(gè)宏other-macro來生成create-inc-n的定義,

(defmacro other-macro ()
    `(defmacro create-inc-n (num)
        `(defmacro inc-n (var)
            `(setq ,var (+ ,',num ,var)))))

那么,將出現(xiàn)三層嵌套反引用。
不過,不用擔(dān)心,嵌套反引用也是有求值規(guī)則的,以下我們用兩層嵌套反引用作為例子來說明。

求值規(guī)則:
(1)嵌套反引用被求值的時(shí)候,一次求值,只去掉一層反引用,內(nèi)層反引用不受影響,

`(defmacro inc-n (var)
    `(setq ,var (+ ,',num ,var))))

=> (defmacro inc-n (var)
    `(setq ,var (+ ,'y ,var)))

(2)嵌套反引用表達(dá)式中的逗號表達(dá)式,是否被求值,要根據(jù)情況來定,
如果最外層嵌套反引用總共有n層,那么一定不會出現(xiàn)包含大于n個(gè)逗號的表達(dá)式,
且包含逗號數(shù)目小于n的表達(dá)式不會被求值,只有逗號數(shù)目等于n的表達(dá)式才會被求值。

`(defmacro inc-n (var)
    `(setq ,var (+ ,',num ,var))))

=> (defmacro inc-n (var)
    `(setq ,var (+ ,'y ,var)))

最外層嵌套反引用總共有n=2層,
,var表達(dá)式包含一個(gè)逗號,1<n,不會被求值,
,',num表達(dá)式包含兩個(gè)逗號,2=n,會被求值。

(3)被求值的逗號表達(dá)式,其求值方式是,
去掉最右邊的一個(gè)逗號,然后將表達(dá)式替換成它的值。

`(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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容