第 8 章曾提到,宏的長(zhǎng)處之一是其變換參數(shù)的能力。setf
?就是這類(lèi)宏中的一員。本章將著重分析setf
?的內(nèi)涵,然后以幾個(gè)宏為例,它們將建立在?setf
?的基礎(chǔ)之上。
要在?setf
?上編寫(xiě)正確無(wú)誤的宏并非易事,其難度讓人咋舌。為了介紹這個(gè)主題,第一節(jié)會(huì)先給出一個(gè)有點(diǎn)小問(wèn)題的簡(jiǎn)單例子。接下來(lái)的小節(jié)將解釋該宏的錯(cuò)誤之處,然后展示如何改正它。第三和第四節(jié)會(huì)介紹一些基于?setf
?的實(shí)用工具的例子,而最后一節(jié)則會(huì)說(shuō)明如何定義你自己的?setf
?逆變換。
內(nèi)置宏?setf
?是?setq
?的推廣形式。setf
?的第一個(gè)參數(shù)可以是個(gè)函數(shù)調(diào)用而非簡(jiǎn)單的變量:
> (setq lst '(a b c))
(A B C)
> (setf (car lst) 480)
480
> lst
(480 B C)
一般而言,(setf x y)
?可以理解成 "務(wù)必讓 x 的求值結(jié)果為 y"。作為一個(gè)宏,setf
?得以深入到參數(shù)內(nèi)部,弄清需要做哪些工作,才能滿(mǎn)足這個(gè)要求。如果第一個(gè)參數(shù)(在宏展開(kāi)以后) 是個(gè)符號(hào),那么setf
?就只會(huì)展開(kāi)成 setq。但如果第一個(gè)參數(shù)是個(gè)查詢(xún)語(yǔ)句,那么?setf
?則會(huì)展開(kāi)到對(duì)應(yīng)的斷言上。由于第二個(gè)參數(shù)是常量,所以前面的例子可以展開(kāi)成:
(progn (rplaca lst 480) 480)
這種從查詢(xún)到斷言的變換被稱(chēng)為逆變換。Common Lisp 中所有最常用的訪(fǎng)問(wèn)函數(shù)都有預(yù)定義的逆,包括?car
、cdr
、nth
、aref
、get
、gethash
,以及那些由?defstruct
?創(chuàng)建的訪(fǎng)問(wèn)函數(shù)。( 完整的名單見(jiàn)?CLTL2?的第 125 頁(yè)。)
能充當(dāng)?setf
?第一個(gè)參數(shù)的表達(dá)式被稱(chēng)為廣義變量。廣義變量已經(jīng)成為了一種強(qiáng)有力的抽象機(jī)制。宏調(diào)用和廣義變量的相似之處在于:一個(gè)宏調(diào)用,只要能展開(kāi)成可逆引用,那么其本身就一定是可逆的。
當(dāng)我們也加入這個(gè)行列,基于?setf
?編寫(xiě)自己的宏時(shí),這種組合可以產(chǎn)生顯而易見(jiàn)更清爽的程序。我們可以在?setf
?上面定義的宏有很多,其中一個(gè)是?toggle
:【注1】
(defmacro toggle (obj)
'(setf ,obj (not ,obj)))
它可以反轉(zhuǎn)一個(gè)廣義變量的值:
> (let ((lst '(a b c)))
(toggle (car lst))
lst)
(NIL B C)
現(xiàn)在考慮下面的應(yīng)用。假設(shè)有個(gè)人,他可能是個(gè)肥皂劇作者、精力充沛的好事者,或是居委會(huì)大媽?zhuān)胍S護(hù)一個(gè)數(shù)據(jù)庫(kù)。其中記錄著小鎮(zhèn)上所有居民之間的種種恩怨情仇。在數(shù)據(jù)庫(kù)里的表里,其中有一張便是用來(lái)保存朋友關(guān)系的:
(defvar *friends* (make-hash-table))
這個(gè)哈希表的表項(xiàng)本身也是哈希表,其中,潛在的朋友被映射到?t
?或者?nil
?:
(setf (gethash 'mary *friends*) (make-hash-table))
為了使 John 成為 Mary 的朋友,我們可以說(shuō):
(setf (gethash 'john (gethash 'mary *friends*)) t)
這個(gè)鎮(zhèn)被分為兩派。正如幫派的傳統(tǒng),每個(gè)人都聲稱(chēng) "凡人非友即敵",所以鎮(zhèn)上所有人都被迫加入一方或者另一方。這樣當(dāng)某人轉(zhuǎn)變立場(chǎng)時(shí),他所有的朋友都變成敵人,而所有的敵人則變成朋友。
如果只用內(nèi)置的操作符來(lái)切換?x
?和?y
?的敵友關(guān)系,我們必須這樣說(shuō):
(setf (gethash x (gethash y *friends*))
(not (gethash x (gethash y *friends*))))
盡管去掉?setf
?后要簡(jiǎn)單許多,這個(gè)表達(dá)式還是相當(dāng)復(fù)雜。倘若我們?yōu)閿?shù)據(jù)庫(kù)定義了一個(gè)訪(fǎng)問(wèn)宏,如下:
(defmacro friend-of (p q)
'(gethash ,p (gethash ,q *friends*)))
那么在這個(gè)宏和?toggle
?的協(xié)助下,我們就得以更方便地修改數(shù)據(jù)庫(kù)的數(shù)據(jù)。前面那個(gè)更新數(shù)據(jù)庫(kù)的語(yǔ)句可以簡(jiǎn)化成:
(toggle (friend-of x y))
廣義變量就像是美味的健康食品。它們能讓你的程序良好地模塊化,同時(shí)變得更為優(yōu)雅。如果你給出宏或者可逆函數(shù),用來(lái)訪(fǎng)問(wèn)你的數(shù)據(jù)結(jié)構(gòu),那么其他模塊就可以使用?setf
?來(lái)修改你的數(shù)據(jù)結(jié)構(gòu)而無(wú)需了解其內(nèi)部細(xì)節(jié)。
上一節(jié)曾警告說(shuō),我們最初的?toggle
?定義是不正確的:
(defmacro toggle (obj) ; wrong
'(setf ,obj (not ,obj)))
它會(huì)碰到第 10.1 節(jié)里提到的多重求值問(wèn)題。如果它的參數(shù)有副作用,那麻煩就來(lái)了。比如說(shuō),若lst
?是一個(gè)對(duì)象列表,我們這樣寫(xiě):
(toggle (nth (incf i) lst))
并期待它能反轉(zhuǎn)第?(i+1)
?個(gè)元素。事與愿違,如果使用?toggle
?現(xiàn)在的定義,這個(gè)調(diào)用將展開(kāi)成:
(setf (nth (incf i) lst)
(not (nth (incf i) lst)))
這會(huì)使 i 遞增兩次,并且將第?(i+1)
?個(gè)元素設(shè)置成第?(i+2)
?個(gè)元素的反。所以在本例中:
> (let ((lst '(t nil t))
(i -1))
(toggle (nth (incf i) lst))
lst)
(T NIL T)
調(diào)用?toggle
?毫無(wú)效果。
僅僅把作為?toggle
?參數(shù)給出的表達(dá)式插入到?setf
?的第一個(gè)參數(shù)的位置上還不夠。我們必須深入到表達(dá)式內(nèi)部,看看它到底做了什么:如果它含有?subform
?,而且這些?subform
?有副作用的話(huà),我們就需要把它們分開(kāi),并單獨(dú)求值。一般而言,這件事情并不那么簡(jiǎn)單。
為了讓問(wèn)題容易些,Common Lisp 提供了一個(gè)宏,它可以幫助我們自動(dòng)定義一些基于?setf
?的宏,不過(guò)適用范圍有限。宏的名字叫?define-modify-macro
?,它接受三個(gè)參數(shù):被定義宏的宏名,它的附加參數(shù)(出現(xiàn)在廣義變量之后),以及一個(gè)函數(shù)名,這個(gè)函數(shù)將為廣義變量產(chǎn)生新值。【注2】【注3】
使用?define-modify-macro
?,我們可以像下面這樣定義?toggle
?:
(define-modify-macro toggle () not)
具體說(shuō),就是 "若要求值形如 (toggle place) 的表達(dá)式,應(yīng)該先找到?place
?指定的位置,并且,如果保存在那里的值是?val
,將其替換成?(not val)
?的值"。下面把這個(gè)新宏用在原來(lái)的例子里:
> (let ((lst '(t nil t))
(i -1))
(toggle (nth (incf i) lst))
lst)
(NIL NIL T)
雖然這個(gè)版本正確無(wú)誤地給出了結(jié)果,但它本可以寫(xiě)得更通用些。由于?setf
?和?setq
?兩者對(duì)其參數(shù)數(shù)量都沒(méi)有限制,toggle
?也應(yīng)如此。我們可以通過(guò)在修改宏 (modify-macro) 的基礎(chǔ)上定義另一個(gè)宏,來(lái)賦予它這種能力,如 [示例代碼 12.1]所示。
[示例代碼 12.1]:操作在廣義變量上的宏
(defmacro allf (val &rest args)
(with-gensyms (gval)
'(let ((,gval ,val))
(setf ,@(mapcan #'(lambda (a) (list a gval))
args)))))
(defmacro nilf (&rest args) '(allf nil ,@args))
(defmacro tf (&rest args) '(allf t ,@args))
(defmacro toggle (&rest args)
'(progn
,@(mapcar #'(lambda (a) '(toggle2 ,a))
args)))
(define-modify-macro toggle2 () not)
本節(jié)將給出一些新的實(shí)用工具為例,我們用它們對(duì)廣義變量進(jìn)行操作。這些實(shí)用工具必須是宏,以便將參數(shù)原封不動(dòng)地傳給?setf
。
[示例代碼 12.1] 中有四個(gè)基于?setf
?的新宏。第一個(gè)是?allf
?,它被用來(lái)將同一值賦給多個(gè)廣義變量。nilf
?和?tf
?就是基于它實(shí)現(xiàn)的,它們分別將參數(shù)設(shè)置 為?nil
?和?t
?。雖然這些宏很簡(jiǎn)單,但是方便實(shí)用。
和?setq
?一樣,setf
?也可以接受多個(gè)參數(shù) -- 即交替出現(xiàn)的變量和對(duì)應(yīng)的值:
(setf x 1 y 2)
這些新的實(shí)用工具同樣有這個(gè)能力,而且只用傳原來(lái)一半的參數(shù)就可以了。如果你想要把多個(gè)變量初始化為?nil
?,那么可以不再使用:
(setf x nil y nil z nil)
而改成說(shuō):
(nilf x y z)
就行了。最后一個(gè)宏是前一節(jié)曾介紹過(guò)的?toggle
?:它和?nilf
?差不多,但給每個(gè)參數(shù)設(shè)置的是真值的反。
這四個(gè)宏說(shuō)明了關(guān)于賦值操作符的一個(gè)要點(diǎn)。就算我們只需要對(duì)普通變量使用一個(gè)操作符,而把這個(gè)操作符號(hào)展開(kāi)成?setf
?而非?setq
?,這樣做,有百利而無(wú)一害。如果第一個(gè)參數(shù)是符號(hào),setf
?將直接展開(kāi)到?setq
。由于不費(fèi)吹灰之力,就能擁有?setf
?的一般性,所以很少有必要在展開(kāi)式里使用setq
。
[示例代碼 12.2] 廣義變量上的列表操作
(define-modify-macro concf (obj) nconc)
(defun conc1f/function (place obj)
(nconc place (list obj)))
(define-modify-macro conc1f (obj) conc1f/function)
(defun concnew/function (place obj &rest args)
(unless (apply #'member obj place args)
(nconc place (list obj))))
(define-modify-macro concnew (obj &rest args)
concnew/function)
[示例代碼 12.2] 【注4】包含三個(gè)破壞性修改列表結(jié)尾的宏。第 3.1 節(jié)提到依賴(lài)
(nconc x y)
的副作用是不可靠的,并且必須改成:【注5】
(setq x (nconc x y))
這一習(xí)慣用法被嵌入在?concf
?中了。更特殊的?conc1f
?和?concnew
?就像是用于列表另一端的push
?和?pushnew
,conc1f
?在列表結(jié)尾追加一個(gè)元素,而?concnew
?的功能相同,但只有當(dāng)這個(gè)元素不在列表中時(shí)才會(huì)動(dòng)作。
第 2.2 節(jié)曾提到,函數(shù)的名字既可以是符號(hào),也可以是–表達(dá)式。因此,把整個(gè)λ表達(dá)式作為第三個(gè)參數(shù)傳給?define-modify-macro
?也是可行的,正如?conc1f
?的定義。【注6】 如果用第 4.3 節(jié)上的conc1
?的話(huà),這個(gè)宏也可以寫(xiě)成:
(define-modify-macro conc1f (obj) conc1)
在一種情況下,[示例代碼 12.2] 中的宏應(yīng)該限制使用。如果你正準(zhǔn)備通過(guò)在結(jié)尾處追加元素的方式來(lái)構(gòu)造列表,那么最好用?push
?,最后再?nreverse
?這個(gè)列表。在列表的開(kāi)頭處理數(shù)據(jù)比在結(jié)尾要方便些,因?yàn)樵诮Y(jié)尾處處理數(shù)據(jù)的話(huà),你首先得到那里。Common Lisp 有許多用于前者的操作符,而適用于后者的操作符則屈指可數(shù),這很可能是為了鼓勵(lì)程序員設(shè)計(jì)更高效率的程序。
并非所有基于 setf 的宏都可以用 define-modify-macro 定義。比如說(shuō),假設(shè)我們想要定義一個(gè)宏 _f ,讓它破壞性把函數(shù)應(yīng)用于一個(gè)廣義變量。內(nèi)置宏 incf 就相當(dāng)于使用了 + 的 setf 的縮寫(xiě)。把:
(setf x (+ x y))
取而代之,我們只需說(shuō):
(incf x y)
新的宏?_f
?就是上述思路的推廣:incf
?能展開(kāi)成對(duì)?+
?的調(diào)用,而?_f
?則會(huì)展開(kāi)成對(duì)由第一個(gè)參數(shù)給出操作符的調(diào)用。例如,在第 8.3 節(jié) scale-objs 的定義里,我們必須這樣寫(xiě):
(setf (obj-dx o) (* (obj-dx o) factor))
改用?_f
?的話(huà),將變成:
(_f * (obj-dx o) factor)
_f
?可能會(huì)被錯(cuò)寫(xiě)成:
(defmacro _f (op place &rest args) ; wrong
'(setf ,place (,op ,place ,@args)))
不幸的是,我們無(wú)法用?define-modify-macro
?正確無(wú)誤地定義?_f
?,因?yàn)閼?yīng)用到廣義變量上的操作符是由參數(shù)給定的。
這類(lèi)更復(fù)雜的宏必須由手工編寫(xiě)。為了讓這種宏的編寫(xiě)方便些,Common Lisp
?提供了函數(shù)?get-setf-expansion
?【注7】,它接受一個(gè)廣義變量并返回所有用于獲取和設(shè)置其值的必要信息。通過(guò)為下面表達(dá)式手工生成展開(kāi)式,我們將了解如何使用這些信息:
(incf (aref a (incf i)))
當(dāng)我們對(duì)廣義變量調(diào)用?get-setf-expansion
?時(shí),可以得到五個(gè)值用作宏展開(kāi)式的原材料:
> (get-setf-expansion '(aref a (incf i)))
(#:G4 #:G5)
(A (INCF I))
(#:G6)
(SYSTEM:SET-AREF #:G6 #:G4 #:G5)
(AREF #:G4 #:G5)
最開(kāi)始的兩個(gè)值分別是臨時(shí)變量列表,以及應(yīng)該給它們賦的值。因此,我們可以這樣開(kāi)始展開(kāi)式:
(let* ((#:g4 a)
(#:g5 (incf i)))
...)
這些綁定應(yīng)該在?let*
?里創(chuàng)建。因?yàn)橐话銇?lái)說(shuō),這些值?form
?可能會(huì)引用到前面的變量。第三【注8】和第五個(gè)值是另一個(gè)臨時(shí)變量和將返回廣義變量初值的?form
。由于我們想要在這個(gè)值上加?1
,所以把后者包在對(duì)?1+
?的調(diào)用里:
(let* ((#:g4 a)
(#:g5 (incf i))
(#:g6 (1+ (aref #:g4 #:g5))))
...)
最后,get-setf-expansion
?返回的第四個(gè)值是一個(gè)賦值的表達(dá)式,該賦值必須在新綁定環(huán)境下進(jìn)行:
(let* ((#:g4 a)
(#:g5 (incf i))
(#:g6 (1+ (aref #:g4 #:g5))))
(system:set-aref #:g6 #:g4 #:g5))
不過(guò),這個(gè)?form
?多半會(huì)引用一些內(nèi)部函數(shù),而這些內(nèi)部函數(shù)不屬于 Common Lisp 標(biāo)準(zhǔn)。通常setf
?掩蓋了這些函數(shù)的存在,但它們必須存在于某處。因?yàn)殛P(guān)于它們的所有東西都依賴(lài)于具體的實(shí)現(xiàn),所以注重可移植性的代碼應(yīng)該使用由?get-setf-expansion
?返回的這些?form
,而不是直接引用諸如?system:set-aref
?這樣的函數(shù)。
現(xiàn)在為實(shí)現(xiàn)?_f
?而編寫(xiě)的宏,所要完成的工作,幾乎和我們剛才手工展開(kāi)?incf
?時(shí)做的事情完全一樣。唯一的區(qū)別就是,不再把?let*
?里的最后一個(gè)?form
?包裝在?1+
?調(diào)用里,而是將它包裝在來(lái)自_f
?參數(shù)的一個(gè)表達(dá)式里。[示例代碼 12.3] 給出了?_f
?的定義。
[示例代碼 12.3] setf 上更復(fù)雜的宏
(defmacro _f (op place &rest args)
(multiple-value-bind (vars forms var set access)
(get-setf-expansion place)
'(let* (,@(mapcar #'list vars forms)
(,(car var) (,op ,access ,@args)))
,set)))
(defmethod pull (obj place &rest args)
(multiple-value-bind (vars forms var set access)
(get-setf-expansion place)
(let ((g (gensym)))
'(let* ((,g ,obj)
,@(mapcar #'list vars forms)
(,(car var) (delete ,g ,access ,@args)))
,set))))
(defmacro pull-if (test place &rest args)
(multiple-value-bind (vars forms var set access)
(get-setf-expansion place)
(let ((g (gensym)))
'(let* ((,g ,test)
,@(mapcar #'list vars forms)
(,(car var) (delete-if ,g ,access ,@args)))
,set))))
(defmacro popn (n place)
(multiple-value-bind (vars forms var set access)
(get-setf-expansion place)
(with-gensyms (gn glst)
'(let* ((,gn ,n)
,@(mapcar #'list vars forms)
(,glst ,access)
(,(car var) (nthcdr ,gn ,glst)))
(prog1 (subseq ,glst 0 ,gn)
,set)))))
這是個(gè)很有用的實(shí)用工具。舉個(gè)例子,現(xiàn)在在它的幫助下,我們就可以輕易地將任意有名函數(shù)替換成其記憶化(第5.3 節(jié))的等價(jià)函數(shù)。【注9】要對(duì)?foo
?進(jìn)行記憶化的處理,可以用:
(_f memoize (symbol-function 'foo))
使用?_f
?,也有助于簡(jiǎn)化其他基于?setf
?的宏的定義。例如,我們現(xiàn)在可以把?conc1f
?([示例代碼 12.2])定義成:
(defmacro conc1f (lst obj)
'(_f nconc ,lst (list ,obj)))
[示例代碼 12.3] 中還有其他一些有用的宏,它們同樣基于?setf
。下一個(gè)是?pull
?,它是內(nèi)置的pushnew
?的逆操作。
這對(duì)操作符,就像是給?push
?和?pop
?賦予了一定的鑒別能力。如果給定的新元素不是列表的成員,pushnew
?就把它加入到這個(gè)列表里面,而?pull
?則是破壞性地從列表里刪除給定的元素。pull
?定義中的?&rest
?參數(shù)使?pull
?可以接受和?delete
?相同的關(guān)鍵字參數(shù):
> (setq x '(1 2 (a b) 3))
(1 2 (A B) 3)
> (pull 2 x)
(1 (A B) 3)
> (pull '(a b) x :test #'equal)
(1 3)
> x
(1 3)
你幾乎可以把這個(gè)宏當(dāng)成這樣定義的:
(defmacro pull (obj seq &rest args) ; wrong
'(setf ,seq (delete ,obj ,seq ,@args)))
不過(guò),如果它真的這樣定義,它將同時(shí)碰到求值順序和求值次數(shù)方面的問(wèn)題。我們也可以把?pull
?定義成簡(jiǎn)單的修改宏:
(define-modify-macro pull (obj &rest args)
(lambda (seq obj &rest args)
(apply #'delete obj seq args)))
但由于修改宏必須將廣義變量作為第一個(gè)參數(shù),所以我們只得以相反的次序給出前兩個(gè)參數(shù),這樣顯得有些不自然。
更通用的?pull-if
?接受一個(gè)初始的函數(shù)參數(shù),并且會(huì)展開(kāi)成?delete-if
?而非?delete
?:
> (let ((lst '(1 2 3 4 5 6)))
(pull-if #'oddp lst)
lst)
(2 4 6)
這兩個(gè)宏說(shuō)明了另一個(gè)有普遍意義的要點(diǎn)。如果下層函數(shù)接受可選參數(shù),建立在其上的宏也應(yīng)該這樣做。
pull
?和?pull-if
?都把可選參數(shù)傳給了它們的?delete
?。
[示例代碼 12.3] 中最后一個(gè)宏是?popn
?,它是?pop
?的推廣形式。其功能不再是僅僅從列表里彈出一個(gè)元素,而是能彈出并返回任意長(zhǎng)度的子序列:
> (setq x '(a b c d e f))
(A B C D E F)
> (popn 3 x)
(A B C)
> x
(D E F)
[示例代碼 12.4] 中的宏能對(duì)它的參數(shù)排序。如果?x
?和?y
?是變量,而且我們想要確保x 的值不是兩個(gè)值中較小的那個(gè),那么我們可以寫(xiě):
(if (> y x) (rotatef x y))
但如果我們想對(duì)三個(gè)或者數(shù)量更多的變量做這個(gè)操作,所需的代碼量就會(huì)迅速膨脹。與其手工編寫(xiě)這樣的代碼,不妨讓?sortf
?來(lái)為我們代勞。這個(gè)宏接受一個(gè)比較操作符,還有任意數(shù)量的廣義變量,然后不斷交換它們的值,直到這些廣義變量的順序符合操作符的要求。在最簡(jiǎn)單的情形,參數(shù)可以是普通變量:
[示例代碼 12.4] 一個(gè)排序其參數(shù)的宏
(defmacro sortf (op &rest places)
(let* ((meths (mapcar #'(lambda (p)
(multiple-value-list
(get-setf-expansion p)))
places))
(temps (apply #'append (mapcar #'third meths))))
'(let* ,(mapcar #'list
(mapcan #'(lambda (m)
(append (first m)
(third m)))
meths)
(mapcan #'(lambda (m)
(append (second m)
(list (fifth m))))
meths))
,@(mapcon #'(lambda (rest)
(mapcar
#'(lambda (arg)
'(unless (,op ,(car rest) ,arg)
(rotated ,(car rest) ,arg)))
(cdr rest)))
temps)
,@(mapcar #'fourth meths))))
> (setq x 1 y 2 z 3)
3
> (sortf > x y z)
3
> (list x y z)
(3 2 1)
一般情況下,它們可以是任何可逆的表達(dá)式。假設(shè)?cake
?是一個(gè)可逆函數(shù),它能返回某人的蛋糕,而bigger
?是個(gè)針對(duì)蛋糕的比較函數(shù)。如果我們想要推行一個(gè)規(guī)定,要求?moe
?的?cake
?不得小于larry
?的?cake
?,而后者的?cake
?也不得小于?curly
?的,我們寫(xiě)成:
(sortf bigger (cake 'moe) (cake 'larry) (cake 'curly))
sortf
?的定義的大致結(jié)構(gòu)和?_f
?差不多。它以一個(gè)?let*
?開(kāi)始,在這個(gè)?let*
?表達(dá)式中,由?get-setf-expansion
?返回的臨時(shí)變量被綁定到廣義變量的初始值上。sortf
?的核心是中間的?mapcon
表達(dá)式,該表達(dá)式生成的代碼將被用來(lái)對(duì)這些臨時(shí)變量進(jìn)行排序。宏的這部分生成的代碼量會(huì)隨著參數(shù)個(gè)數(shù)以指數(shù)速度增長(zhǎng)。在排序之后,廣義變量會(huì)被用那些由?get-setf-expansion
?返回的?form
重新賦值。這里使用的算法是 的冒泡排序,但如果調(diào)用的時(shí)候參數(shù)非常多的話(huà),這個(gè)宏就不適用了。
[示例代碼 12.5] 給出的是對(duì) sortf 調(diào)用的展開(kāi)式。在最前面的 let* 中,參數(shù)和它們的 subform 按照從左到右的順序小心地求值。之后出現(xiàn)的三個(gè)表達(dá)式分別比較幾個(gè)臨時(shí)變量的值,有可能還會(huì)交換它們:先是比較第一個(gè)和第二個(gè),接著是第一個(gè)和第三個(gè),然后第二個(gè)和第三個(gè)。最后廣義變量從左到右被重新賦值。盡管很少需要注意這個(gè)問(wèn)題,但還是提一下:通常,宏參數(shù)應(yīng)該按從左到右的順序進(jìn)行賦值,這和它們求值的順序是一致的。
有些操作符,如?_f
?和?sortf
?,它們與接受函數(shù)型參數(shù)的函數(shù)之間確實(shí)有相似之處。不過(guò)也應(yīng)該認(rèn)識(shí)到它們是完全不同的東西。類(lèi)似?find-if
?的函數(shù)接受一個(gè)函數(shù)并調(diào)用它;而類(lèi)似?_f
?的宏接受的則是一個(gè)名字,這些宏會(huì)讓它成為一個(gè)表達(dá)式的?car
。讓?_f
?和?sortf
?都接受函數(shù)型參數(shù)也不無(wú)可能。例如,_f
?可以這樣實(shí)現(xiàn):
(sortf > x (aref ar (incf i)) (car lst))
展開(kāi)(在某個(gè)可能的實(shí)現(xiàn)里) 成:
[示例代碼 12.5] 一個(gè) sortf 調(diào)用的展開(kāi)式
(let* ((#:g1 x)
(#:g4 ar)
(#:g3 (incf i))
(#:g2 (aref #:g4 #:g3))
(#:g6 lst)
(#:g5 (car #:g6)))
(unless (> #:g1 #:g2)
(rotatef #:g1 #:g2))
(unless (> #:g1 #:g5)
(rotatef #:g1 #:g5))
(unless (> #:g2 #:g5)
(rotatef #:g2 #:g5))
(setq x #:g1)
(system:set-aref #:g2 #:g4 #:g3)
(system:set-car #:g6 #:g5))
(defmacro _f (op place &rest args)
(let ((g (gensym)))
(multiple-value-bind (vars forms var set access)
(get-setf-expansion place)
'(let* ((,g ,op)
,@(mapcar #'list vars forms)
(,(car var) (funcall ,g ,access ,@args)))
,set))))
然后調(diào)用?(_f #'+ x 1)
。但是?_f
?原來(lái)的版本不但擁有這個(gè)版本的所有功能,而且由于它處理的是名字,所以它還可以接受宏或者?special form
?的名字。就像?+
?那樣,比如說(shuō),你還可以調(diào)用nif
?(102頁(yè)):
> (let ((x 2))
(_f nif x 'p 'z 'n)
x)
P
12.1 節(jié)說(shuō)明了一個(gè)道理:如果一個(gè)宏調(diào)用能展開(kāi)成可逆引用,那么它本身應(yīng)該也是可逆的。不過(guò),你也用不著只是為了可逆,就把操作符定義成宏。通過(guò)使用?defsetf
?,你可以告訴?Lisp
?如何對(duì)任意的函數(shù)或宏調(diào)用求逆。
使用這個(gè)宏的方法有兩種。在最簡(jiǎn)單的情況下,它的參數(shù)是兩個(gè)符號(hào):
(defsetf symbol-value set)
如果用更復(fù)雜的方法,那么?defsetf
?的調(diào)用和?defmacro
?調(diào)用會(huì)有幾分相似,它另外帶有一個(gè)參數(shù)用于更新值?form
。例如,下式可以為?car
?定義一種可能的逆:
(defsetf car (lst) (new-car)
'(progn (rplaca ,lst ,new-car)
,new-car))
defmacro
?和?defsetf
?之間有一個(gè)重要的區(qū)別:后者會(huì)自動(dòng)為其參數(shù)創(chuàng)建生成符號(hào)(gensym)。通過(guò)上面給出的定義,(setf (car x) y)
?將展開(kāi)成:
(let* ((#:g2 x)
(#:g1 y))
(progn (rplaca #:g2 #:g1)
#:g1))
這樣,我們寫(xiě)?defsetf
?展開(kāi)器時(shí)就沒(méi)有后顧之憂(yōu),不用擔(dān)心諸如變量捕捉,或者求值的次數(shù)和順序之類(lèi)的問(wèn)題了。
在?CLTL2?的 Common Lisp 中,也可以直接用?defun
?定義?setf
?的逆。因而前面的示例也可以寫(xiě)成:
(defun (setf car) (new-car lst)
(rplaca lst new-car)
new-car)
新的值應(yīng)該作為這個(gè)函數(shù)的第一個(gè)參數(shù)。同樣按照習(xí)慣,也應(yīng)該把這個(gè)值作為函數(shù)的返回值。
目前為止的示例都認(rèn)為,廣義變量應(yīng)該指向數(shù)據(jù)結(jié)構(gòu)中的某個(gè)位置。不法之徒把人質(zhì)帶進(jìn)地牢,而見(jiàn)義勇為之士則讓她重見(jiàn)天日;他們移動(dòng)的路徑相同,但方向相反。所以,如果人們覺(jué)得?setf
?的工作方式也只能是這樣,那不足為奇,因?yàn)樗蓄A(yù)定義的逆看上去都是如此;確實(shí),習(xí)慣上,將被求逆的參數(shù)也常會(huì)使用?place
?作為其參數(shù)名。
從理論上說(shuō),setf
?可以更一般化:accessform
?和它的逆的操作對(duì)象甚至可以不是同種數(shù)據(jù)結(jié)構(gòu)。假設(shè)在某個(gè)應(yīng)用里,我們想要把數(shù)據(jù)庫(kù)的更新緩存起來(lái)。這可能是迫不得已的,舉例來(lái)說(shuō),倘若每次修改數(shù)據(jù),都即時(shí)完成真正的更新操作,就有可能會(huì)降低效率,或者,如果要求所有的更新都必須在提交之前驗(yàn)證一致性,那就必須引入緩存的機(jī)制。
[示例代碼 12.6] 一個(gè)非對(duì)稱(chēng)的逆轉(zhuǎn)換
(defvar *cache* (make-hash-table))
(defun retrieve (key)
(multiple-value-bind (x y) (gethash key *cache*)
(if y
(values x y)
(cdr (assoc key *world*)))))
(defsetf retrieve (key) (val)
'(setf (gethash ,key *cache*) ,val))
假設(shè)?\*world\*
?是實(shí)際的數(shù)據(jù)庫(kù)。為簡(jiǎn)單起見(jiàn),我們將它做成一個(gè)元素為?(key . val)
?形式的關(guān)聯(lián)表(assoc-list)。[示例代碼 12.6] 顯示了一個(gè)稱(chēng)為?retrieve
?的查詢(xún)函數(shù)。如果?\*world\*
?是:
((a . 2) (b . 16) (c . 50) (d . 20) (f . 12))
那么:
> (retrieve 'c)
50
和?car
?的調(diào)用不同,retrieve
?調(diào)用并不指向一個(gè)數(shù)據(jù)結(jié)構(gòu)中的特定位置。返回值可能來(lái)自?xún)蓚€(gè)位置里的
一個(gè)。而?retrieve
?的逆,同樣定義在 [示例代碼 12.6] 中,僅指向它們中的一個(gè):
> (setf (retrieve 'n) 77)
77
> (retrieve 'n)
77
T
該查詢(xún)返回第二個(gè)值?t
?,以表明在緩存中找到了答案。
就像宏一樣,廣義變量是一種威力非凡的抽象機(jī)制。這里肯定還有更多的東西有待發(fā)掘。當(dāng)然,有的用戶(hù)很可能已經(jīng)發(fā)現(xiàn)了一些使用廣義變量的方法,使用這些方法能得到更優(yōu)雅和強(qiáng)大的程序。但也不排除以全新的方式使用?setf
?逆的可能性,或者發(fā)現(xiàn)其它類(lèi)似的有用的變換技術(shù)。
備注:
【注1】這個(gè)定義是錯(cuò)誤的,下一節(jié)將給出解釋。
【注2】一般意義上的函數(shù)名:1+
?或者?(lambda (x) (+ x 1))
?都可以。
【注3】譯者注:現(xiàn)行 Common Lisp 標(biāo)準(zhǔn) (CLHS) 事實(shí)上要求?define-modify-macro
?和?define-compiler-macro
?的第三個(gè)參數(shù)的類(lèi)型必須是符號(hào)。
【注4】譯者注:這里根據(jù)現(xiàn)行 Common Lisp 標(biāo)準(zhǔn)對(duì)源代碼加以修改,我們額外定義了兩個(gè)輔助函數(shù)以確保?define-modify-macro
?的第三個(gè)參數(shù)只能是符號(hào)。
【注5】譯者注:當(dāng)作為?nconc
?第一個(gè)參數(shù)的變量為空列表,也就是?nil
?時(shí),該變量在?nconc
?執(zhí)行之后將仍是?nil
?,而不是整個(gè)?nconc
?表達(dá)式的那個(gè)相當(dāng)于其第二個(gè)參數(shù)的值。
【注6】譯者注:正如前面兩個(gè)腳注里提到的那樣,Common Lisp 標(biāo)準(zhǔn)并沒(méi)有定義?define-modify-macro
?的第三個(gè)參數(shù)可以是符號(hào)之外的其他東西,盡管λ表達(dá)式出現(xiàn)在一個(gè)函數(shù)調(diào)用形式的函數(shù)位置上確實(shí)是合法的。原書(shū)作者試圖通過(guò)類(lèi)比來(lái)說(shuō)明 λ表達(dá)式用在?define-modify-macro
?中的合法性,這是不恰當(dāng)?shù)?,?qǐng)讀者注意。
【注7】譯者注:原書(shū)中給出的函數(shù)實(shí)際上是?get-setf-method
?,但這個(gè)函數(shù)已經(jīng)不在現(xiàn)行 Common Lisp 標(biāo)準(zhǔn)中了,參見(jiàn)?X3J13 Issue 308
:SETF-METHOD-VS-SETF-METHOD
?取代它的是get-setf-expansion
?,這個(gè)函數(shù)接受兩個(gè)參數(shù),place
?以及可選的?environment
?環(huán)境參數(shù)。本書(shū)后面對(duì)于所有采用?get-setf-method
?的地方一律直接改用?get-setf-expansion
?,不再另行說(shuō)明。
【注8】第三個(gè)值當(dāng)前總是一個(gè)單元素列表。它被返回成一個(gè)列表來(lái)提供(目前為止還不可能)在廣義變量中保存多值的可能性。
【注9】然而,內(nèi)置函數(shù)是個(gè)例外,它們不應(yīng)該以這種方式被記憶化。Common Lisp 禁止重定義內(nèi)置函數(shù)。
更多建議: