请注意 (car lst) 表单,即已经定义了 setf 扩展器的实际访问器,是如何在两个 defun 中的。
但这只是在宏观扩张之前显然是正确的。在您的设置器中,(car lst)表单是分配的目标。它将扩展为其他内容,例如对类似于以下内容的某些内部函数的调用rplaca:
您可以手动执行类似的操作:
(defun new-car (lst)
(car lst))
(defun (setf new-car) (new-value lst)
(rplaca lst new-value)
new-value)
瞧;您不再有重复调用car;getter 调用car和 setter调用rplaca。
请注意,我们必须手动返回new-value,因为rplaca返回lst。
你会发现在许多 Lisps 中,内置的setf扩展器 forcar使用了一个替代函数(可能是命名sys:rplaca的,或者随之而来的变体),它返回分配的值。
在 Common Lisp 中定义新类型的地方时,我们通常最小化代码重复的方法是使用define-setf-expander.
使用这个宏,我们将一个新的地点符号与两个项目相关联:
- 一个宏 lambda 列表,它定义了该地点的语法。
- 一段代码,计算并返回五条信息,作为五个返回值。这些统称为“
setf膨胀”。
位置变异宏像setf使用宏 lambda 列表来解构位置语法并调用计算这五个部分的代码体。然后使用这五个部分来生成位置访问/更新代码。
不过请注意,setf扩展的最后两项是store form和access form。我们无法摆脱这种二元性。如果我们setf为类似地方定义扩展car,我们的访问表单将调用car并且存储表单将基于rplaca,确保返回新值,就像在上面的两个函数中一样。
但是,可能存在可以在访问和存储之间共享重要内部计算的地方。
假设我们定义my-cadar而不是my-car:
(defun new-cadar (lst)
(cadar lst))
(defun (setf new-cadar) (new-value lst)
(rplaca (cdar lst) new-value)
new-value)
请注意,如果我们这样做 (incf (my-cadar place)),则会浪费重复遍历列表结构,因为cadar调用它以获取旧值,然后cdar再次调用以计算存储新值的单元格。
通过使用更难和更低级别的define-setf-expander接口,我们可以拥有它,以便cdar在访问表单和存储表单之间共享计算。也就是说,(incf (my-cadar x))将计算(cadr x)一次并将其存储到临时变量#:c中。然后更新将通过访问(car #:c)、添加 1 并将其存储在 中来进行(rplaca #:c ...)。
这看起来像:
(define-setf-expander my-cadar (cell)
(let ((cell-temp (gensym))
(new-val-temp (gensym)))
(values (list cell-temp) ;; these syms
(list `(cdar ,cell)) ;; get bound to these forms
(list new-val-temp) ;; these vars receive the values of access form
;; this form stores the new value(s) into the place:
`(progn (rplaca ,cell-temp ,new-val-temp) ,new-val-temp)
;; this form retrieves the current value(s):
`(car ,cell-temp))))
测试:
[1]> (macroexpand '(incf (my-cadar x)))
(LET* ((#:G3318 (CDAR X)) (#:G3319 (+ (CAR #:G3318) 1)))
(PROGN (RPLACA #:G3318 #:G3319) #:G3319)) ;
T
#:G3318来自cell-temp,并且#:G3319是new-val-tempgensym。
但是,请注意,上面只定义了setf扩展。有了以上,我们只能my-cadar作为一个地方使用。如果我们尝试将它作为函数调用,它就会丢失。