第 8 章 何時(shí)使用宏

2018-02-24 15:54 更新

第 8 章 何時(shí)使用宏

我們?nèi)绾沃酪粋€(gè)給定的函數(shù)是否真的應(yīng)該是函數(shù),而不是宏呢?多數(shù)時(shí)候,會(huì)很容易分清楚在哪種情況下需要用到宏,哪種情況不需要。缺省情況下,我們應(yīng)該用函數(shù),因?yàn)槿绻瘮?shù)能解決問題,而偏要用上宏的話,會(huì)讓程序變得不優(yōu)雅。我們應(yīng)當(dāng)只有在宏能帶來特別的好處時(shí)才使用它們。

什么情況下,宏能給我們帶來優(yōu)勢(shì)呢?這就是本章的主題。通常這不是錦上添花,而是一種必須。大多數(shù)我們用宏可以做到的事情,函數(shù)都無法完成。第 8.1 節(jié)列出了只能用宏來實(shí)現(xiàn)的幾種操作符。盡管如此,也有一小類(但很有意思的) 情況介于兩者之間,對(duì)它們來說,不管把操作符實(shí)現(xiàn)成函數(shù)還是宏似乎都言之有理。對(duì)于這種情況,第 8.2 節(jié)給出了關(guān)于宏的正反兩方面考量。最后,在充分考察了宏的能力后,我們?cè)诘?8.3 節(jié)里轉(zhuǎn)向一個(gè)相關(guān)問題:人們都用宏干什么?

8.1 當(dāng)別無他法時(shí)

優(yōu)秀設(shè)計(jì)的一個(gè)通用原則就是:當(dāng)你發(fā)現(xiàn)在程序中的幾處都出現(xiàn)了相似的代碼時(shí),就應(yīng)該寫一個(gè)子例程,并把那些相似的語句換成對(duì)這個(gè)子例程的調(diào)用。如果也把這條原則用到 Lisp 程序上,就必須先決定這個(gè) "子例程" 應(yīng)該是函數(shù)還是宏。

有時(shí),可以很容易確定應(yīng)當(dāng)寫一個(gè)宏而不是函數(shù),因?yàn)橹挥泻瓴拍軡M足需求。一個(gè)像 1+ 這樣的函數(shù)或許既可以寫成函數(shù)也可以寫成宏:

(defun 1+ (x) (+ 1 x))
(defmacro 1+ (x) '(+ 1 ,x))

但是來自第 7.3 節(jié)的?while?,則只能被定義成宏:

(defmacro while (test &body body)
  '(do ()
    ((not ,test))
    ,@body))

無法用函數(shù)來重現(xiàn)這個(gè)宏的行為。while?的定義里拼接了一個(gè)作為?body?傳入?do?的主體里的表達(dá)式,它只有當(dāng)?test?表達(dá)式返回?nil?時(shí)才會(huì)被求值。沒有函數(shù)可以做到這一點(diǎn);是因?yàn)樵诤瘮?shù)調(diào)用里,所有的參數(shù)在函數(shù)調(diào)用開始之前就會(huì)被求值。

當(dāng)你需要用宏時(shí),你看中了它哪一點(diǎn)呢?宏有兩點(diǎn)是函數(shù)無法做到的:宏可以控制(或阻止) 對(duì)其參數(shù)的求值,并且它可以展開進(jìn)入到主調(diào)方的上下文中。任何需要宏的應(yīng)用,歸根到底都是要用上述兩個(gè)屬性中的至少一個(gè)。

"宏不對(duì)其參數(shù)進(jìn)行求值",這個(gè)非正式的說法不太準(zhǔn)確。更確切的說法應(yīng)該是,"宏能控制宏調(diào)用中參數(shù)的求值"。取決于參數(shù)在宏展開式中的位置,它們可以被求值一次,多次,或者根本不求值。宏的這種控制主要體現(xiàn)在四個(gè)方面:

1. 變換

Common Lisp 的 setf 宏就是這類宏中的一員,它們?cè)谇笾登岸紩?huì)對(duì)傳入的參數(shù)嚴(yán)加檢查。內(nèi)置的訪問函數(shù)(access function) 通常都有一個(gè)對(duì)應(yīng)的逆操作,其作用是對(duì)該訪問函數(shù)所獲取的對(duì)象賦值。car 的逆操作是 rplaca ,對(duì)于 cdr 來說是 rplacd ,等等。有了 setf ,我們就可以把對(duì)這些訪問函數(shù)的調(diào)用當(dāng)成變量賦值。(setf (car x) 'a)?就是個(gè)例子,這個(gè)表達(dá)式可以展開成?(progn (rplaca x 'a) 'a).

為了有這樣的效果,setf 必須非常了解它的第一個(gè)參數(shù)。如果要知道上述的情況需要用到?rplaca?,

setf?就得清楚它的第一個(gè)參數(shù)是個(gè)以 car 開始的表達(dá)式。這樣的話,setf 以及其他修改參數(shù)的操作符,就必須被寫成宏。

2. 綁定

詞法變量必須在源代碼中直接出現(xiàn)。例如,由于 setq 的第一個(gè)參數(shù)是不求值的,所以,所有在setq 之上構(gòu)建的東西都必須是展開到setq 的宏,而不能是調(diào)用它的函數(shù)。對(duì)于 let 這樣的操作符也是如此,它的實(shí)參必須作為 lambda 表達(dá)式的形參出現(xiàn),還有類似 do 這樣展開到 let 的宏也是這樣,等等。任何新操作符,只要它修改了參數(shù)的詞法綁定,那么它就必須寫成宏。

3. 條件求值

函數(shù)的所有參數(shù)都會(huì)被求值。在像 when 這樣的結(jié)構(gòu)里,我們希望一些參數(shù)僅在特定條件下才被求值。只有通過宏才可能獲得這種靈活性。

4. 多重求值

函數(shù)的所有參數(shù)不但都會(huì)被求值,而且求值的次數(shù)都正好是一次。我們需要用宏來定義像 do 這樣的結(jié)構(gòu),這樣子,就可以對(duì)特定的參數(shù)多次求值。

也有幾種方式可以利用宏產(chǎn)生的內(nèi)聯(lián)展開式帶來的優(yōu)勢(shì)。這里必須強(qiáng)調(diào)一點(diǎn),宏展開后生成的展開式將會(huì)出現(xiàn)在宏調(diào)用所在的詞法環(huán)境之中,因?yàn)橄铝腥N用法有兩種都基于這個(gè)事實(shí)。它們是:

5. 利用調(diào)用方環(huán)境

宏生成的展開式可以含有這樣的變量,變量的綁定來自宏調(diào)用的上下文環(huán)境。下面這個(gè)宏:

(defmacro foo (x)
  '(+ ,x y))

的行為將因 foo 被調(diào)用時(shí) y 的綁定而不同。

這種詞法交流通常更多地被視為瘟疫的傳染源,而非快樂之源。一般來說,寫這樣的宏不是什么好習(xí)慣。函數(shù)式編程的思想對(duì)于宏也同樣適用:與一個(gè)宏交流的最佳方式就是通過它的參數(shù)。事實(shí)上,需要用到調(diào)用方環(huán)境的情況極少,因此,如果出現(xiàn)了這樣的用法,那十有八九就是什么地方出了問題。(見第9 章)

縱觀本書中的所有宏,只有續(xù)延傳遞(continuation-passing)宏(第 20 章)和 ATN 編譯器(第23 章)的一部分以這種方式利用了調(diào)用方環(huán)境。

6. 包裝新環(huán)境

宏也可以使其參數(shù)在一個(gè)新的詞法環(huán)境下被求值。最經(jīng)典的例子就是let ,它可以用lambda 實(shí)現(xiàn)成宏的形式(見 11.1 節(jié))。在一個(gè)?(let ((y 2)) (+ x y))?這樣的表達(dá)式里,y 將指向一個(gè)新的變量。

7. 減少函數(shù)調(diào)用

宏展開后,展開式內(nèi)聯(lián)地插入展開環(huán)境。這個(gè)設(shè)計(jì)的第三個(gè)結(jié)果是宏調(diào)用在編譯后的代碼中沒有額外開銷。到了運(yùn)行期,宏調(diào)用已經(jīng)替換成了它的展開式。(這個(gè)說法對(duì)于聲明成inline 的函數(shù)也一樣成立。)

很明顯,如果不是有意為之,情形 5 和 6 將產(chǎn)生變量捕捉上的問題,這可能是宏的編寫者所有擔(dān)心的事情里面最頭疼的一件。變量捕捉將在第 9 章討論。

與其說有七種使用宏的方式,不如說有六個(gè)半。在理想的世界里,所有 Common Lisp 編譯器都會(huì)遵守 inline 聲明,所以減少函數(shù)調(diào)用將是內(nèi)聯(lián)函數(shù)的職責(zé),而不是宏的。這個(gè)建立理想世界的重任就作為練習(xí)留給讀者吧。

8.2 宏還是函數(shù)?

上一節(jié)解決了較簡(jiǎn)單的一類問題。一個(gè)操作符,倘若在參數(shù)被求值前就需要訪問它,那么這個(gè)操作符就應(yīng)該寫成宏,因?yàn)閯e無他法。那么,如果有操作符用兩種寫法都能實(shí)現(xiàn),那該怎么辦呢?

比如說操作符?avg?。它返回參數(shù)的平均值。它可以定義成函數(shù):

(defun avg (&rest args)
  (/ (apply #'+ args) (length args)))

但把它定義成宏也不錯(cuò):

(defmacro avg (&rest args)
  '(/ (+ ,@args) ,(length args)))

因?yàn)槊看握{(diào)用 avg 的函數(shù)版本時(shí),都毫無必要地調(diào)用了一次 length。在編譯期我們可能不清楚這些參數(shù)的值,但卻知道參數(shù)的個(gè)數(shù),所以那是調(diào)用 length 最佳的時(shí)機(jī)。當(dāng)我們面臨這樣的選擇時(shí),可以考慮下列幾點(diǎn):

利:

  1. 編譯期計(jì)算。宏調(diào)用共有兩次參與計(jì)算,分別是:宏展開的時(shí)候,以及展開式被求值的時(shí)候。一旦程序編譯好,Lisp 程序中所有的宏展開也就完成了,而在編譯期每進(jìn)行一次計(jì)算,都幫助程序在運(yùn)行的時(shí)候卸掉了一個(gè)包袱。如果在編寫操作符時(shí),可以讓它在宏展開的階段就完成一部分工作,那么把它寫成宏將會(huì)讓程序更加高效。因?yàn)橹灰锹斆鞯木幾g器無法自己完成的工作,函數(shù)就只能把這些事情拖到運(yùn)行期做。第13章介紹一些類似avg 的宏,這些宏能在宏展開的階段就完成一部分工作。

  2. 和Lisp 的集成。有時(shí),用宏代替函數(shù)可以令程序和Lisp 集成得更緊密。解決一個(gè)特定問題的方法,可以是專門寫一個(gè)程序,你也可以用宏把這個(gè)問題變換成另一個(gè)Lisp 已經(jīng)知道解決辦法的問題。如果可行的話,這種方法常??梢允钩绦蜃兊酶绦。哺咝В焊∈且?yàn)長(zhǎng)isp 代勞了一部分工作,更高效則是因?yàn)楫a(chǎn)品級(jí)Lisp 系統(tǒng)通常比用戶程序做了更多的優(yōu)化。這一優(yōu)勢(shì)大多時(shí)候會(huì)出現(xiàn)在嵌入式語言里,而我們從第19章起會(huì)全面轉(zhuǎn)向嵌入式語言。

  3. 免除函數(shù)調(diào)用。宏調(diào)用在它出現(xiàn)的地方直接展開成代碼。所以,如果你把常用的代碼片段寫成宏,那么就可以每次在使用它的時(shí)候免去一次函數(shù)調(diào)用。在Lisp 的早期方言中,程序員借助宏的這個(gè)屬性在運(yùn)行期避免函數(shù)調(diào)用。而在Common Lisp 里,這個(gè)差事應(yīng)該由聲明成 inline 類型的函數(shù)接手了。

通過將函數(shù)聲明成inline,你要求把這個(gè)函數(shù)就像宏一樣,直接編譯進(jìn)調(diào)用方的代碼。不過,理想和現(xiàn)實(shí)還是有距離的; ???2(229 頁) 說 "編譯器可以隨意地忽略該聲明",而且某些 Common Lisp 編譯器確實(shí)也是這樣做的。

在某些情況下,效率因素和跟Lisp 之間緊密集成的組合優(yōu)勢(shì)可以充分證實(shí)使用宏的必要性。在第19章的查詢編譯器里,可以轉(zhuǎn)移到編譯期的計(jì)算量相當(dāng)可觀,這使我們有理由把整個(gè)程序變成一個(gè)獨(dú)立的巨型宏。盡管效率是初衷,這一轉(zhuǎn)移同時(shí)也讓程序和Lisp 走得更近:在新版本里,能更容易地使用Lisp 表達(dá)式,比如說可以在查詢的時(shí)候用Lisp 的算術(shù)表達(dá)式。

弊:

  1. 函數(shù)即數(shù)據(jù),而宏在編譯器看來,更像是一些指令。函數(shù)可以當(dāng)成參數(shù)傳遞(例如用 apply),被函數(shù)返回,或者保存在數(shù)據(jù)結(jié)構(gòu)里。但這些宏都做不到。

有的情況下,你可以通過將宏調(diào)用封裝在 lambda 表達(dá)式里來達(dá)到目的。如果你想用 apply 或 funcall 來調(diào)用某些的宏,這樣是可行的,例如:

> (funcall #'(lambda (x y) (avg x y)) 1 3)
2

不過這樣做還是有些麻煩。而且它有時(shí)還無法正常工作:如果這個(gè)宏帶有&rest 形參,那么就無法給它傳遞可變數(shù)量的實(shí)參,avg 就是個(gè)例子。

  1. 源代碼清晰。宏定義和等價(jià)的函數(shù)定義相比更難閱讀。所以如果將某個(gè)功能寫成宏只能稍微改善程序,那么最好還是改成使用函數(shù)。

  2. 運(yùn)行期清晰。宏有時(shí)比函數(shù)更難調(diào)試。如果你在含有許多宏的代碼里碰到運(yùn)行期錯(cuò)誤,那么你在 backtrace 里看到的代碼將包含所有這些宏調(diào)用的展開式,而它們和你最初寫的代碼看起來可能會(huì)大相徑庭。

并且由于宏展開以后就消失了,所以它們?cè)谶\(yùn)行時(shí)是看不到的。你不是總能使用 trace 來分析一個(gè)宏的調(diào)用過程。如果 trace 真的奏效的話,它展示給你的只是對(duì)宏展開函數(shù)的調(diào)用,而非宏調(diào)用本身的調(diào)用。

  1. 遞歸。在宏里使用遞歸不像在函數(shù)里那么簡(jiǎn)單。盡管展開一個(gè)宏里的展開函數(shù)可能是遞歸的,但展開式本身可能不是。第 10.4 節(jié)將處理跟宏里的遞歸有關(guān)的主題。

在決定何時(shí)使用宏的時(shí)候需要權(quán)衡利弊,綜合考慮所有這些因素。只有靠經(jīng)驗(yàn)才能知道哪一個(gè)因素在起主導(dǎo)作用。盡管如此,出現(xiàn)在后續(xù)章節(jié)里的宏的示例涵蓋了大多數(shù)對(duì)宏有利的情形。如果一個(gè)潛在的宏符合這里給出的條件,那么把它寫成這樣可能就是合適的。

最后,應(yīng)該注意運(yùn)行期清晰(觀點(diǎn) 6) 很少成為障礙。調(diào)試那種用很多宏寫成的代碼并不像你想象的那樣困難。如果一個(gè)宏的定義長(zhǎng)達(dá)數(shù)百行,在運(yùn)行期調(diào)試它的展開式的確是件苦差事。但至少實(shí)用工具往往出現(xiàn)在小而可靠的程序?qū)哟沃?。通常它們的定義長(zhǎng)度不超過 15 行。所以就算你最終只得仔細(xì)檢查一系列的 backtrace ,這種宏也不會(huì)讓你云遮霧繞,摸不著頭腦。

8.3 宏的應(yīng)用場(chǎng)合

在了解了宏的十八般武藝之后,下一個(gè)問題是:我們可以把宏用在哪一類程序里?關(guān)于宏的用途,最正式的表述可能是:它們主要用于句法轉(zhuǎn)換(syntactic transformations)。這并不是要嚴(yán)格限制宏的使用范圍。由于 Lisp 程序從列表中生成,而列表是 Lisp 數(shù)據(jù)結(jié)構(gòu),"句法轉(zhuǎn)換" 的確有很大的發(fā)揮空間。第 19-24 章展示的整個(gè)程序,其目的就可以說成 "句法轉(zhuǎn)換",而且從效果上看,所有宏莫不是如此。

宏的種種應(yīng)用一起織成了一條緞帶,這些應(yīng)用涵蓋了從像 while 這樣小型通用的宏,直到后面章節(jié)定義的大型、特殊用途的宏。緞帶的一端是實(shí)用工具,它們和每個(gè) Lisp 都內(nèi)置的那些宏是一樣的。它們通常短小、通用,而且相互獨(dú)立。盡管如此,你也可以為一些特別類型的程序編寫實(shí)用工具,然后當(dāng)你有一組宏用于,比如說,圖形程序的時(shí)候,它們看起來就像是一種專門用于圖形編程的語言。在緞帶的遠(yuǎn)端,宏允許你用一種和 Lisp 截然不同的語言來編寫整個(gè)程序。以這種方式使用宏的做法被稱為實(shí)現(xiàn)嵌入式語言。

實(shí)用工具是自底向上風(fēng)格的首批成果。甚至當(dāng)一個(gè)程序規(guī)模很小而不必分層構(gòu)建時(shí),它也仍然能夠?qū)Τ绦虻淖畹讓?,?Lisp 本身加以擴(kuò)充,并從中獲益。nil! 將其參數(shù)設(shè)置為 nil ,這個(gè)實(shí)用工具只能定義成宏:

(defmacro nil! (x)
  '(setf ,x nil))

看到 nil! ,可能有人會(huì)說它什么都做不了,無非可以讓我們少輸入幾個(gè)字罷了。是的,但是充其量,宏所能做的也就是讓你少打些字而已。如果有人非要這樣想的話,那么其實(shí)編譯器的工作也不過是讓人們用機(jī)器語言編程的時(shí)候可以少些。不可低估實(shí)用工具的價(jià)值,因?yàn)樗鼈兊墓τ脮?huì)積少成多:幾層簡(jiǎn)單的宏拉開了一個(gè)優(yōu)雅的程序和一個(gè)晦澀的程序之間的差距。

多數(shù)實(shí)用工具都含有模式。當(dāng)你注意到代碼中存在模式時(shí),不妨考慮把它寫成實(shí)用工具。模式是計(jì)算機(jī)最擅長(zhǎng)的。為什么有程序可以代勞,還要自己動(dòng)手呢?假設(shè)在寫某個(gè)程序的時(shí)候,你發(fā)現(xiàn)自己以同樣的通用形式在很多地方做循環(huán)操作:

(do ()
  ((not <condition>))
  . <body of code>)

從列表中生成,是指列表作為編譯器的輸入。函數(shù)不再?gòu)牧斜碇猩桑m然在一些早期的方言里的確是這樣處理的。

