數(shù)據(jù)和代碼
如果說Lisp語言有一個(gè)特性最能使人津津樂道的話,我想應(yīng)該是它的宏系統(tǒng)(macro system)了吧,
在Lisp語言中,程序和代碼的表現(xiàn)形式(textual representation)幾乎一致,造就了它無與倫比的元編程能力。
這種對(duì)稱性,使得Lisp語言可以像處理數(shù)據(jù)一樣優(yōu)雅的處理代碼本身。
并且和其他語言不同的是,Lisp的宏系統(tǒng),并不是簡(jiǎn)單的文本操作,
而是建立在語法對(duì)象(syntax object)基礎(chǔ)之上。
前文提到過,我們直接寫(foo bar bar)
表示函數(shù)調(diào)用或者宏調(diào)用(macro call),
加引用'(foo bar bar)
表示列表字面量,
直接寫x
表示變量或者函數(shù),加引用'x
表示符號(hào)(symbol)。
如果我們把列表字面量和符號(hào)看做數(shù)據(jù),把變量和函數(shù)調(diào)用看做程序,
那么數(shù)據(jù)和程序的表現(xiàn)形式(textual representation)幾乎是相同的,只差一個(gè)引用。
所以,如果一個(gè)函數(shù)能夠處理數(shù)據(jù)(列表/變量),那么它也一定能夠處理被引用的程序,
同理,如果一個(gè)函數(shù)能夠返回一段數(shù)據(jù)(列表/變量),那么去掉引用之后(使用eval
),
也可以看做它是返回了一段程序。
例如,
(defun inc (var)
(list 'setq var (list '1+ var)))
(inc 'x) ; (setq x (1+ x))
我們定義了一個(gè)inc
函數(shù),它接受var
作為參數(shù),返回了一個(gè)列表。
即,(inc 'x)
的求值結(jié)果為(setq x (1+ x))
,
其中,(setq x (1+ x))
是一個(gè)列表。
我們可以通過eval
直接把返回的列表當(dāng)做程序來執(zhí)行,
(defvar x 0)
(eval (inc 'x))
x ; 1
x
的值被修改了,變成了1
。
定義一個(gè)宏
我們只需要將上文的inc
稍作修改,就可以把它轉(zhuǎn)換成一個(gè)宏(macro),
我們只需要將defun
改成defmacro
即可,
(defmacro inc (var)
(list 'setq var (list '1+ var)))
現(xiàn)在inc
就是一個(gè)宏(macro)了,它的使用方式和函數(shù)非常相似,
(defvar x 0)
(inc x)
x ; 1
我們看到,這里直接使用了(inc x)
,而不是(inc 'x)
,
并且,(inc x)
的作用和直接寫程序(setq x (1+ x))
是一樣的。
(inc x)
我們稱之為宏調(diào)用(macro call),
而(setq x (1+ x))
我們稱之為宏展開(macro expansion)后的程序。
編譯器或者解釋器會(huì)采用不同的策略進(jìn)行宏展開,
一般而言,編譯器會(huì)在求值程序之前,將代碼中所有的宏(macro)進(jìn)行展開,
即,將所有的宏調(diào)用(inc x)
,替換成它返回的那段程序(setq x (1+ x))
,
直到代碼中不再包含宏(macro)為止,然后再進(jìn)行編譯。
一個(gè)簡(jiǎn)單的解釋器實(shí)現(xiàn),可能會(huì)一邊執(zhí)行程序一邊進(jìn)行宏展開操作,
它會(huì)在運(yùn)行時(shí),通過判斷符號(hào)(symbol)的類型,來決定進(jìn)行函數(shù)調(diào)用還是宏調(diào)用。
這樣可能會(huì)有助于理解宏的遞歸展開問題。
一個(gè)宏展開式中,可能還會(huì)包含其它的宏,也可能還會(huì)包含另一個(gè)宏的定義。
(以后的文章中,我們會(huì)介紹)
因此,在宏定義中,進(jìn)行的具有副作用(side effect)的操作,
其執(zhí)行時(shí)機(jī)并不是在運(yùn)行時(shí),而是在宏展開階段,
而如果宏實(shí)參中包含了帶有副作用的操作,那么它可能被展開到源代碼中的多個(gè)位置,
從而被執(zhí)行多次。
語法對(duì)象
在Emacs Lisp中,宏變量inc
實(shí)際上是一個(gè)轉(zhuǎn)換函數(shù),
它將var
轉(zhuǎn)換成了(list 'setq var (list '1+ var))
,即把符號(hào)(symbol)轉(zhuǎn)換成了一個(gè)列表對(duì)象。
宏變量的值與函數(shù)一樣會(huì)保存在符號(hào)(symbol)inc
的function cell中,
因此,一個(gè)符號(hào)(symbol)不可能既表示一個(gè)函數(shù)又表示一個(gè)宏(macro)。
當(dāng)Lisp解釋器遇到一個(gè)符號(hào)(symbol)的時(shí)候,
會(huì)判斷它到底是一個(gè)變量,一個(gè)函數(shù)還是一個(gè)宏(macro)。
(defun add1 (x)
(+ x 1))
(defvar a 1)
(add1 a)
如果是一個(gè)函數(shù),且當(dāng)前進(jìn)行的是函數(shù)調(diào)用(add1 a)
,
那么就會(huì)先求值它的實(shí)參,a
求值為1
,
再將add1
的形參x
綁定為實(shí)參的值1
,再求值函數(shù)體,
即,求值(+ x 1)
,結(jié)果為2
。
(defmacro inc (var)
(list 'setq var (list '1+ var)))
(defvar x 0)
(inc x)
x ; 1
如果是一個(gè)宏(macro),且當(dāng)前進(jìn)行的是宏調(diào)用(inc x)
,
那么它并不會(huì)像函數(shù)那樣先求值函數(shù)體,而是直接將宏形參綁定為宏調(diào)用的實(shí)參值。
即,var
綁定為符號(hào)(symbol)x
。
值得注意的是,宏調(diào)用的實(shí)參,是一個(gè)符號(hào)(symbol),它是一個(gè)Lisp對(duì)象,而不是一個(gè)字符串,
宏(macro)所返回的結(jié)果,也是一個(gè)Lisp對(duì)象。
更明確的說,宏(macro)是一個(gè)針對(duì)語法對(duì)象(syntax object)的變換函數(shù),
它對(duì)讀取器獲得的語法對(duì)象(syntax object)進(jìn)行變換。
在某些Lisp方言,例如Scheme,這些語法對(duì)象(syntax object)包含了上下文信息,使用它們可以編寫出強(qiáng)大而靈活的宏(macro)。
這里容易引起混亂的是,在Emacs Lisp中,直接使用了符號(hào)和列表表示了語法對(duì)象,
而實(shí)際上語法對(duì)象是一個(gè)數(shù)據(jù)結(jié)構(gòu),在其內(nèi)部包含了符號(hào)和列表的信息。
這樣做的好處是,在宏展開階段宏(macro)接受和返回的都是語法對(duì)象,
而在運(yùn)行時(shí)階段,處理的都是運(yùn)行時(shí)對(duì)象了。
(例如:syntax->datum和datum->syntax)
通過以下程序我們可以驗(yàn)證,var
確實(shí)是一個(gè)符號(hào)(symbol)。
(defmacro inc (var)
(message "%s" (symbolp var)) ; t
(list 'setq var (list '1+ var)))
我們之前十分小心的區(qū)分了標(biāo)識(shí)符,符號(hào)(symbol)和變量,
是為了在類似這樣的場(chǎng)景中保持清醒。
標(biāo)識(shí)符經(jīng)過Lisp讀取器,在Lisp內(nèi)部會(huì)變成一個(gè)符號(hào)(symbol),它是一個(gè)語法對(duì)象,
然后Lisp會(huì)對(duì)所有的宏(macro)進(jìn)行展開,將這些語法對(duì)象綁定到宏形參上,對(duì)語法對(duì)象進(jìn)行變換。
最后,求值器在運(yùn)行時(shí)會(huì)求值這些符號(hào)(symbol),得到一個(gè)變量值或者函數(shù)值。
因此,編寫宏(macro)可以看作是對(duì)編譯器或者解釋器進(jìn)行編程,
Lisp允許用戶在表達(dá)式被求值之前對(duì)它進(jìn)行一些變換。
總結(jié)
本文初步介紹了Lisp的宏系統(tǒng),展示了宏調(diào)用與函數(shù)調(diào)用之間的異同,
我們發(fā)現(xiàn)Lisp的宏系統(tǒng)是建立在語法對(duì)象(syntax object)基礎(chǔ)之上的,而不是簡(jiǎn)單的進(jìn)行文本替換。
此外,由于Emacs Lisp的宏(macro)不是衛(wèi)生的(hygienic),所以會(huì)和Common Lisp一樣出現(xiàn)變量捕獲問題。
下文我們開始介紹一些Lisp宏的常見陷阱和用法。
參考
GNU Emacs Lisp Reference Manual
Chez Scheme Version 8 User's Guide
An Introduction to Scheme and its Implementation