## 第 9 章 變量捕捉
宏很容易遇到一類被稱為**變量捕捉**的問題。變量捕捉發生在宏展開導致名字沖突的時候,名字沖突指:某些符號結果出乎意料地引用了來自另一個上下文中的變量。無意的變量捕捉可能會造成極難發覺的 bug。
本章將介紹預見和避免它們的辦法。不過,有意的變量捕捉卻也是一種有用的編程技術,而且第 14 章的宏都是靠這種技術實現的。
### 9.1 宏參數捕捉
如果一個宏對無意識的變量捕捉毫無防備,那么它就是有 bug 的宏。為了避免寫出這樣的宏,我們必須確切地知道捕捉發生的時機。變量捕捉可以分為兩類情況:
> **宏參數捕捉**和**自由符號捕捉**。
所謂宏參數捕捉,就是在宏調用中作為參數傳遞的符號無意地引用到了宏展開式本身建立的變量。考慮下面這個?`for`?宏的定義,它像?`Pascal`?的?`for`?在一系列表達式上循環操作:
~~~
(defmacro `for` ((var start stop) &body body)
'(do ((,var ,start (1+ ,var))
(limit ,stop))
((> ,var limit))
,@body))
~~~
這個宏乍看之下沒有問題。它甚至似乎也可以正常工作:
~~~
> (for (x 1 5)
(princ x))
12345
NIL
~~~
確實,這個錯誤如此隱蔽,可能用上這個版本的宏數百次,都毫無問題。但如果我們這樣調用它,問題就出來了:
~~~
(for (limit 1 5)
(princ limit))
~~~
我們可能會認為這個表達式和之前的結果相同。但它卻沒有任何輸出:它產生了一個錯誤。為了找到原因,我們仔細觀察它的展開式:
~~~
(do ((limit 1 (1+ limit))
(limit 5))
((> limit limit))
(print limit))
~~~
現在錯誤的地方就很明顯了。在宏展開式本身的符號和作為參數傳遞給宏的符號之間出現了名字沖突。宏展開*捕捉*了?`limit`。這導致它在同一個?`do`?里出現了兩次,而這是非法的。
由變量捕捉導致的錯誤比較罕見,但頻率越低其性質就越惡劣。上個捕捉相對還比較溫和, 至少這次我們得到了一個錯誤。更普遍的情況是,捕捉了變量的宏只是產生錯誤的結果,卻沒有給出任何跡象顯示問題的源頭。在下面的例子中:
~~~
> (let ((limit 5))
(for (i 1 10)
(when (> i limit)
(princ i))))
NIL
~~~
產生的代碼靜悄悄地什么也不做。
### 9.2 自由符號捕捉
偶爾會出現這樣的情況,宏定義本身有這么一些符號,它們在宏展開時無意中卻引用到了其所在環境中的綁定。假設有個程序,它希望把運行中產生的警告信息保存在一個列表里供事后檢查,而不是在問題發生時直接打印輸出給用戶。于是有人寫了一個宏?`gripe`?,它接受一個警告信息,并把它加入全局列表?`w`?:
~~~
(defvar w nil)
(defmacro gripe (warning) ; wrong
'(progn (setq w (nconc w (list ,warning)))
nil))
~~~
之后,另一個人希望寫個函數?`sample-ratio`?,用來返回兩個列表的長度比。如果任何一個列表中的元素少于兩個,函數就改為返回 nil ,同時產生一個警告說明這個函數處理的是一個統計學上沒有意義的樣本。(實際的警告本可以帶有更多的信息,但它們的內容與本例無關。)
~~~
(defun sample-ratio (v w)
(let ((vn (length v)) (wn (length w)))
(if (or (< vn 2) (< wn 2))
(gripe "sample < 2")
(/ vn wn))))
~~~
如果用?`w = (b)`?來調用?`sample-ratio`?,那么它將會警告說它有個參數只含一個元素,因而得出的結果從統計上來講是無意義的。但是當對 gripe 的調用被展開時,sample-ratio 就好像被定義成:
~~~
(defun sample-ratio (v w)
(let ((vn (length v)) (wn (length w)))
(if (or (< vn 2) (< wn 2))
(progn (setq w (nconc w (list "sample < 2")))
nil)
(/ vn wn))))
~~~
這里的問題是,使用 gripe 時的上下文含有 w 自己的局部綁定。所以,產生的警告沒能保存到全局的警告列表里,而是被 nconc 連接到了 sample-ratio 的一個參數的結尾。不但警告丟失了,而且列表?`(b)`?也加上了一個多余的字符串,而程序的其他地方可能還會把它作為數據繼續使用:
~~~
> (let ((lst '(b)))
(sample-ratio nil lst)
lst)
(B "sample < 2")
> w
NIL
~~~
### 9.3 捕捉發生的時機
許多宏的編寫者都希望通過查看宏的定義,就可以預見到所有可能來自上述兩種捕捉類型的問題。變量捕捉有些難以捉摸,需要一些經驗才能預料到那些被捕捉的變量在程序中所有搗亂的伎倆。幸運的是,還是有辦法在你的宏定義中找出那些可能被捕捉的符號,并排除它們的,而無需操心這些符號捕捉如何搞砸你的程序。本節將介紹一套直接了當的檢測原則,用它就可以找出可捕捉的符號。本章的其余部分則解釋了避免出現變量捕捉的相關技術。
我們接下來提出的方法可以用來定義可捕捉的變量,但是它基于幾個從屬的概念,所以在繼續之前必須首先給這些概念下個定義:
> 自由(free):我們認為表達式中的符號 s 是自由的,當且僅當它被用作表達式中的變量,但表達式卻沒有為它創建一個綁定。
在下列表達式里:
~~~
(let ((x y) (z 10))
(list w x z))
~~~
w ,x 和 z 在 list 表達式中看上去都是自由的,因為這個表達式沒有建立任何綁定。不過,外圍的 let 表達式為 x 和 z 創建了綁定,從整體上說,在 let 里面,只有 y 和 w 是自由的。注意到在:
~~~
(let ((x x))
x)
~~~
里 x 的第二個實例是自由的。因為它并不在為 x 創建的新綁定的作用域內。
> 框架(skeleton): 宏展開式的框架是整個展開式,并且去掉任何在宏調用中作為實參的部分。
如果 foo 的定義是:
~~~
(defmacro foo (x y)
'(/ (+ ,x 1) ,y))
~~~
并且被這樣調用:
~~~
(foo (- 5 2) 6)
~~~
那么它就會產生如下的展開式:
~~~
(/ (+ (- 5 2) 1) 6)
~~~
這一展開式的框架就是上面這個表達式在把形參 x 和 y 拿走,留下空白后的樣子:
~~~
(/ (+ 1) )
~~~
有了這兩個概念,就可以把判斷可捕捉符號的方法簡單表述如下:
> 可捕捉(capturable):如果一個符號滿足下面條件之一,那就可以認為它在某些宏展開里是可捕捉的
>
> > (a) 它作為自由符號出現在宏展開式的框架里,或者 (b) 它被綁定到框架的一部分,而該框架中含有傳遞給宏的參數,這些參數被綁定或被求值。
用些例子可以明確這個標準的含義。在最簡單的情況下:
~~~
(defmacro cap1 ()
'(+ x 1))
~~~
x 可被捕捉是因為它作為自由符號出現在框架里。這就是導致?`gripe`?中 bug 的原因。在這個宏里:
~~~
(defmacro cap2 (var)
'(let ((x ...)
(,var ...))
...))
~~~
`x`?可被捕捉是因為它被綁定在一個表達式里,而同時也有一個宏調用的參數被綁定了。(這就是for 中出現的錯誤。)同樣對于下面兩個宏:
~~~
(defmacro cap3 (var)
'(let ((x ...))
(let ((,var ...))
...)))
(defmacro cap4 (var)
'(let ((,var ...))
(let ((x ...))
...)))
~~~
x 在兩個宏里都是可捕捉的。然而,如果 x 的綁定和作為參數傳遞的變量沒有這樣一個上下文,在這個上下文中,兩者是同時可見的,就像在這個宏里:
~~~
(defmacro safe1 (var)
'(progn (let ((x 1))
(print x))
(let ((,var 1))
(print ,var))))
~~~
那么 x 將不會被捕捉到。并非所有綁定在框架里的變量都是有風險的。盡管如此,如果宏調用的參數在一個由框架建立的綁定里被求值:
~~~
(defmacro cap5 (&body body)
'(let ((x ...))
,@body))
~~~
那么,這樣綁定的變量就有被捕捉的風險:在?`cap5`?中,x 是可捕捉的。不過對于下面這種情況:
~~~
(defmacro safe2 (expr)
'(let ((x ,expr))
(cons x 1)))
~~~
x 是不可捕捉的,因為當傳給?`expr`?的參數被求值時,x 的新綁定將是不可見的。同時,請注意我們只需關心那些框架變量的綁定。在這個宏里:
~~~
(defmacro safe3 (var &body body)
'(let ((,var ...))
,@body))
~~~
沒有符號會因沒有防備而被捕捉(假設第一個參數的綁定是用戶有意為之)。
現在讓我們來檢查一下?`for`?最初的定義,看看使用新的規則是否能發現可捕捉的符號:
~~~
(defmacro for ((var start stop) &body body) ; wrong
'(do ((,var ,start (1+ ,var))
(limit ,stop))
((> ,var limit))
,@body))
~~~
現在可以看出?`for`?的這一定義可能遭受兩種方式的捕捉:limit 可能會被作為第一個參數傳給?`for`,就像在最早的例子里那樣:
~~~
(for (limit 1 5)
(princ limit))
~~~
但是,如果 limit 出現在循環體里,也同樣危險:
~~~
(let ((limit 0))
(for (x 1 10)
(incf limit x))
limit)
~~~
這樣用?`for`?的人,可能會期望他自己的 limit 綁定就是在循環里遞增的那個,最后整個表達式返回`55`;事實上,只有那個由展開式框架生成的?`limit`?綁定會遞增:
~~~
(do ((x 1 (1+ x))
(limit 10))
((> x limit))
(incf limit x))
~~~
并且,由于迭代過程是由這個變量控制的,所以循環甚至將無法終止。
本節中介紹的這些規則不過是個參考,在實際編程中僅僅具有指導意義。它們甚至不是形式化定義的,更不能完全保證其正確性。捕捉是一個不能明確定義的問題,它依賴于你期望的行為。例如,在下面的表達式里:
~~~
(let ((x 1)) (list x))
~~~
x 在 (list x)被求值時,會指向新的變量,不過我們不會把它視為錯誤。這正是 let 要做的事。檢測捕捉的規則也含混不清。你可以寫出通過這些測試的宏,而這樣的宏卻仍然有可能會遭受意料之外的捕捉。例如:
~~~
(defmacro pathological (&body body) ; wrong
(let* ((syms (remove-if (complement #'symbolp)
(flatten body)))
(var (nth (random (length syms))
syms)))
'(let ((,var 99))
,@body)))
~~~
當調用這個宏的時候,宏主體中的表達式就像是在一個?`progn`?中被求值 但是主體中有一個隨機選出的變量將帶有一個不同的值。這很明顯是一個捕捉,但它通過了我們的測試,因為這個變量并沒有出現在框架里。然而,實踐表明該規則在絕大多數時候都是正確的:很少有人(如果真有的話)會想寫出類似上面那個例子的宏。
### 9.4 取更好的名字避免捕捉
前兩節將變量捕捉分為兩類:參數捕捉,在這種情況下,由宏框架建立的綁定會捕捉參數中用到的符號;和自由符號捕捉,而在這里,宏展開處的綁定會捕捉到宏展開式中的自由符號。常常可以通過給全局變量取個明顯的名字來解決后一類問題。在 Common Lisp 中,習慣上會給全局變量取一個兩頭都是星號的名字。
例如,定義當前包的變量叫做 package 。(這樣的名字可以發音為 "star-package-star" 來強調它不是普通的變量。)
所以 gripe 的作者的的確確有責任把那些警告保存在一個名字類似?*warnings*?而非 w 的變量中。如果 sample-ratio 的作者執意要用 *warnings* 做函數參數,那他碰到的每個 bug 都是咎由自取,但如果他覺得用 w 作為參數的名字應該比較保險,就不應該再怪他了。
### 9.5 通過預先求值避免捕捉
有時,如果不在任何宏展開創建的綁定里求值那些有危險的參數,就可以輕松消除參數捕捉。最簡單的情況可以這樣處理:讓宏以 let 表達式開頭。[示例代碼 9.1] 包含宏 before 的兩個版本,該宏接受兩個對象和一個序列,當且僅當第一個對象在序列中出現于第二個對象之前時返回真【注1】。第一個定義是不正確的。它開始的 let 確保了作為 seq 傳遞的 form 只求值一次,但是它不能有效地避免下面這個問題:
* * *
**[示例代碼 9.1] 用 let 避免捕捉**
易于被捕捉的:
~~~
(defmacro before (x y seq)
'(let ((seq ,seq))
(< (position ,x seq)
(position ,y seq))))
~~~
一個正確的版本:
~~~
(defmacro before (x y seq)
'(let ((xval ,x) (yval ,y) (seq ,seq))
(< (position xval seq)
(position yval seq))))
~~~
* * *
~~~
> (before (progn (setq seq '(b a)) 'a)
'b
'(a b))
NIL
~~~
這相當于問 "(a b) 中的 a 是否在 b 前面?" 如果 before 是正確的,它將返回真。宏展開式揭示了真相:對?`<`?的第一個參數的求值重新排列了那個將在第二個參數里被搜索的列表。
~~~
(let ((seq '(a b)))
(< (position (progn (setq seq '(b a)) 'a)
seq)
(position 'b seq)))
~~~
要想避免這個問題,只要在一個巨大的?`let`?里求值所有參數就行了。這樣 [示例代碼 9.1] 中的第二個定義對于捕捉就是安全的了。
不幸的是,這種 let 技術只能在很有限的一類情況下才可行:
1. 所有可能被捕捉的參數都只求值一次,并且
2. 沒有一個參數需要在宏框架建立的綁定下被求值。
這個規則排除了相當多的宏。我們比較贊成的?`for`?宏就同時違反了這兩個限制。然而,我們可以把這個技術加以變化,使類似?`for`?的宏免于發生捕捉,即將其 body forms 包裝在一個 λ表達式里,同時讓這個 λ表達式位于任何局部創建的綁定之外。
有些宏(其中包括用于迭代的宏),如果宏調用里面有表達式出現,那么在宏展開后,這些表達式將會在一個新建的綁定中求值。例如在?`for`?的定義中,循環體必須在一個由宏創建的?`do`?中進行求值。因此,`do`?創建的變量綁定會很容易就捕捉到循環里的變量。我們可以把循環體包在一個閉包里,同時在循環里,不再把直接插入表達式,而只是簡單地?`funcall`?這個閉包。通過這種辦法來保護循環中的變量不被捕捉。
[示例代碼 9.2] 給出了一個?`for`?的實現,它使用的就是這種技術。由于閉包是?`for`?展開時生成的第一個東西,因此,所有出現在宏體內的自由符號將全部指向宏調用環境中的變量。現在?`do`?通過閉包的參數跟宏體通信。閉包需要從?`do`?知道的全部就是當前迭代的數字,所以它只有一個參數,也就是宏調用中作為索引指定的那個符號。
這種將表達式包裝進 lambda 的方法也不是萬金油。雖然你可以用它來保護代碼體,但閉包有時也起不到任何作用,例如,當存在同一變量在同一個 let 或?`do`?里被綁定兩次的風險時(就像開始的那個有缺陷的for 那樣)。幸運的是,在這種情況下,通過重寫?`for`?將其主體包裝在一個閉包里,我們同時也消除了do 為 var 參數建立綁定的需要。原先那個?`for`?中的 var 參數變成了閉包的參數并且在?`do`?里面可以被一個實際的符號 count 替換掉。所以這個for 的新定義對于捕捉是完全免疫的,就像 9.3 節里的測試所顯示的那樣。
* * *
**[示例代碼 9.2] 用閉包避免捕捉**
易于被捕捉的:
~~~
(defmacro for ((var start stop) &body body)
'(do ((,var ,start (1+ ,var))
(limit ,stop))
((> ,var limit))
,@body))
~~~
正確的版本:
~~~
(defmacro for ((var start stop) &body body)
'(do ((b #'(lambda (,var) ,@body))
(count ,start (1+ count))
(limit ,stop))
((> count limit))
(funcall b count)))
~~~
* * *
閉包的缺點在于,它們的效率可能不大理想。我們可能會因此造成又一次函數調用。更糟糕的是,如果編譯器沒有給閉包分配動態作用域(dynamicextent),那么一等到運行期,閉包所需的空間將不得不從堆里分配。【注2】
### 9.6 通過 gensym 避免捕捉
這里有一種切實可行的方法可供避免宏參數捕捉:把可捕捉的符號換成 gensym。在?`for`?的最初版本中,當兩個符號意外地重名時,就會出問題。如果我們想要避免這種情況:宏框架里含有的符號也同時出現在了調用方代碼里,我們也許會給宏定義里的符號取個怪異的名字,寄希望以此來擺脫參數捕捉的魔爪:
~~~
(defmacro for ((var start stop) &body body) ; wrong
'(do ((,var ,start (1+ ,var))
(xsf2jsh ,stop))
((> ,var xsf2jsh))
,@body))
~~~
但是這治標不治本。它并沒有消除 bug,只是降低了出問題的可能性。并且還有一個可能性不那么小的問題懸而未決 不難想象,如果把同一個宏嵌套使用的話,仍會出現名字沖突。
我們需要一個辦法來確保符號都是唯一的。Common Lisp 函數 gensym 的意義正是在于此。它返回的符號稱為 gensym ,這個符號可以保證不和任何手工輸入或者由程序生成的符號相等(eq)。
那 Lisp 是如何保證這一點的呢?在 Common Lisp 中,每個包都維護著一個列表,用于保存這個包知道的所有符號。【注3】
一個符號,只要出現在這個列表上,我們就說它被約束(intern)在這個包里。每次調用 gensym 都會返回唯一的,未約束的符號。而 read 每見到一個符號,都會把它約束,所以沒人能輸入和 gensym 相同的東西。也就是說,如果你有個表達式是這樣開頭的:
~~~
(eq (gensym) ...
~~~
那么將無法讓這個表達式返回真。
讓 gensym 為你構造符號,這個辦法其實和 "選個怪名字" 的方法異曲同工,而且更進一步 gensym 給你的名字甚至在電話薄里也找不到。如果 Lisp 不得不顯示 gensym,
~~~
> (gensym)
#:G47
~~~
它打印出來的東西基本上就相當于 Lisp 的 "張三",即為那種名字無關緊要的東西編造出來的毫無意義的名字。并且為了確保我們不會對此有任何誤會,gensym 在顯示時候,前面加了一個井號和一個冒號,這是一種特殊的讀取宏(read-macro),其目的是為了讓我們在試圖第二次讀取該 gensym 時報錯。
在 CLSH2 Common Lisp 里,gensym 的打印形式中的數字來自 *gensym-counter* ,這個全局變量總是綁定到某個整數。如果重置這個計數器,我們就可以讓兩個 gensym 的打印輸出一模一樣:
~~~
> (setq x (gensym))
#:G48
> (setq *gensym-counter* 48 y (gensym))
#:G48
> (eq x y)
NIL
~~~
但它們不是一回事。
* * *
**[示例代碼 9.3] 用 gensym 避免捕捉**
易于被捕捉的:
~~~
(defmacro for ((var start stop) &body body)
'(do ((,var ,start (1+ ,var))
(limit ,stop))
((> ,var limit))
,@body))
~~~
一個正確的版本:
~~~
(defmacro for ((var start stop) &body body)
(let ((gstop (gensym)))
'(do ((,var ,start (1+ ,var))
(,gstop ,stop))
((> ,var ,gstop))
,@body)))
~~~
* * *
[示例代碼 9.3] 中有一個使用 gensym 的?`for`?的正確定義。現在就沒有 limit 可以和傳進宏的 form 里的符號有沖突了。它已經被換成一個在現場生成的符號。宏每次展開的時候,limit 都會被一個在展開期創建的唯一符號取代。
初次就把?`for`?定義得完美無缺,還是很難的。完成后的代碼,如同一個完成了的定理,精巧漂亮的證明的背后是一次次的嘗試和失敗。所以不要擔心你可能會對一個宏寫好幾個版本。在開始寫類似`for`?這樣的宏時,你可以在不考慮變量捕捉問題的情況下,先把第一個版本寫出來,然后再回過頭來為那些可能卷入捕捉的符號制作 gensym。
### 9.7 通過包避免捕捉
從某種程度上說,如果把宏定義在它們自己的包里,就有可能避免捕捉。倘若你創建一個 macros 包,并且在其中定義?`for`?,那么你甚至可以使用最初給出的定義
~~~
(defmacro for ((var start stop) &body body)
'(do ((,var ,start (1+ ,var))
(limit ,stop))
((> ,var limit))
,@body))
~~~
這樣,就可以毫無顧慮地從其他任何包調用它。如果你從另一個包,比方說 mycode,里調用 for,就算把 limit 作為第一個參數,它也是 mycode::limit 這和 macros::limit 是兩回事,后者才是出現在宏框架中的符號。
然而,包還是沒能為捕捉問題提供面面俱到的通用解決方案。首先,宏是某些程序不可或缺的組成部分,將它們從自己的包里分離出來會很不方便。其次,這種方法無法為 macros 包里的其他代碼提供任何捕捉保護。
### 9.8 其他名字空間里的捕捉
前面幾節都把捕捉說成是一種僅影響變量的問題。盡管多數捕捉都是變量捕捉,但是 Common Lisp 的其他名字空間里也同樣會有這種問題。
函數也可能在局部被綁定,因而,函數綁定也會因無意的捕捉而導致問題。例如,
~~~
> (defun fn (x) (+ x 1))
FN
> (defmacro mac (x) '(fn ,x))
MAC
> (mac 10)
11
> (labels ((fn (y) (- y 1)))
(mac 10))
9
~~~
正如捕捉規則預料的那樣,以自由之身出現在 mac 框架中的 fn 帶來了被捕捉的風險。如果 fn 在局部被重新綁定的話,那么 mac 的返回值將和平時不一樣。
對于這種情況,該如何應對呢?當有捕捉風險的符號與內置函數或宏重名時,那么聽之任之應該是上策。CLTL2(260 頁) 說,如果任何內置的名字被用作局部函數或宏綁定,"后果是未定義的。" 所以你的宏無論做了什么都沒關系 -- 任何人,如果重新綁定內置函數,那么他將來碰到的問題會比你的這個宏更多。
另一方面,保護變量名的方法同樣可以用來幫助函數名免于宏參數捕捉:通過使用 gensym 作為宏框架局部定義的任何函數的名字。但是,如果要避免像上面這種情況中的自由符號捕捉,就會稍微麻煩一點。要讓變量免受自由符號捕捉,采用的保護方法是使用一目了然的全局名稱:例如把 w 換成 *warnings* 。
然而,這個解決方案對函數有些不切實際,因為沒有把全局函數的名字區分出來的習慣 大多數函數都是全局的。如果你擔心發生這種情況,一個宏使用了另一個函數,而調用這個宏的環境可能會重定義這個函數,那么最佳的解決方案或許就是把你的代碼放在一個單獨的包里。
代碼塊名字(block-name) 同樣可以被捕捉,比如說那些被?`go`?和?`throw`?使用的標簽(tag)。當你的宏需要這些符號時,你應該像 7.8 節的?`our-do`?的定義那樣,使用 gensym。
還需要注意的是像?`do`?這樣的操作符隱式封裝在一個名為?`nil`?的塊里。這樣在?`do`?里面的一個`return`?或?`return-from nil`?將從?`do`?本身而非包含這個?`do`?的表達式里返回:
~~~
> (block nil
(list 'a
(do ((x 1 (1+ x)))
(nil)
(if (> x 5)
(return-from nil x)
(princ x)))))
12345
(A 6)
~~~
如果?`do`?沒有創建一個名為?`nil`?的塊,這個例子將只返回 6 ,而不是`(A 6)`。
`do`?里面的隱式塊不是問題,因為?`do`?的這種工作方式廣為人知。盡管如此,如果你寫一個展開到`do`?的宏,它將捕捉 nil 這個塊名稱。在一個類似?`for`?的宏里,?`return`?或 return-from nil 將從`for`?表達式而非封裝這個?`for`?表達式的塊中返回。
### 9.9 為何要庸人自擾
前面舉的例子中有些非常牽強做作。看著它們,有人可能會說,"變量捕捉既然這么少見 為什么還要操心它呢?" 回答這個問題有兩個方法。一個是用另一個問題反詰道:要是你寫得出沒有 bug 的程序,為什么還要寫有小 bug 的程序呢?
更長的答案是指出在現實應用程序中,對你代碼的使用方式做任何假設都是危險的。任何 Lisp 程序都具備現在被稱之為 "開放式架構" 的特征。如果你正在寫的代碼以后會為他人所用,很可能他們調用你代碼的方式是出乎你預料的。而且你要擔心的不光是人。程序也能編寫程序。可能沒人會寫這樣的代碼
~~~
(before (progn (setq seq '(b a)) 'a)
'b
'(a b))
~~~
但是程序生成的代碼看起來經常就像這樣。即使單個的宏生成的是簡單合理的展開式,一旦你開始把宏嵌套著調用,展開式就可能變成巨大的,而且看上去沒人能寫得出來的程序。在這個前提下,就有必要去預防那些可能使你的宏不正確地展開的情況,就算這種情況像是有意設計出來的。
最后,避免變量捕捉不管怎么說,并非難于上青天。它很快會成為你的第二直覺。Common Lisp 中經典的 defmacro 好比廚子手中的菜刀:美妙的想法看上去會有些危險,但是這件利器一到了專家那里,就如入庖丁之手,游刃有余。
【注1】 這個宏只是個例子。實際編程中,它既不應當實現成宏,也不該用這種低效的算法。若需要正確的定義,可見 4.4 節。
【注2】 譯者注:dynamicextent 是一種Lisp 編譯器優化技術,詳情請見 Common Lisp Hyper Spec 的有關內容。
【注3】 關于包(package) 的介紹,可見**附錄**。
- 封面
- 譯者序
- 前言
- 第 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)