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