当前位置: 首页 > 文档资料 > On Lisp >

第 12 章 广义变量

优质
小牛编辑
130浏览
2023-12-01

第 8 章曾提到,宏的长处之一是其变换参数的能力。setf 就是这类宏中的一员。本章将着重分析 setf 的内涵,然后以几个宏为例,它们将建立在 setf 的基础之上。

要在 setf 上编写正确无误的宏并非易事,其难度让人咋舌。为了介绍这个主题,第一节会先给出一个有点小问题的简单例子。接下来的小节将解释该宏的错误之处,然后展示如何改正它。第三和第四节会介绍一些基于 setf 的实用工具的例子,而最后一节则会说明如何定义你自己的 setf 逆变换。

12.1 概念

内置宏 setfsetq 的推广形式。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 中所有最常用的访问函数都有预定义的逆,包括 carcdrntharefgetgethash,以及那些由 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)

这个镇被分为两派。正如帮派的传统,每个人都声称 "凡人非友即敌",所以镇上所有人都被迫加入一方或者另一方。这样当某人转变立场时,他所有的朋友都变成敌人,而所有的敌人则变成朋友。

如果只用内置的操作符来切换 xy 的敌友关系,我们必须这样说:

(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)

虽然这个版本正确无误地给出了结果,但它本可以写得更通用些。由于 setfsetq 两者对其参数数量都没有限制,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 ,它被用来将同一值赋给多个广义变量。nilftf 就是基于它实现的,它们分别将参数设置 为 nilt 。虽然这些宏很简单,但是方便实用。

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 中了。更特殊的 conc1fconcnew 就像是用于列表另一端的 pushpushnewconc1f 在列表结尾追加一个元素,而 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 的逆操作。

这对操作符,就像是给 pushpop 赋予了一定的鉴别能力。如果给定的新元素不是列表的成员,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)

这两个宏说明了另一个有普遍意义的要点。如果下层函数接受可选参数,建立在其上的宏也应该这样做。

pullpull-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] 中的宏能对它的参数排序。如果 xy 是变量,而且我们想要确保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 是个针对蛋糕的比较函数。如果我们想要推行一个规定,要求 moecake 不得小于 larrycake ,而后者的 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 按照从左到右的顺序小心地求值。之后出现的三个表达式分别比较几个临时变量的值,有可能还会交换它们:先是比较第一个和第二个,接着是第一个和第三个,然后第二个和第三个。最后广义变量从左到右被重新赋值。尽管很少需要注意这个问题,但还是提一下:通常,宏参数应该按从左到右的顺序进行赋值,这和它们求值的顺序是一致的。

有些操作符,如 _fsortf ,它们与接受函数型参数的函数之间确实有相似之处。不过也应该认识到它们是完全不同的东西。类似 find-if 的函数接受一个函数并调用它;而类似 _f 的宏接受的则是一个名字,这些宏会让它成为一个表达式的 car。让 _fsortf 都接受函数型参数也不无可能。例如,_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))

defmacrodefsetf 之间有一个重要的区别:后者会自动为其参数创建生成符号(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-macrodefine-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 308SETF-METHOD-VS-SETF-METHOD 取代它的是 get-setf-expansion ,这个函数接受两个参数,place 以及可选的 environment 环境参数。本书后面对于所有采用 get-setf-method 的地方一律直接改用 get-setf-expansion ,不再另行说明。

【注8】第三个值当前总是一个单元素列表。它被返回成一个列表来提供(目前为止还不可能)在广义变量中保存多值的可能性。

【注9】然而,内置函数是个例外,它们不应该以这种方式被记忆化。Common Lisp 禁止重定义内置函数。