第 16 章 定義宏的宏

2018-02-24 15:54 更新

第 16 章 定義宏的宏

代碼中的模式通常預(yù)示著需要新的抽象。這一規(guī)則對于宏代碼本身也一樣適用。如果幾個宏的定義在形式上比較相似,我們就可能寫一個編寫宏的宏來產(chǎn)生它們。本章展示三個宏定義宏的例子:一個用來定義縮略語,另一個用來定義訪問宏,第三個則用來定義在 14.1 節(jié)中介紹的那種指代宏。

16.1 縮略語

宏最簡單的用法就是作為縮略語。一些 Common Lisp 操作符的名字相當(dāng)之長。它們中最典型的 (盡管不是最長的) 是 destructuring-bind ,長達(dá) 18 個字符。Steele 原則(4.3 節(jié)) 的一個直接推論是,常用的操作符應(yīng)該取個簡短的名字。("我們認(rèn)為加法的成本較低,部分原因是由于我們只要用一個字符 '+' 就可以表示它。") 內(nèi)置的 destructuring-bind 宏引入了一個新的抽象層,但它在簡潔上作出的貢獻(xiàn)被它的長名字抹殺了:

(let ((a (car x)) (b (cdr x))) ...)
(destructuring-bind (a . b) x ...)

和打印出來的文本相似,程序在每行的字符數(shù)不超過 70 的時候,是最容易閱讀的。當(dāng)單個名字的長度達(dá)到這個長度的四分之一時,我們就開始覺得不便了。

幸運(yùn)的是,在像 Lisp 這樣的語言里你完全沒有必要逆來順受設(shè)計(jì)者的每個決定。只要定義了:

(defmacro dbind (&rest args)
  '(destructuring-bind ,@args))

你就再也不沒必要用那個長長的名字了。對于名字更長也更常用的multiple-value-bind 也是一樣的道理。

(defmacro mvbind (&rest args)
  '(multiple-value-bind ,@args))

注意到 dbind 和 mvbind 的定義是何等的相似。確實(shí),使用這種 rest 和逗號-at 的慣用法,就已經(jīng)能為任意一個函數(shù)【注1】、宏,或者?special form?定義其縮略語了。既然我們可以讓一個宏幫我們代勞,為什么還老是照著?mvbind?的模樣寫出一個又一個的定義呢?

為了定義一個定義宏的宏,我們通常會要用到嵌套的反引用。嵌套反引用的難以理解是出了名的。盡管最終我們會對那些常見的情況了如指掌,但你不能指望隨便挑一個反引用表達(dá)式,都能看一眼,就能立即說出它可以產(chǎn)生什么。這不能歸罪于 Lisp。就像一個復(fù)雜的積分,沒人能看一眼就得出積分的結(jié)果,但是我們不能因?yàn)檫@個就把問題歸咎于積分的表示方法。道理是一樣的。難點(diǎn)在于問題本身,而非表示問題的方法。

盡管如此,正如在我們在做積分的時候,我們同樣也可以把對反引用的分析拆成多個小一些的步驟,讓每一步都可以很容易地完成。假設(shè)我們想要寫一個 abbrev 宏,它允許我們僅用:

(abbrev mvbind multiple-value-bind)

[示例代碼 16.1] 自動定義縮略語

(defmacro abbrev (short long)
  '(defmacro ,short (&rest args)
    '(,',long ,@args)))

(defmacro abbrevs (&rest names)
  '(progn
    ,@(mapcar #'(lambda (pair)
        '(abbrev ,@pair))
      (group names 2))))

來定義 mvbind 。[示例代碼 16.1] 給出了一個這個宏的定義。它是怎樣寫出來的呢?這個宏的定義可以從一個示例展開式開始。一個展開式是:

(defmacro mvbind (&rest args)
  '(multiple-value-bind ,@args))

如果我們把 multiple-value-bind 從反引用里拉出來的話,就會讓推導(dǎo)變得更容易些,因?yàn)槲覀冎浪鼘⒊蔀樽罱K要得到的那個宏的參數(shù)。這樣就得到了等價的定義:

(defmacro mvbind (&rest args)
  (let ((name 'multiple-value-bind))
    '(,name ,@args)))

現(xiàn)在我們將這個展開式轉(zhuǎn)化成一個模板。我們先把反引用放在前面,然后把可變的表達(dá)式替換成變量。

'(defmacro ,short (&rest args)
  (let ((name ',long))
    '(,name ,@args)))

最后一步是通過把代表 name 的 ',long 從內(nèi)層反引用中消去,來簡化表達(dá)式:

'(defmacro ,short (&rest args)
  '(,',long ,@args))

這就得到了 [示例代碼 16.1] 中定義的宏的主體。

[示例代碼 16.1] 中還有一個 abbrevs ,它用于我們想要一次性定義多個縮略語的場合.

(abbrevs dbind destructuring-bind
  mvbind multiple-value-bind
  mvsetq multiple-value-setq)

abbrevs 的用戶無需插入多余的括號,因?yàn)?abbrevs 通過調(diào)用 group (4.3 節(jié)) 來將其參數(shù)兩兩分組。對于宏來說,為用戶節(jié)省邏輯上不必要的括號是件好事,而 group 對于多數(shù)這樣的宏來說都是有用的。

16.2 屬性

Lisp 提供多種方式將屬性和對象關(guān)聯(lián)在一起。如果問題中的對象可以表示成符號,那么最便利(盡管可能最低效) 的方式之一是使用符號的屬性表。為了描述對象 -- 具有值為 的屬性 -- 的這一事實(shí),我們修改的屬性表:

(setf (get o p) v)

所以如果說 ball1 的 color 為 red ,我們用:

(setf (get 'ball1 'color) 'red)

如果我們打算經(jīng)常引用對象的某些屬性,我們可以定義一個宏來得到它:

(defmacro color (obj)
  '(get ,obj 'color))

然后在 get 的位置上使用 color 就可以了:

> (color 'ball1)
RED

由于宏調(diào)用對 setf 是透明的(見第 12 章),我們也可以用:

> (setf (color 'ball1) 'green)
GREEN

這種宏會有如下優(yōu)勢:它能把程序表示對象顏色的方式隱藏起來。屬性表的訪問速度比較慢;程序在將來的版本里,可能會出于速度考慮,將顏色表示成結(jié)構(gòu)體的一個字段,或者哈希表中的一個表項(xiàng)。如果通過類似 color 宏這樣的外部接口訪問數(shù)據(jù),我們可以很輕易地對底層代碼做翻天覆地的改動,就算是已經(jīng)成形的程序也不在話下。如果一個程序從屬性表改成用結(jié)構(gòu)體,那么在訪問宏的外部接口以上的程序可以原封不動;甚至使用這個接口的代碼可以根本就對背后的重構(gòu)過程毫無察覺。

對于重量這個屬性,我們可以定義一個宏,它和為 color 寫的那個宏差不多:

(defmacro weight (obj)
  '(get ,obj 'weight))

和上節(jié)的情況相似,color 和 weight 的定義幾乎一模一樣。在這里 propmacro ([示例代碼 16.2]) 扮演了和 abbrev 相同的角色。


[示例代碼 16.2] 自動定義訪問宏

(defmacro propmacro (propname)
  '(defmacro ,propname (obj)
    '(get ,obj ',',propname)))

(defmacro propmacros (&rest props)
  '(progn
    ,@(mapcar #'(lambda (p) '(propmacro ,p)
        props))))

一個用來定義宏的宏可以采用和任何其他宏相同的設(shè)計(jì)過程:先理解宏調(diào)用,然后分析預(yù)期的展開式,再想出來如何將前者轉(zhuǎn)化成后者。我們想要

(propmacro color)

被展開成

(defmacro color (obj)
  '(get ,obj 'color))

盡管這個展開式本身也是一個 defmacro ,我們?nèi)匀荒軌驗(yàn)樗鲆粋€模板,先把它放到反引用里,然后把加了逗號的參數(shù)名放在color 的實(shí)例的位置上。如同前一節(jié)那樣,我們首先通過轉(zhuǎn)化,讓展開式已有的反引 用里面沒有 color 實(shí)例:

(defmacro color (obj)
  (let ((p 'color))
    '(get ,obj ',p)))

然后我們接下來構(gòu)造這個模板:

'(defmacro ,propname (obj)
  (let ((p ',propname))
    '(get ,obj ',p)))

再簡化成:

'(defmacro ,propname (obj)
  '(get ,obj ',',propname))

對于需要把一組屬性名全部定義成宏的場合,還有 propmacros ([示例代碼 16.2]),它展開到一系列單獨(dú)的對 propmacro 的調(diào)用。就像 abbrevs ,這段不長的代碼事實(shí)上是一個定義定義宏的宏的宏。

雖然本章針對的是屬性表,但這里的技術(shù)是通用的。對于以任何形式保存的數(shù)據(jù),我們都可以用它定義適用的數(shù)據(jù)訪問宏。

16.3 指代宏

第14.1節(jié)已經(jīng)給出了幾種指代宏的定義。當(dāng)你使用類似 aif 或者 aand 這樣的宏時,在一些參數(shù)求值的過程中,符號 it 將被綁定到其他參數(shù)返回的值上。所以,無需再用:

(let ((res (complicated-query)))
  (if res
    (foo res)))

只要說

(aif (complicated-query)
  (foo it))

就可以了,而:

(let ((o (owner x)))
  (and o (let ((a (address o)))
      (and a (city a)))))

則可以簡化成:

(aand (owner x) (address it) (city it))

第 14.1 節(jié)給出了七個指代宏:aif ,awhen ,awhile ,acond ,alambda ,ablock 和 aand。這七個絕不是唯一有用的這種類型的指代宏。事實(shí)上,我們可以為任何 Common Lisp 函數(shù)或宏定義出對應(yīng)的指代變形。這些宏中有許多的情況會和 mapcon 很像:很少用到,可一旦需要就是不可替代的。

例如,我們可以定義 a+ ,讓它和 aand 一樣,使 it 總是綁定到上個參數(shù)返回的值上。下面的函數(shù)用來計(jì)算 在Massachusetts 的晚餐開銷:

(defun mass-cost (menu-price)
  (a+ menu-price (* it .05) (* it 3)))

Massachusetts 的餐飲稅是 5%,而顧客經(jīng)常按照這個稅的三倍來計(jì)算小費(fèi)。按照這個公式計(jì)算的話,

在 Dolphin 海鮮餐廳吃烤鱈魚的費(fèi)用共計(jì):

> (mass-cost 7.95)
9.54

不過這里還包括了沙拉和一份烤土豆。


[示例代碼 16.3] a+ 和 alist 的定義

(defmacro a+ (&rest args)
  (a+expand args nil))

(defun a+expand (args syms)
  (if args
    (let ((sym (gensym)))
      '(let* ((,sym ,(car args))
          (it ,sym))
        ,(a+expand (cdr args)
          (append syms (list sym)))))
    '(+ ,@syms)))

(defmacro alist (&rest args)
  (alist-expand args nil))

(defun alist-expand (args syms)
  (if args
    (let ((sym (gensym)))
      '(let* ((,sym ,(car args))
          (it ,sym))
        ,(alist-expand (cdr args)
          (append syms (list sym)))))
    '(list ,@syms)))

[示例代碼 16.3] 中定義的 a+ ,依賴于一個遞歸函數(shù) a+expand ,來生成其展開式。a+expand 的一般策略是對宏調(diào)用中的參數(shù)列表不斷地求 cdr,同時生成一系列嵌套的 let 表達(dá)式;每一個 let 都將 it 綁定到不同的參數(shù)上,但同時也把每個參數(shù)綁定到一個不同的生成符號上。展開函數(shù)聚集出一個這些生成符號的列表,并且當(dāng)?shù)竭_(dá)參數(shù)列表的結(jié)尾時,它就返回一個以這些生成符號作為參數(shù)的+ 表達(dá)式。所以表達(dá)式:

(a+ menu-price (* it .05) (* it 3))

得到了展開式:

(let* ((#:g2 menu-price) (it #:g2))
  (let* ((#:g3 (* it 0.05)) (it #:g3))
    (let* ((#:g4 (* it 3)) (it #:g4))
      (+ #:g2 #:g3 #:g4))))

[示例代碼 16.3] 中還定義了一個類似的 alist :

> (alist 1 (+ 2 it) (+ 2 it))
(1 3 5)

歷史重演了,a+ 和 alist 的定義幾乎完全一樣。如果我們想要定義更多像它們那樣的宏,這些宏也將在很大程度上大同小異。為什么不寫一個程序,讓它幫助我們產(chǎn)生這些宏呢?[示例代碼 16.4] 中的 defanaph 將達(dá)到這個目的。借助defanaph ,宏 a+ 和alist 的定義過程可以簡化成:

(defanaph a+)
(defanaph alist)

這樣定義出的 a+ 和 alist 展開式將和 [示例代碼 16.3] 中的代碼產(chǎn)生的展開式相同。這個用來定義宏的defanaph 宏將為任何其參數(shù)按照正常函數(shù)求值規(guī)則來求值的東西創(chuàng)建出指代變形來。這就是說,defanaph 將適用于任何參數(shù)全部被求值,并且是從左到右求值的東西上。所以你不能用這個版本的 defanaph 來定義 aand 或 awhile ,但你可以用它給任何函數(shù)定義出其指代版本。

正如 a+ 調(diào)用 a+expand 來生成其展開式,defanaph 所定義的宏也調(diào)用 anaphex 來做這個事情。通用展開器 anaphex 跟 a+expand 的唯一不同之處在于其接受作為參數(shù)的函數(shù)名使其出現(xiàn)在最終的展開式里。事實(shí)上,a+ 現(xiàn)在可以定義成:


[示例代碼 16.4] 自動定義指代宏

(defmacro a+ (&rest args)
  (anaphex args '(+)))

(defmacro defanaph (name &optional calls)
  (let ((calls (or calls (pop-symbol name))))
    '(defmacro ,name (&rest args)
      (anaphex args (list ',calls)))))

(defun anaphex (args expr)
  (if args
    (let ((sym (gensym)))
      '(let* ((,sym ,(car args))
          (it ,sym))
        ,(anaphex (cdr args)
          (append expr (list sym)))))
    expr))

(defun pop-symbol (sym)
  (intern (subseq (symbol-name sym) 1)))

無論 anaphex 還是 a+expand 都不需要被定義成單獨(dú)的函數(shù):anaphex 可以用 labels 或 alambda 定義在 defanaph 里面。這里把展開式生成器拆成分開的函數(shù)只是出于澄清的理由。

默認(rèn)情況下,defanaph 通過將其參數(shù)前面的第一個字母(假設(shè)是一個 a ) 拉出來以決定在最后的展開式里調(diào)用什么。(這個操作是由 pop-symbol 完成的。) 如果用戶更喜歡另外指定一個名字,它可以作為一個可選參數(shù)。盡管defanaph 可以為所有函數(shù)和某些宏定義出其 anaphoric 變形,但它有一些令人討厭的局限:

  1. 它只能工作在其參數(shù)全部求值的操作符上。

  2. 在宏展開中,it 總被綁定在前一個參數(shù)上。在某些場合, 例如 awhen 我們想要 it 始終綁在第一個參數(shù)的值上。

  3. 它無法工作在像 setf 這種期望其第一個參數(shù)是廣義變量的宏上。

讓我們考慮一下如何在一定程度上打破這些局限。第一個問題的一部分可以通過解決第二個問題來解決。

為了給類似 aif 的宏生成展開式,我們需要對 anaphex 加以修改,讓它在宏調(diào)用中只替換第一個參數(shù):

(defun anaphex2 (op args)
  '(let ((it ,(car args)))
    (,op it ,@(cdr args))))

這個非遞歸版本的 anaphex 不需要確保宏展開式將 it 綁定到當(dāng)前參數(shù)前面的那個參數(shù)上,所以它可以生成的展開式?jīng)]有必要對宏調(diào)用中的所有參數(shù)求值。只有第一個參數(shù)是必須被求值的,以便將 it 綁定到它的值上。所以 aif 可以被定義成:

(defmacro aif (&rest args)
  (anaphex2 'if args))

這個定義和 14.1 節(jié)上原來的定義相比,唯一的區(qū)別在于: 之前那個版本里,如果你傳給 aif 參數(shù)的個數(shù)不對的話,那程序會報錯;如果調(diào)用宏的方法是正確的話,這兩個版本將生成相同的展開式。

至于第三個問題,也就是 defanaph 無法工作在廣義變量上的問題,可以通過在展開式中使用 _f (12.4 節(jié)) 來解決。像 setf 這樣的操作符可以被下面定義的 anaphex2 的變種來處理:

(defun anaphex3 (op args)
  '(_f (lambda (it) (,op it ,@(cdr args))) ,(car args)))

這個展開器假設(shè)宏調(diào)用必須帶有一個以上的參數(shù),其中第一個參數(shù)將是一個廣義變量。使用它我們可以這樣定義 asetf:【注2】【注3】


[示例代碼 16.5] 更一般的 defanaph

(defmacro asetf (&rest args)
  (anaphex3 '(lambda (x y) (declare (ignore x)) y) args))

(defmacro defanaph (name &key calls (rule :all))
  (let* ((opname (or calls (pop-symbol name)))
      (body (case rule
          (:all '(anaphex1 args '(,opname)))
          (:first '(anaphex2 ',opname args))
          (:place '(anaphex3 ',opname args)))))
    '(defmacro ,name (&rest args)
      ,body)))

(defun anaphex1 (args call)
  (if args
    (let ((sym (gensym)))
      '(let* ((,sym ,(car args))
          (it ,sym))
        ,(anaphex1 (cdr args)
          (append call (list sym)))))
    call))

(defun anaphex2 (op args)
  '(let ((it ,(car args))) (,op it ,@(cdr args))))

(defun anaphex3 (op args)
  '(_f (lambda (it) (,op it ,@(cdr args))) ,(car args)))

[示例代碼 16.5] 顯示了所有三個展開器函數(shù)在單獨(dú)一個宏 defanaph 的控制下拼接在一起的結(jié)果。用戶可以通過可選的 rule 關(guān)鍵字參數(shù)來設(shè)置目標(biāo)宏展開的類型,這個參數(shù)指定了在宏調(diào)用中參數(shù)所采用的求值規(guī)則。如果這個參數(shù)是:

:all (默認(rèn)值) 宏展開將采用alist 模型。宏調(diào)用中所有參數(shù)都將被求值,同時it 總是被綁定在前一個參數(shù)的值上。

:first 宏展開將采用aif 模型。只有第一個參數(shù)是必須求值的,并且it 將被綁定在這個值上。

:place 宏展開將采用asetf 模型。第一個參數(shù)被按照廣義變量來對待,而it 將被綁定在它的初始值上。

使用新的 defanaph ,前面的一些例子將被定義成下面這樣:

(defanaph alist)
(defanaph aif :rule first)
(defanaph asetf :rule :place)

asetf 的一大優(yōu)勢是它可以定義出一大類基于廣義變量而不必?fù)?dān)心多重求值問題的宏。例如,我們可以將incf 定義成:

(defmacro incf (place &optional (val 1))
  '(asetf ,place (+ it ,val)))

再比如說 pull ( 12.4 節(jié)):

(defmacro pull (obj place &rest args)
  '(asetf ,place (delete ,obj it ,@args)))

備注:

【注1】盡管這種縮略語不能傳遞給 apply 或者funcall。

【注2】譯者注:這里給出的 asetf 采用了原書勘誤中給出的形式。未勘誤的版本里用 'setf 代替了 '(lambda (x y) (declare (ignore x) y))。這個版本也是有效的,但其中的 setf 是不必要的,真正的廣義變量賦值操作是由背后的 _f 宏完成的。比較一下后面給出 incf 宏在一個普通調(diào)用 (incf a 1) 下兩種 asetf 產(chǎn)生的展開式就可以了解這點(diǎn)了。

【注3】譯者注:本書中所有忽略了某些形參的函數(shù)定義都由譯者添加了類似 (declare (ignore char)) 的聲明以免編譯器報警。

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

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號