在 Lisp 表達(dá)式的一生中,有三個(gè)最重要的時(shí)刻,分別是讀取期(read-time),編譯期(compile-time) 和運(yùn)行期(runtime)。運(yùn)行期由函數(shù)左右。宏給了我們在編譯期對程序做轉(zhuǎn)換的機(jī)會。本章討論讀取宏(read-macro),它們在讀取期發(fā)揮作用。
按照 Lisp 的一般哲學(xué),你可以在很大程度上控制?reader
?。它的行為是由那些可隨時(shí)改變的屬性和變量控制的。Reader 可以在幾個(gè)層面上編程。若要改變其行為,最簡單的方式就是定義新的宏字符。
宏字符(macro character) 是一種被 Lisp?reader
?特殊對待的字符。舉個(gè)例子,小寫字母?a
?的處理方式和小寫字母?b
?是一樣的,它們都由常規(guī)的處理方式處理。但左括號就有些不同:它告訴 Lisp 開始讀取一個(gè)列表。
每個(gè)這樣的字符都有一個(gè)與之關(guān)聯(lián)的函數(shù),告訴 Lisp?reader
?當(dāng)遇到該字符的時(shí)候做什么。你可以改變一個(gè)已有的宏字符的關(guān)聯(lián)函數(shù),或者定義你自己的新的宏字符。
內(nèi)置函數(shù)?set-macro-character
?提供了一種定義讀取宏的方式。它接受一個(gè)字符和一個(gè)函數(shù),以后當(dāng)?read
?遇到這個(gè)字符時(shí),它就返回調(diào)用該函數(shù)的結(jié)果。
[示例代碼 17.1] '(引號)的可能定義
(set-macro-character #\'
#'(lambda (stream char)
(declare (ignore char))
(list 'quote (read stream t nil t))))
Lisp 中最古老的讀取宏之一是單引號?'
?,即引用。你也可以不用?'
,而總是將?'a
?寫成?(quote a)
,但這將會非常煩人, 而且會降低代碼的可讀性。引用讀取宏使?(quote a)
?可以簡寫成?'a
。我們可以用 [示例代碼 17.1] 中的方法實(shí)現(xiàn)它。當(dāng)?read
?在一個(gè)普通的上下文中(例如,不在?"a'b"
或?|a'b|
?中) 遇到?'
?時(shí),它將返回在當(dāng)前流和字符上調(diào)用這個(gè)函數(shù)的結(jié)果。(該函數(shù)忽略了它的第二個(gè)形參,因?yàn)樗偸悄莻€(gè)引用字符。) 所以當(dāng)?read
?看到?'a
?時(shí),它將返回?(quote a)
。
read
?的最后三個(gè)參數(shù)分別控制:是否在碰到?end-of-file
?時(shí)報(bào)錯,如果不報(bào)錯的話返回什么值,以及這個(gè)?read
?調(diào)用是否是發(fā)生在?read
?調(diào)用中的(譯者注:關(guān)于?read
?的最后一個(gè)參數(shù)(recursive-p),詳見?CLTL?中對?read
?的解釋。) 。在幾乎所有的讀取宏里,第二和第四個(gè)參數(shù)都應(yīng)該是?t
?,所以第三個(gè)參數(shù)也就無關(guān)緊要了。
讀取宏和常規(guī)宏一樣,其實(shí)質(zhì)都是函數(shù)。和生成宏展開的函數(shù)一樣,和宏字符相關(guān)的函數(shù),除了作用于它讀取的流以外,不應(yīng)該再有其他副作用。Common Lisp 明確聲明:一個(gè)與宏字符相關(guān)聯(lián)的函數(shù)何時(shí)被執(zhí)行,或者被執(zhí)行幾次 Common Lisp 對其將不給予保證。(見?CLTL2?的 543 頁。)
宏和讀取宏在不同的階段分析和觀察你的程序。宏在程序中發(fā)生作用時(shí),它已經(jīng)被 reader 解析成了 Lisp 對象,而讀取宏在程序還是文本的階段時(shí),就對它施加影響了。盡管如此,通過在這些文本上調(diào)用 read ,一個(gè)讀取宏,如果它愿意的話,同樣可以得到解析后的 Lisp 對象。這樣說來,讀取宏至少和常規(guī)宏一樣強(qiáng)有力。
事實(shí)上,讀取宏至少在兩方面比常規(guī)宏更為強(qiáng)大。讀取宏可以影響 Lisp 讀取的每一樣?xùn)|西,而宏只是在代碼里被展開。并且,由于讀取宏通常遞歸地調(diào)用 read,一個(gè)類似:
''a
的表達(dá)式將變成:
(quote (quote a))
而如果我們試圖用一個(gè)普通的宏來為?quote
?定義縮略語的話:
(defmacro q (obj)
'(quote ,obj))
它在某些情況下可以正常工作:
> (eq 'a (q a))
T
但在被嵌套使用時(shí)就不行了。例如:
(q (q a))
將展開成:
(quote (q a))
譯者注:解決這個(gè)問題的正確方法是定義一個(gè)編譯器宏(compiler-macro)。Common Lisp 內(nèi)置的?define-compiler-macro
?用于定義編譯器宏,詳見?CLTL??? 中關(guān)于此操作符的說明。
dispatching
?宏字符#'
?和其他?#
?開頭的讀取宏一樣,是一種稱為?dispatching
?讀取宏的實(shí)例。這些讀取宏以兩個(gè)字符出現(xiàn),其中第一個(gè)字符稱為?dispatch
?字符。這類宏的目的,簡單說就是盡可能地充分利用?? ?? 字符集;如果只有單字符讀取宏的話,那么讀取宏的數(shù)量就會受限于字符集的大小。
你可以(通過使用?make-dispatch-macro-character
) 來定義你自己的?dispatching
?宏字符,但由于?#
?已經(jīng)定義了,所以你也可以直接用它。一些?#
?打頭的組合就是特意為你保留的;其他的那些,如果 Common Lisp 還沒有給它們賦予含義的話,也可以拿來用。完整的列表可見?CLTL2?的第 531 頁。
[示例代碼17.2] 一個(gè)用于常數(shù)函數(shù)的讀取宏
(set-dispatch-macro-character #\# #\?
#'(lambda (stream char1 char2)
(declare (ignore char1 char2))
'#'(lambda (&rest ,(gensym))
,(read stream t nil t))))
新的?dispatching
?宏字符組合可以通過調(diào)用?set-dispatch-macro-character
?函數(shù)定義,除了接受兩個(gè)字符參數(shù)以外和?set-macro-character
?的用法差不多。一個(gè)預(yù)留給程序員的組合是?#?
?。[示例代碼 17.2] 顯示了如何將這個(gè)組合定義成一個(gè)用于常數(shù)函數(shù)的讀取宏?,F(xiàn)在?#?2
?將被讀取為一個(gè)函數(shù),其接受任意數(shù)量的參數(shù),并且返回?2
。例如:
> (mapcar #?2 '(a b c))
(2 2 2)
這個(gè)例子里定義的新操作符看起來相當(dāng)無聊,但在使用了很多函數(shù)型參數(shù)的程序里,常常會用到常數(shù)函數(shù)。
事實(shí)上,有些方言提供了一個(gè)名叫?always
?的內(nèi)置函數(shù),專門用來定義它們。
注意到在這個(gè)宏字符的定義中使用宏字符是完全沒有問題的:和任何 Lisp 表達(dá)式一樣,當(dāng)這個(gè)定義被讀取以后這些宏字符就都消失了。在?#?
?的后面使用宏字符也是可以的。因?yàn)?#?
?的定義調(diào)用了read
?,所以諸如?'
?和?#'
?此類宏字符也可以正常使用:
> (eq (funcall #?'a) 'a)
T
> (eq (funcall #?#'oddp) (symbol-function 'oddp))
T
[示例代碼 17.3] 一個(gè)定義定界符的讀取宏
(set-macro-character #\] (get-macro-character #\)))
(set-dispatch-macro-character #\# #\[
#'(lambda (stream char1 char2)
(declare (ignore char1 char2))
(let ((accum nil)
(pair (read-delimited-list #\] stream t)))
(do ((i (ceiling (car pair)) (1+ i)))
((> i (floor (cadr pair)))
(list 'quote (nreverse accum)))
(push i accum)))))
除了簡單的宏字符,定義得最多的宏字符要算列表定界符了。另一個(gè)為用戶預(yù)留的組合字符是?#[
?。[示例代碼 17.3] 給出的例子,顯示了把這個(gè)字符定義成一個(gè)更復(fù)雜的左括號的方法。它定義形如?#[x y]
?的表達(dá)式,使得這樣的表達(dá)式被讀取為在?x
?到?y
?的閉區(qū)間上所有整數(shù)的列表:
> #[2 7]
(2 3 4 5 6 7)
這個(gè)讀取宏里,唯一的新東西是對?read-delimited-list
?的調(diào)用,這個(gè)函數(shù)是一個(gè)完全為這種情況度身定制的內(nèi)置函數(shù)。它的第一個(gè)參數(shù)是那個(gè)被當(dāng)作列表結(jié)尾的字符。有其名才能行其實(shí),為了把]
?識別成定界符,程序在開始的地方調(diào)用了?set-macro-character
。
[示例代碼17.4] 一個(gè)用于定義定界符讀取宏的宏
(defmacro defdelim (left right parms &body body)
'(ddfn ,left ,right #'(lambda ,parms ,@body)))
(let ((rpar (get-macro-character #\))))
(defun ddfn (left right fn)
(set-macro-character right rpar)
(set-dispatch-macro-character #\# left
#'(lambda (stream char1 char2)
(declare (ignore char1 char2))
(apply fn
(read-delimited-list right stream t))))))
多數(shù)潛在的定界符讀取宏都將在很大程度上重復(fù) [示例代碼 17.3] 中的代碼?;蛟S可以寫個(gè)宏,讓它從這些機(jī)制中提煉出更抽象的接口,以簡化代碼。[ 示例代碼 17.4] 就是一個(gè)實(shí)現(xiàn),我們可以像它那樣定義一個(gè)實(shí)用工具,用其定義定界符讀取宏。宏?defdelim
?接受兩個(gè)字符,一個(gè)參數(shù)列表,以及一個(gè)代碼主體。參數(shù)列表和代碼主體隱式地定義了一個(gè)函數(shù)。一個(gè)對 defdelim 的調(diào)用將首個(gè)字符定義為?dispatching
?讀取宏,它讀取到第二個(gè)字符為止,然后將這個(gè)函數(shù)應(yīng)用到它讀到的東西,并返回其結(jié)果。
無獨(dú)有偶,[示例代碼 17.3] 中的函數(shù)體也迫切需要一個(gè)實(shí)用工具,事實(shí)上,這個(gè)實(shí)用工具已經(jīng)定義過了:見 4.5 節(jié)的?mapa-b
?。使用?defdelim
?和?mapa-b
?,[示例代碼 17.3] 中定義的讀取宏現(xiàn)在只需寫成:
(defdelim #\[ #\] (x y)
(list 'quote (mapa-b #'identity (ceiling x) (floor y))))
定界符讀取宏也可以用來做函數(shù)復(fù)合。第5.4 節(jié)定義了一個(gè)用于函數(shù)復(fù)合的操作符:
> (let ((f1 (compose #'list #'1+))
(f2 #'(lambda (x) (list (1+ x)))))
(equal (funcall f1 7) (funcall f2 7)))
T
當(dāng)我們復(fù)合像?list
?和?1+
?這樣的內(nèi)置函數(shù)時(shí),沒有理由等到運(yùn)行期才去對 compose 的調(diào)用求值。第 5.7 節(jié)建議一個(gè)替代方案;通過給一個(gè)?compose
?表達(dá)式前綴?sharp-dot
?讀取宏:
#.(compose #'list #'1+)
我們可以令其在讀取期就被求值。
[示例代碼 17.5]:一個(gè)用于函數(shù)型復(fù)合的讀取宏
(defdelim #\{ #\} (&rest args)
'(fn (compose ,@args)))
這里我們給出一個(gè)與之類似但更清晰的解決方案。[示例代碼 17.5] 中定義的讀取宏定義了一個(gè)?#{ }
形式的表達(dá)式,這個(gè)表達(dá)式將被讀取成 的復(fù)合。這樣:
> (funcall #{list 1+} 7)
(8)
它生成一個(gè)對?fn
?(15.1 節(jié)) 的調(diào)用,該調(diào)用在編譯期創(chuàng)建函數(shù)。
最后,澄清一個(gè)可能造成困惑的問題應(yīng)該會有所幫助。如果讀取宏是在常規(guī)宏之前作用的話,那么宏是怎樣展開成含有讀取宏的表達(dá)式的呢?例如,這個(gè)宏:
(defmacro quotable ()
'(list 'able))
會生成一個(gè)帶有引用的展開式。還是說它沒有生成?事實(shí)上,真相是:這個(gè)宏定義中的兩個(gè)引用在這個(gè)?defmacro
?表達(dá)式被讀取時(shí),就都被展開了,展開結(jié)果如下
(defmacro quotable ()
(quote (list (quote able))))
通常,在宏展開式里包含讀取宏是沒有什么問題的。因?yàn)橐粋€(gè)讀取宏的定義在讀取期和編譯期之間將不會(或者說不應(yīng)該) 發(fā)生變化。
更多建議: