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