第 17 章 讀取宏(read-macro)

2018-02-24 15:54 更新

第 17 章 讀取宏(read-macro)

在 Lisp 表達(dá)式的一生中,有三個(gè)最重要的時(shí)刻,分別是讀取期(read-time),編譯期(compile-time) 和運(yùn)行期(runtime)。運(yùn)行期由函數(shù)左右。宏給了我們?cè)诰幾g期對(duì)程序做轉(zhuǎn)換的機(jī)會(huì)。本章討論讀取宏(read-macro),它們?cè)谧x取期發(fā)揮作用。

17.1 宏字符

按照 Lisp 的一般哲學(xué),你可以在很大程度上控制?reader?。它的行為是由那些可隨時(shí)改變的屬性和變量控制的。Reader 可以在幾個(gè)層面上編程。若要改變其行為,最簡(jiǎn)單的方式就是定義新的宏字符。

宏字符(macro character) 是一種被 Lisp?reader?特殊對(duì)待的字符。舉個(gè)例子,小寫(xiě)字母?a?的處理方式和小寫(xiě)字母?b?是一樣的,它們都由常規(guī)的處理方式處理。但左括號(hào)就有些不同:它告訴 Lisp 開(kāi)始讀取一個(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] '(引號(hào))的可能定義

(set-macro-character #\'
  #'(lambda (stream char)
    (declare (ignore char))
    (list 'quote (read stream t nil t))))

Lisp 中最古老的讀取宏之一是單引號(hào)?'?,即引用。你也可以不用?',而總是將?'a?寫(xiě)成?(quote a),但這將會(huì)非常煩人, 而且會(huì)降低代碼的可讀性。引用讀取宏使?(quote a)?可以簡(jiǎn)寫(xiě)成?'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)錯(cuò),如果不報(bào)錯(cuò)的話(huà)返回什么值,以及這個(gè)?read?調(diào)用是否是發(fā)生在?read?調(diào)用中的(譯者注:關(guān)于?read?的最后一個(gè)參數(shù)(recursive-p),詳見(jiàn)?CLTL?中對(duì)?read?的解釋。) 。在幾乎所有的讀取宏里,第二和第四個(gè)參數(shù)都應(yīng)該是?t?,所以第三個(gè)參數(shù)也就無(wú)關(guān)緊要了。

讀取宏和常規(guī)宏一樣,其實(shí)質(zhì)都是函數(shù)。和生成宏展開(kāi)的函數(shù)一樣,和宏字符相關(guān)的函數(shù),除了作用于它讀取的流以外,不應(yīng)該再有其他副作用。Common Lisp 明確聲明:一個(gè)與宏字符相關(guān)聯(lián)的函數(shù)何時(shí)被執(zhí)行,或者被執(zhí)行幾次 Common Lisp 對(duì)其將不給予保證。(見(jiàn)?CLTL2?的 543 頁(yè)。)

宏和讀取宏在不同的階段分析和觀(guān)察你的程序。宏在程序中發(fā)生作用時(shí),它已經(jīng)被 reader 解析成了 Lisp 對(duì)象,而讀取宏在程序還是文本的階段時(shí),就對(duì)它施加影響了。盡管如此,通過(guò)在這些文本上調(diào)用 read ,一個(gè)讀取宏,如果它愿意的話(huà),同樣可以得到解析后的 Lisp 對(duì)象。這樣說(shuō)來(lái),讀取宏至少和常規(guī)宏一樣強(qiáng)有力。

事實(shí)上,讀取宏至少在兩方面比常規(guī)宏更為強(qiáng)大。讀取宏可以影響 Lisp 讀取的每一樣?xùn)|西,而宏只是在代碼里被展開(kāi)。并且,由于讀取宏通常遞歸地調(diào)用 read,一個(gè)類(lèi)似:

''a

的表達(dá)式將變成:

(quote (quote a))

而如果我們?cè)噲D用一個(gè)普通的宏來(lái)為?quote?定義縮略語(yǔ)的話(huà):