當(dāng)你在自己的代碼里發(fā)現(xiàn)一個(gè)重復(fù)的模式時(shí),這個(gè)模式經(jīng)常會(huì)有一個(gè)名字。這里,模式的名字是 while 。如果我們想把它作為實(shí)用工具提供出來,那么只能以宏的形式,因?yàn)樾枰玫綆l件判斷的求值,和重復(fù)求值。倘若用第 7.4 節(jié)的定義實(shí)現(xiàn) while ,如下:

(defmacro while (test &body body)
  '(do ()
    ((not ,test))
    ,@body))

就可以將該模式的所有實(shí)例替換成:

(while <condition>
  . <body of code>)

這樣做使得代碼更簡(jiǎn)短,同時(shí)也更清晰地表明了程序的意圖。

宏的這種變換參數(shù)的能力使得它在編寫接口時(shí)特別有用。適當(dāng)?shù)暮昕梢栽诒緫?yīng)需要輸入冗長(zhǎng)復(fù)雜表達(dá)式的地方只輸入簡(jiǎn)短的表達(dá)式。盡管圖形界面減少了為最終用戶編寫這類宏的需要,程序員卻一直使用這種類型的宏。最普通的例子是 defun ,在表面上,它創(chuàng)建的函數(shù)綁定類似用 Pascal 或 C 這樣的語言定義的函數(shù)。第 2 章提到下面兩個(gè)表達(dá)式差不多具有相同的效果:

(defun foo (x) (* x 2))

(setf (symbol-function 'foo)
  #'(lambda (x) (* x 2)))

這樣 defun 就可以實(shí)現(xiàn)成一個(gè)將前者轉(zhuǎn)換成后者的宏。我們可以想象它會(huì)這樣寫:

(defmacro our-defun (name parms &body body)
  '(progn
    (setf (symbol-function ',name)
      #'(lambda ,parms (block ,name ,@body)))
    ',name))

像 while 和 nil! 這樣的宏可以被視為通用的實(shí)用工具。任何 Lisp 程序都可以使用它們。但是特定的領(lǐng)

域同樣也可以有它們自己的實(shí)用工具。沒有理由認(rèn)為擴(kuò)展編程語言的唯一平臺(tái)只能是原始的 Lisp。舉個(gè)例子,如果你正在編寫一個(gè) ?? 程序,有時(shí),最佳的實(shí)現(xiàn)可能會(huì)把它寫成兩層:一門專用于 CAD 程序的語言

(或者如果你偏愛更現(xiàn)代的說法,一個(gè)工具箱(toolkit)),以及在這層之上的,你的特定應(yīng)用。

Lisp 模糊了許多對(duì)其他語言來說理所當(dāng)然的差異。在其他語言里,在編譯期和運(yùn)行期,程序和數(shù)據(jù),以及語言和程序之間具有根本意義上的差異。而在 Lisp 里,這些差異就退化成了口頭約定。例如,在語言和程序之間就沒有明確的界限。你可以根據(jù)手頭程序的情況自行界定。因而,是把底層代碼稱作工具箱,還是稱之為語言,確實(shí)不過是個(gè)說法而已。將其視為語言的一個(gè)好處是,它暗示著你可以擴(kuò)展這門語言,就像你通過實(shí)用工具來擴(kuò)展 Lisp 一樣。

設(shè)想我們正在編寫一個(gè)交互式的 2D 繪圖程序。為了簡(jiǎn)單起見,我們將假定程序處理的對(duì)象只有線段,每條線段都表示成一個(gè)起點(diǎn) 和一個(gè)向量 。并且我們的繪圖程序的任務(wù)之一是平移一組對(duì)象。

這正是 [示例代碼 8.1] 中函數(shù) move-objs 的任務(wù)。出于效率考慮,我們不想在每個(gè)操作結(jié)束后重繪整個(gè)屏幕 只畫那些改變了的部分。因此兩次調(diào)用了函數(shù) bounds ,它返回表示一組對(duì)象的矩形邊界的四個(gè)坐標(biāo)(最小x ,最小y ,最大x ,最大y)。move-objs 的操作部分被夾在了兩次對(duì) bounds 調(diào)用的中間,它們分別找到平移前后的矩形邊界,然后重繪整個(gè)區(qū)域。

函數(shù) scale-objs 被用來改變一組對(duì)象的大小。由于區(qū)域邊界可能隨縮放因子的不同而放大或者縮小,這個(gè)函數(shù)也必須在兩次 bounds 調(diào)用之間發(fā)生作用。隨著我們繪圖程序開發(fā)進(jìn)度的不斷推進(jìn),這個(gè)模式一次又一次地出現(xiàn)在我們眼前:在旋轉(zhuǎn),翻轉(zhuǎn),轉(zhuǎn)置等函數(shù)里。

通過一個(gè)宏,我們可以把這些函數(shù)中相同的代碼抽象出來。[示例代碼 8.2] 中的宏with-redraw 給出了一個(gè)框架,

它是圖 8.1 中幾個(gè)函數(shù)所共有的。 這樣的話,這些函數(shù)每一個(gè)的定義都縮減到了四行代碼,如圖 8.2 末尾


[示例代碼 8.1] 最初的平移和縮放

(defun move-objs (objs dx dy)
  (multiple-value-bind (x0 y0 x1 y1) (bounds objs)
    (dolist (o objs)
      (incf (obj-x o) dx)
      (incf (obj-y o) dy))
    (multiple-value-bind (xa ya xb yb) (bounds objs)
      (redraw (min x0 xa) (min y0 ya)
        (max x1 xb) (max y1 yb)))))

(defun scale-objs (objs factor)
  (multiple-value-bind (x0 y0 x1 y1) (bounds objs)
    (dolist (o objs)
      (setf (obj-dx o) (* (obj-dx o) factor)
        (obj-dy o) (* (obj-dy o) factor)))
    (multiple-value-bind (xa ya xb yb) (bounds objs)
      (redraw (min x0 xa) (min y0 ya)
        (max x1 xb) (max y1 yb)))))

[示例代碼 8.2] 骨肉分離后的平移和縮放

(defmacro with-redraw ((var objs) &body body)
  (let ((gob (gensym))
      (x0 (gensym)) (y0 (gensym))
      (x1 (gensym)) (y1 (gensym)))
    '(let ((,gob ,objs))
      (multiple-value-bind (,x0 ,y0 ,x1 ,y1) (bounds ,gob)
        (dolist (,var ,gob) ,@body)
        (multiple-value-bind (xa ya xb yb) (bounds ,gob)
          (redraw (min ,x0 xa) (min ,y0 ya)
            (max ,x1 xb) (max ,y1 yb)))))))

(defun move-objs (objs dx dy)
  (with-redraw (o objs)
    (incf (obj-x o) dx)
    (incf (obj-y o) dy)))

(defun scale-objs (objs factor)
  (with-redraw (o objs)
    (setf (obj-dx o) (* (obj-dx o) factor)
      (obj-dy o) (* (obj-dy o) factor))))

所示。通過這兩個(gè)函數(shù),這個(gè)新寫的宏在簡(jiǎn)潔性方面作出的貢獻(xiàn)證明了它是物有所值的。并且,一旦把屏幕重繪的細(xì)節(jié)部分抽象出來,這兩個(gè)函數(shù)就變得清爽多了。

對(duì) with-redraw ,有一種看法是把它視為一種語言的控制結(jié)構(gòu),這種語言專門用于編寫交互式的繪圖程序。

隨著我們開發(fā)出更多這樣的宏,它們不管從名義上,還是在實(shí)際上都會(huì)構(gòu)成一門專用的編程語言,并且我們的程序也將開始表現(xiàn)出其不俗之處,這正是我們用特制的語言撰寫程序所期望的效果。

宏的另一主要用途就是實(shí)現(xiàn)嵌入式語言。Lisp 在編寫編程語言方面是一種特別優(yōu)秀的語言,因?yàn)長(zhǎng)isp 程序可以表達(dá)成列表,而且Lisp 還有內(nèi)置的解析器(read) 和編譯器(compile) 可以用在以這種方式表達(dá)的程序中。多數(shù)時(shí)候甚至不用調(diào)用 compile ;你可以通過編譯那些用來做轉(zhuǎn)換的代碼(第 2.9 節(jié)),讓你的嵌入式語言在無形中完成編譯。

這個(gè)宏的定義使用了下一章才出現(xiàn)的 gensym 。它的作用接下來就會(huì)說明。

與其說嵌入式語言是構(gòu)建于 Lisp 之上的語言,不如說它是和Lisp 融為一體的,這使得其語法成為了一個(gè) Lisp 和新語言中特有結(jié)構(gòu)的混合體。實(shí)現(xiàn)嵌入式語言的初級(jí)方式是用Lisp 給它寫一個(gè)解釋器。有可能的話,一個(gè)更好的方法是通過語法轉(zhuǎn)換實(shí)現(xiàn)這種語言:將每個(gè)表達(dá)式轉(zhuǎn)換成 Lisp 代碼,然后讓解釋器可以通過求值的方式來運(yùn)行它。這就是宏大展身手的時(shí)候了。宏的工作恰恰是將一種類型的表達(dá)式轉(zhuǎn)換成另一種類型,所以在編寫嵌入式語言時(shí),宏是最佳人選。

一般而言,嵌入式語言可以通過轉(zhuǎn)換實(shí)現(xiàn)的部分越多越好。主要原因是可以節(jié)省工作量。舉個(gè)例子,如果新語言里含有數(shù)值計(jì)算,那你就無需面對(duì)表示和處理數(shù)值量的所有細(xì)枝末節(jié)。如果 Lisp 的計(jì)算功能可以滿足你的需要,那么你可以簡(jiǎn)單地將你的算術(shù)表達(dá)式轉(zhuǎn)換成等價(jià)的Lisp 表達(dá)式,然后將其余的留給 Lisp 處理。

代碼轉(zhuǎn)換通常都會(huì)提高你的嵌入式語言的效率。而解釋器在速度方面卻一直處于劣勢(shì)。當(dāng)代碼里出現(xiàn)循環(huán)時(shí),通常每次迭代解釋器都必須重新解釋代碼,而編譯器卻只需做一次編譯。因此,就算解釋器本身是編譯的,使用解釋器的嵌入式語言也會(huì)很慢。但如果新語言里的表達(dá)式被轉(zhuǎn)換成了 Lisp,那么 Lisp 編譯器就會(huì)編譯這些轉(zhuǎn)換出來的代碼。這樣實(shí)現(xiàn)的語言不需要在運(yùn)行期承受解釋的開銷。要是你還沒有為你的語言編寫一個(gè)真正編譯器,宏會(huì)幫助你獲得最優(yōu)的性能。事實(shí)上,轉(zhuǎn)換新語言的宏可以看作該語言的編譯器 -- 只不過它的大部分工作是由已有的 Lisp 編譯器完成的。

這里我們暫時(shí)不會(huì)考慮任何嵌入式語言的例子,第19-25 章都是關(guān)于該主題的。第 19 章專門講述了解釋與轉(zhuǎn)換嵌入式語言之間的區(qū)別,并且同時(shí)用這兩種方法實(shí)現(xiàn)了同一種語言。

有一本 Common Lisp 的書斷言宏的作用域是有限的,依據(jù)是:在所有 CLTL1 里定義的操作符中,只有少于 10% 的操作符是宏。這就好比是說因?yàn)槲覀兊姆孔邮怯么u砌成的,我們的家具也必須得是。宏在一個(gè) Common Lisp 程序中所占的比例多少完全要看這個(gè)程序想干什么。有的程序里可能根本沒有宏,而有的程序可能全是宏。

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

掃描二維碼

下載編程獅App

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

編程獅公眾號(hào)