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