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