(defmacro q (obj)
  '(quote ,obj))

它在某些情況下可以正常工作:

> (eq 'a (q a))
T

但在被嵌套使用時(shí)就不行了。例如:

(q (q a))

將展開(kāi)成:

(quote (q a))

譯者注:解決這個(gè)問(wèn)題的正確方法是定義一個(gè)編譯器宏(compiler-macro)。Common Lisp 內(nèi)置的?define-compiler-macro?用于定義編譯器宏,詳見(jiàn)?CLTL??? 中關(guān)于此操作符的說(shuō)明。

17.2?dispatching?宏字符

#'?和其他?#?開(kāi)頭的讀取宏一樣,是一種稱(chēng)為?dispatching?讀取宏的實(shí)例。這些讀取宏以?xún)蓚€(gè)字符出現(xiàn),其中第一個(gè)字符稱(chēng)為?dispatch?字符。這類(lèi)宏的目的,簡(jiǎn)單說(shuō)就是盡可能地充分利用?? ?? 字符集;如果只有單字符讀取宏的話(huà),那么讀取宏的數(shù)量就會(huì)受限于字符集的大小。

你可以(通過(guò)使用?make-dispatch-macro-character) 來(lái)定義你自己的?dispatching?宏字符,但由于?#?已經(jīng)定義了,所以你也可以直接用它。一些?#?打頭的組合就是特意為你保留的;其他的那些,如果 Common Lisp 還沒(méi)有給它們賦予含義的話(huà),也可以拿來(lái)用。完整的列表可見(jiàn)?CLTL2?的第 531 頁(yè)。


[示例代碼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?宏字符組合可以通過(guò)調(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è)例子里定義的新操作符看起來(lái)相當(dāng)無(wú)聊,但在使用了很多函數(shù)型參數(shù)的程序里,常常會(huì)用到常數(shù)函數(shù)。

事實(shí)上,有些方言提供了一個(gè)名叫?always?的內(nèi)置函數(shù),專(zhuān)門(mén)用來(lái)定義它們。

注意到在這個(gè)宏字符的定義中使用宏字符是完全沒(méi)有問(wèn)題的:和任何 Lisp 表達(dá)式一樣,當(dāng)這個(gè)定義被讀取以后這些宏字符就都消失了。在?#??的后面使用宏字符也是可以的。因?yàn)?#??的定義調(diào)用了read?,所以諸如?'?和?#'?此類(lèi)宏字符也可以正常使用:

> (eq (funcall #?'a) 'a)
T
> (eq (funcall #?#'oddp) (symbol-function 'oddp))
T

17.3 定界符


[示例代碼 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)))))

除了簡(jiǎn)單的宏字符,定義得最多的宏字符要算列表定界符了。另一個(gè)為用戶(hù)預(yù)留的組合字符是?#[?。[示例代碼 17.3] 給出的例子,顯示了把這個(gè)字符定義成一個(gè)更復(fù)雜的左括號(hào)的方法。它定義形如?#[x y]?的表達(dá)式,使得這樣的表達(dá)式被讀取為在?x?到?y?的閉區(qū)間上所有整數(shù)的列表:

> #[2 7]
(2 3 4 5 6 7)

這個(gè)讀取宏里,唯一的新東西是對(duì)?read-delimited-list?的調(diào)用,這個(gè)函數(shù)是一個(gè)完全為這種情況度身定制的內(nèi)置函數(shù)。它的第一個(gè)參數(shù)是那個(gè)被當(dāng)作列表結(jié)尾的字符。有其名才能行其實(shí),為了把]?識(shí)別成定界符,程序在開(kāi)始的地方調(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] 中的代碼。或許可以寫(xiě)個(gè)宏,讓它從這些機(jī)制中提煉出更抽象的接口,以簡(jiǎn)化代碼。[ 示例代碼 17.4] 就是一個(gè)實(shí)現(xiàn),我們可以像它那樣定義一個(gè)實(shí)用工具,用其定義定界符讀取宏。宏?defdelim?接受兩個(gè)字符,一個(gè)參數(shù)列表,以及一個(gè)代碼主體。參數(shù)列表和代碼主體隱式地定義了一個(gè)函數(shù)。一個(gè)對(duì) defdelim 的調(diào)用將首個(gè)字符定義為?dispatching?讀取宏,它讀取到第二個(gè)字符為止,然后將這個(gè)函數(shù)應(yīng)用到它讀到的東西,并返回其結(jié)果。

無(wú)獨(dú)有偶,[示例代碼 17.3] 中的函數(shù)體也迫切需要一個(gè)實(shí)用工具,事實(shí)上,這個(gè)實(shí)用工具已經(jīng)定義過(guò)了:見(jiàn) 4.5 節(jié)的?mapa-b?。使用?defdelim?和?mapa-b?,[示例代碼 17.3] 中定義的讀取宏現(xiàn)在只需寫(xiě)成:

(defdelim #\[ #\] (x y)
  (list 'quote (mapa-b #'identity (ceiling x) (floor y))))

定界符讀取宏也可以用來(lái)做函數(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í),沒(méi)有理由等到運(yùn)行期才去對(duì) compose 的調(diào)用求值。第 5.7 節(jié)建議一個(gè)替代方案;通過(guò)給一個(gè)?compose?表達(dá)式前綴?sharp-dot?讀取宏:

#.(compose  #'list  #'1+)

我們可以令其在讀取期就被求值。


[示例代碼 17.5]:一個(gè)用于函數(shù)型復(fù)合的讀取宏

(defdelim  #\{ #\}  (&rest args)
  '(fn  (compose  ,@args)))

這里我們給出一個(gè)與之類(lèi)似但更清晰的解決方案。[示例代碼 17.5] 中定義的讀取宏定義了一個(gè)?#{ }形式的表達(dá)式,這個(gè)表達(dá)式將被讀取成 的復(fù)合。這樣:

> (funcall #{list 1+} 7)
(8)

它生成一個(gè)對(duì)?fn?(15.1 節(jié)) 的調(diào)用,該調(diào)用在編譯期創(chuàng)建函數(shù)。

17.4 這些發(fā)生于何時(shí)

最后,澄清一個(gè)可能造成困惑的問(wèn)題應(yīng)該會(huì)有所幫助。如果讀取宏是在常規(guī)宏之前作用的話(huà),那么宏是怎樣展開(kāi)成含有讀取宏的表達(dá)式的呢?例如,這個(gè)宏:

(defmacro quotable ()
  '(list 'able))

會(huì)生成一個(gè)帶有引用的展開(kāi)式。還是說(shuō)它沒(méi)有生成?事實(shí)上,真相是:這個(gè)宏定義中的兩個(gè)引用在這個(gè)?defmacro?表達(dá)式被讀取時(shí),就都被展開(kāi)了,展開(kāi)結(jié)果如下

(defmacro quotable ()
  (quote (list (quote able))))

通常,在宏展開(kāi)式里包含讀取宏是沒(méi)有什么問(wèn)題的。因?yàn)橐粋€(gè)讀取宏的定義在讀取期和編譯期之間將不會(huì)(或者說(shuō)不應(yīng)該) 發(fā)生變化。

以上內(nèi)容是否對(duì)您有幫助:
在線(xiàn)筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)