第 12 章 廣義變量

2018-02-24 15:54 更新

第 12 章 廣義變量

第 8 章曾提到,宏的長處之一是其變換參數(shù)的能力。setf?就是這類宏中的一員。本章將著重分析setf?的內(nèi)涵,然后以幾個宏為例,它們將建立在?setf?的基礎(chǔ)之上。

要在?setf?上編寫正確無誤的宏并非易事,其難度讓人咋舌。為了介紹這個主題,第一節(jié)會先給出一個有點小問題的簡單例子。接下來的小節(jié)將解釋該宏的錯誤之處,然后展示如何改正它。第三和第四節(jié)會介紹一些基于?setf?的實用工具的例子,而最后一節(jié)則會說明如何定義你自己的?setf?逆變換。

12.1 概念

內(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、cdrnth、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é)。

12.2 多重求值問題

上一節(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)

12.3 新的實用工具

本節(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?和?pushnewconc1f?在列表結(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è)計更高效率的程序。

12.4 更復(fù)雜的實用工具

并非所有基于 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.5 定義逆

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 308SETF-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ù)。

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號