第 12 章 广义变量
第 8 章曾提到,宏的长处之一是其变换参数的能力。setf
就是这类宏中的一员。本章将着重分析 setf
的内涵,然后以几个宏为例,它们将建立在 setf
的基础之上。
要在 setf
上编写正确无误的宏并非易事,其难度让人咋舌。为了介绍这个主题,第一节会先给出一个有点小问题的简单例子。接下来的小节将解释该宏的错误之处,然后展示如何改正它。第三和第四节会介绍一些基于 setf
的实用工具的例子,而最后一节则会说明如何定义你自己的 setf
逆变换。
12.1 概念
内置宏 setf
是 setq
的推广形式。setf
的第一个参数可以是个函数调用而非简单的变量:
> (setq lst '(a b c))
(A B C)
> (setf (car lst) 480)
480
> lst
(480 B C)
一般而言,(setf x y)
可以理解成 "务必让 x 的求值结果为 y"。作为一个宏,setf
得以深入到参数内部,弄清需要做哪些工作,才能满足这个要求。如果第一个参数(在宏展开以后) 是个符号,那么 setf
就只会展开成 setq。但如果第一个参数是个查询语句,那么 setf
则会展开到对应的断言上。由于第二个参数是常量,所以前面的例子可以展开成:
(progn (rplaca lst 480) 480)
这种从查询到断言的变换被称为逆变换。Common Lisp 中所有最常用的访问函数都有预定义的逆,包括 car
、cdr
、nth
、aref
、get
、gethash
,以及那些由 defstruct
创建的访问函数。( 完整的名单见 CLTL2 的第 125 页。)
能充当 setf
第一个参数的表达式被称为广义变量。广义变量已经成为了一种强有力的抽象机制。宏调用和广义变量的相似之处在于:一个宏调用,只要能展开成可逆引用,那么其本身就一定是可逆的。
当我们也加入这个行列,基于 setf
编写自己的宏时,这种组合可以产生显而易见更清爽的程序。我们可以在 setf
上面定义的宏有很多,其中一个是 toggle
:【注1】
(defmacro toggle (obj)
'(setf ,obj (not ,obj)))
它可以反转一个广义变量的值:
> (let ((lst '(a b c)))
(toggle (car lst))
lst)
(NIL B C)
现在考虑下面的应用。假设有个人,他可能是个肥皂剧作者、精力充沛的好事者,或是居委会大妈,想要维护一个数据库。其中记录着小镇上所有居民之间的种种恩怨情仇。在数据库里的表里,其中有一张便是用来保存朋友关系的:
(defvar *friends* (make-hash-table))
这个哈希表的表项本身也是哈希表,其中,潜在的朋友被映射到 t
或者 nil
:
(setf (gethash 'mary *friends*) (make-hash-table))
为了使 John 成为 Mary 的朋友,我们可以说:
(setf (gethash 'john (gethash 'mary *friends*)) t)
这个镇被分为两派。正如帮派的传统,每个人都声称 "凡人非友即敌",所以镇上所有人都被迫加入一方或者另一方。这样当某人转变立场时,他所有的朋友都变成敌人,而所有的敌人则变成朋友。
如果只用内置的操作符来切换 x
和 y
的敌友关系,我们必须这样说:
(setf (gethash x (gethash y *friends*))
(not (gethash x (gethash y *friends*))))
尽管去掉 setf
后要简单许多,这个表达式还是相当复杂。倘若我们为数据库定义了一个访问宏,如下:
(defmacro friend-of (p q)
'(gethash ,p (gethash ,q *friends*)))
那么在这个宏和 toggle
的协助下,我们就得以更方便地修改数据库的数据。前面那个更新数据库的语句可以简化成:
(toggle (friend-of x y))
广义变量就像是美味的健康食品。它们能让你的程序良好地模块化,同时变得更为优雅。如果你给出宏或者可逆函数,用来访问你的数据结构,那么其他模块就可以使用 setf
来修改你的数据结构而无需了解其内部细节。
12.2 多重求值问题
上一节曾警告说,我们最初的 toggle
定义是不正确的:
(defmacro toggle (obj) ; wrong
'(setf ,obj (not ,obj)))
它会碰到第 10.1 节里提到的多重求值问题。如果它的参数有副作用,那麻烦就来了。比如说,若 lst
是一个对象列表,我们这样写:
(toggle (nth (incf i) lst))
并期待它能反转第 (i+1)
个元素。事与愿违,如果使用 toggle
现在的定义,这个调用将展开成:
(setf (nth (incf i) lst)
(not (nth (incf i) lst)))
这会使 i 递增两次,并且将第 (i+1)
个元素设置成第 (i+2)
个元素的反。所以在本例中:
> (let ((lst '(t nil t))
(i -1))
(toggle (nth (incf i) lst))
lst)
(T NIL T)
调用 toggle
毫无效果。
仅仅把作为 toggle
参数给出的表达式插入到 setf
的第一个参数的位置上还不够。我们必须深入到表达式内部,看看它到底做了什么:如果它含有 subform
,而且这些 subform
有副作用的话,我们就需要把它们分开,并单独求值。一般而言,这件事情并不那么简单。
为了让问题容易些,Common Lisp 提供了一个宏,它可以帮助我们自动定义一些基于 setf
的宏,不过适用范围有限。宏的名字叫 define-modify-macro
,它接受三个参数:被定义宏的宏名,它的附加参数(出现在广义变量之后),以及一个函数名,这个函数将为广义变量产生新值。【注2】【注3】
使用 define-modify-macro
,我们可以像下面这样定义 toggle
:
(define-modify-macro toggle () not)
具体说,就是 "若要求值形如 (toggle place) 的表达式,应该先找到 place
指定的位置,并且,如果保存在那里的值是 val
,将其替换成 (not val)
的值"。下面把这个新宏用在原来的例子里:
> (let ((lst '(t nil t))
(i -1))
(toggle (nth (incf i) lst))
lst)
(NIL NIL T)
虽然这个版本正确无误地给出了结果,但它本可以写得更通用些。由于 setf
和 setq
两者对其参数数量都没有限制,toggle
也应如此。我们可以通过在修改宏 (modify-macro) 的基础上定义另一个宏,来赋予它这种能力,如 [示例代码 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 新的实用工具
本节将给出一些新的实用工具为例,我们用它们对广义变量进行操作。这些实用工具必须是宏,以便将参数原封不动地传给 setf
。
[示例代码 12.1] 中有四个基于 setf
的新宏。第一个是 allf
,它被用来将同一值赋给多个广义变量。nilf
和 tf
就是基于它实现的,它们分别将参数设置 为 nil
和 t
。虽然这些宏很简单,但是方便实用。
和 setq
一样,setf
也可以接受多个参数 -- 即交替出现的变量和对应的值:
(setf x 1 y 2)
这些新的实用工具同样有这个能力,而且只用传原来一半的参数就可以了。如果你想要把多个变量初始化为 nil
,那么可以不再使用:
(setf x nil y nil z nil)
而改成说:
(nilf x y z)
就行了。最后一个宏是前一节曾介绍过的 toggle
:它和 nilf
差不多,但给每个参数设置的是真值的反。
这四个宏说明了关于赋值操作符的一个要点。就算我们只需要对普通变量使用一个操作符,而把这个操作符号展开成 setf
而非 setq
,这样做,有百利而无一害。如果第一个参数是符号,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】包含三个破坏性修改列表结尾的宏。第 3.1 节提到依赖
(nconc x y)
的副作用是不可靠的,并且必须改成:【注5】
(setq x (nconc x y))
这一习惯用法被嵌入在 concf
中了。更特殊的 conc1f
和 concnew
就像是用于列表另一端的 push
和 pushnew
,conc1f
在列表结尾追加一个元素,而 concnew
的功能相同,但只有当这个元素不在列表中时才会动作。
第 2.2 节曾提到,函数的名字既可以是符号,也可以是–表达式。因此,把整个λ表达式作为第三个参数传给 define-modify-macro
也是可行的,正如 conc1f
的定义。【注6】 如果用第 4.3 节上的 conc1
的话,这个宏也可以写成:
(define-modify-macro conc1f (obj) conc1)
在一种情况下,[示例代码 12.2] 中的宏应该限制使用。如果你正准备通过在结尾处追加元素的方式来构造列表,那么最好用 push
,最后再 nreverse
这个列表。在列表的开头处理数据比在结尾要方便些,因为在结尾处处理数据的话,你首先得到那里。Common Lisp 有许多用于前者的操作符,而适用于后者的操作符则屈指可数,这很可能是为了鼓励程序员设计更高效率的程序。
12.4 更复杂的实用工具
并非所有基于 setf 的宏都可以用 define-modify-macro 定义。比如说,假设我们想要定义一个宏 _f ,让它破坏性把函数应用于一个广义变量。内置宏 incf 就相当于使用了 + 的 setf 的缩写。把:
(setf x (+ x y))
取而代之,我们只需说:
(incf x y)
新的宏 _f
就是上述思路的推广:incf
能展开成对 +
的调用,而 _f
则会展开成对由第一个参数给出操作符的调用。例如,在第 8.3 节 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
,因为应用到广义变量上的操作符是由参数给定的。
这类更复杂的宏必须由手工编写。为了让这种宏的编写方便些,Common Lisp
提供了函数 get-setf-expansion
【注7】,它接受一个广义变量并返回所有用于获取和设置其值的必要信息。通过为下面表达式手工生成展开式,我们将了解如何使用这些信息:
(incf (aref a (incf i)))
当我们对广义变量调用 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)
最开始的两个值分别是临时变量列表,以及应该给它们赋的值。因此,我们可以这样开始展开式:
(let* ((#:g4 a)
(#:g5 (incf i)))
...)
这些绑定应该在 let*
里创建。因为一般来说,这些值 form
可能会引用到前面的变量。第三【注8】和第五个值是另一个临时变量和将返回广义变量初值的 form
。由于我们想要在这个值上加 1
,所以把后者包在对 1+
的调用里:
(let* ((#:g4 a)
(#:g5 (incf i))
(#:g6 (1+ (aref #:g4 #:g5))))
...)
最后,get-setf-expansion
返回的第四个值是一个赋值的表达式,该赋值必须在新绑定环境下进行:
(let* ((#:g4 a)
(#:g5 (incf i))
(#:g6 (1+ (aref #:g4 #:g5))))
(system:set-aref #:g6 #:g4 #:g5))
不过,这个 form
多半会引用一些内部函数,而这些内部函数不属于 Common Lisp 标准。通常 setf
掩盖了这些函数的存在,但它们必须存在于某处。因为关于它们的所有东西都依赖于具体的实现,所以注重可移植性的代码应该使用由 get-setf-expansion
返回的这些 form
,而不是直接引用诸如 system:set-aref
这样的函数。
现在为实现 _f
而编写的宏,所要完成的工作,几乎和我们刚才手工展开 incf
时做的事情完全一样。唯一的区别就是,不再把 let*
里的最后一个 form
包装在 1+
调用里,而是将它包装在来自 _f
参数的一个表达式里。[示例代码 12.3] 给出了 _f
的定义。
[示例代码 12.3] setf 上更复杂的宏
(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)))))
这是个很有用的实用工具。举个例子,现在在它的帮助下,我们就可以轻易地将任意有名函数替换成其记忆化(第5.3 节)的等价函数。【注9】要对 foo
进行记忆化的处理,可以用:
(_f memoize (symbol-function 'foo))
使用 _f
,也有助于简化其他基于 setf
的宏的定义。例如,我们现在可以把 conc1f
([示例代码 12.2])定义成:
(defmacro conc1f (lst obj)
'(_f nconc ,lst (list ,obj)))
[示例代码 12.3] 中还有其他一些有用的宏,它们同样基于 setf
。下一个是 pull
,它是内置的 pushnew
的逆操作。
这对操作符,就像是给 push
和 pop
赋予了一定的鉴别能力。如果给定的新元素不是列表的成员,pushnew
就把它加入到这个列表里面,而 pull
则是破坏性地从列表里删除给定的元素。pull
定义中的 &rest
参数使 pull
可以接受和 delete
相同的关键字参数:
> (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)
你几乎可以把这个宏当成这样定义的:
(defmacro pull (obj seq &rest args) ; wrong
'(setf ,seq (delete ,obj ,seq ,@args)))
不过,如果它真的这样定义,它将同时碰到求值顺序和求值次数方面的问题。我们也可以把 pull
定义成简单的修改宏:
(define-modify-macro pull (obj &rest args)
(lambda (seq obj &rest args)
(apply #'delete obj seq args)))
但由于修改宏必须将广义变量作为第一个参数,所以我们只得以相反的次序给出前两个参数,这样显得有些不自然。
更通用的 pull-if
接受一个初始的函数参数,并且会展开成 delete-if
而非 delete
:
> (let ((lst '(1 2 3 4 5 6)))
(pull-if #'oddp lst)
lst)
(2 4 6)
这两个宏说明了另一个有普遍意义的要点。如果下层函数接受可选参数,建立在其上的宏也应该这样做。
pull
和 pull-if
都把可选参数传给了它们的 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] 中的宏能对它的参数排序。如果 x
和 y
是变量,而且我们想要确保x 的值不是两个值中较小的那个,那么我们可以写:
(if (> y x) (rotatef x y))
但如果我们想对三个或者数量更多的变量做这个操作,所需的代码量就会迅速膨胀。与其手工编写这样的代码,不妨让 sortf
来为我们代劳。这个宏接受一个比较操作符,还有任意数量的广义变量,然后不断交换它们的值,直到这些广义变量的顺序符合操作符的要求。在最简单的情形,参数可以是普通变量:
[示例代码 12.4] 一个排序其参数的宏
(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)
一般情况下,它们可以是任何可逆的表达式。假设 cake
是一个可逆函数,它能返回某人的蛋糕,而 bigger
是个针对蛋糕的比较函数。如果我们想要推行一个规定,要求 moe
的 cake
不得小于 larry
的 cake
,而后者的 cake
也不得小于 curly
的,我们写成:
(sortf bigger (cake 'moe) (cake 'larry) (cake 'curly))
sortf
的定义的大致结构和 _f
差不多。它以一个 let*
开始,在这个 let*
表达式中,由 get-setf-expansion
返回的临时变量被绑定到广义变量的初始值上。sortf
的核心是中间的 mapcon
表达式,该表达式生成的代码将被用来对这些临时变量进行排序。宏的这部分生成的代码量会随着参数个数以指数速度增长。在排序之后,广义变量会被用那些由 get-setf-expansion
返回的 form
重新赋值。这里使用的算法是 的冒泡排序,但如果调用的时候参数非常多的话,这个宏就不适用了。
[示例代码 12.5] 给出的是对 sortf 调用的展开式。在最前面的 let* 中,参数和它们的 subform 按照从左到右的顺序小心地求值。之后出现的三个表达式分别比较几个临时变量的值,有可能还会交换它们:先是比较第一个和第二个,接着是第一个和第三个,然后第二个和第三个。最后广义变量从左到右被重新赋值。尽管很少需要注意这个问题,但还是提一下:通常,宏参数应该按从左到右的顺序进行赋值,这和它们求值的顺序是一致的。
有些操作符,如 _f
和 sortf
,它们与接受函数型参数的函数之间确实有相似之处。不过也应该认识到它们是完全不同的东西。类似 find-if
的函数接受一个函数并调用它;而类似 _f
的宏接受的则是一个名字,这些宏会让它成为一个表达式的 car
。让 _f
和 sortf
都接受函数型参数也不无可能。例如,_f
可以这样实现:
(sortf > x (aref ar (incf i)) (car lst))
展开(在某个可能的实现里) 成:
[示例代码 12.5] 一个 sortf 调用的展开式
(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))))
然后调用 (_f #'+ x 1)
。但是 _f
原来的版本不但拥有这个版本的所有功能,而且由于它处理的是名字,所以它还可以接受宏或者 special form
的名字。就像 +
那样,比如说,你还可以调用 nif
(102页):
> (let ((x 2))
(_f nif x 'p 'z 'n)
x)
P
12.5 定义逆
12.1 节说明了一个道理:如果一个宏调用能展开成可逆引用,那么它本身应该也是可逆的。不过,你也用不着只是为了可逆,就把操作符定义成宏。通过使用 defsetf
,你可以告诉 Lisp
如何对任意的函数或宏调用求逆。
使用这个宏的方法有两种。在最简单的情况下,它的参数是两个符号:
(defsetf symbol-value set)
如果用更复杂的方法,那么 defsetf
的调用和 defmacro
调用会有几分相似,它另外带有一个参数用于更新值 form
。例如,下式可以为 car
定义一种可能的逆:
(defsetf car (lst) (new-car)
'(progn (rplaca ,lst ,new-car)
,new-car))
defmacro
和 defsetf
之间有一个重要的区别:后者会自动为其参数创建生成符号(gensym)。通过上面给出的定义,(setf (car x) y)
将展开成:
(let* ((#:g2 x)
(#:g1 y))
(progn (rplaca #:g2 #:g1)
#:g1))
这样,我们写 defsetf
展开器时就没有后顾之忧,不用担心诸如变量捕捉,或者求值的次数和顺序之类的问题了。
在 CLTL2 的 Common Lisp 中,也可以直接用 defun
定义 setf
的逆。因而前面的示例也可以写成:
(defun (setf car) (new-car lst)
(rplaca lst new-car)
new-car)
新的值应该作为这个函数的第一个参数。同样按照习惯,也应该把这个值作为函数的返回值。
目前为止的示例都认为,广义变量应该指向数据结构中的某个位置。不法之徒把人质带进地牢,而见义勇为之士则让她重见天日;他们移动的路径相同,但方向相反。所以,如果人们觉得 setf
的工作方式也只能是这样,那不足为奇,因为所有预定义的逆看上去都是如此;确实,习惯上,将被求逆的参数也常会使用 place
作为其参数名。
从理论上说,setf
可以更一般化:accessform
和它的逆的操作对象甚至可以不是同种数据结构。假设在某个应用里,我们想要把数据库的更新缓存起来。这可能是迫不得已的,举例来说,倘若每次修改数据,都即时完成真正的更新操作,就有可能会降低效率,或者,如果要求所有的更新都必须在提交之前验证一致性,那就必须引入缓存的机制。
[示例代码 12.6] 一个非对称的逆转换
(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))
假设 \*world\*
是实际的数据库。为简单起见,我们将它做成一个元素为 (key . val)
形式的关联表(assoc-list)。[示例代码 12.6] 显示了一个称为 retrieve
的查询函数。如果 \*world\*
是:
((a . 2) (b . 16) (c . 50) (d . 20) (f . 12))
那么:
> (retrieve 'c)
50
和 car
的调用不同,retrieve
调用并不指向一个数据结构中的特定位置。返回值可能来自两个位置里的
一个。而 retrieve
的逆,同样定义在 [示例代码 12.6] 中,仅指向它们中的一个:
> (setf (retrieve 'n) 77)
77
> (retrieve 'n)
77
T
该查询返回第二个值 t
,以表明在缓存中找到了答案。
就像宏一样,广义变量是一种威力非凡的抽象机制。这里肯定还有更多的东西有待发掘。当然,有的用户很可能已经发现了一些使用广义变量的方法,使用这些方法能得到更优雅和强大的程序。但也不排除以全新的方式使用 setf
逆的可能性,或者发现其它类似的有用的变换技术。
备注:
【注1】这个定义是错误的,下一节将给出解释。
【注2】一般意义上的函数名:1+
或者 (lambda (x) (+ x 1))
都可以。
【注3】译者注:现行 Common Lisp 标准 (CLHS) 事实上要求 define-modify-macro
和 define-compiler-macro
的第三个参数的类型必须是符号。
【注4】译者注:这里根据现行 Common Lisp 标准对源代码加以修改,我们额外定义了两个辅助函数以确保 define-modify-macro
的第三个参数只能是符号。
【注5】译者注:当作为 nconc
第一个参数的变量为空列表,也就是 nil
时,该变量在 nconc
执行之后将仍是 nil
,而不是整个 nconc
表达式的那个相当于其第二个参数的值。
【注6】译者注:正如前面两个脚注里提到的那样,Common Lisp 标准并没有定义 define-modify-macro
的第三个参数可以是符号之外的其他东西,尽管λ表达式出现在一个函数调用形式的函数位置上确实是合法的。原书作者试图通过类比来说明 λ表达式用在 define-modify-macro
中的合法性,这是不恰当的,请读者注意。
【注7】译者注:原书中给出的函数实际上是 get-setf-method
,但这个函数已经不在现行 Common Lisp 标准中了,参见 X3J13 Issue 308
:SETF-METHOD-VS-SETF-METHOD
取代它的是 get-setf-expansion
,这个函数接受两个参数,place
以及可选的 environment
环境参数。本书后面对于所有采用 get-setf-method
的地方一律直接改用 get-setf-expansion
,不再另行说明。
【注8】第三个值当前总是一个单元素列表。它被返回成一个列表来提供(目前为止还不可能)在广义变量中保存多值的可能性。
【注9】然而,内置函数是个例外,它们不应该以这种方式被记忆化。Common Lisp 禁止重定义内置函数。