注意 (car lst) 表单,即已经定义了 setf 扩展器的实际访问器,是如何在两个定义中的。
但这只是在宏扩展之前显然是正确的。在您的设置器中,(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 中,car 的内置 setf 扩展器使用了一个替代函数(可能名为 sys:rplaca,或其变体),它返回分配的值。
在 Common Lisp 中定义新类型的地方时,我们通常尽量减少代码重复的方法是使用 define-setf-expander。
通过这个宏,我们将一个新的地点符号与两个项目相关联:
- 定义地点语法的宏 lambda 列表。
- 计算并返回five pieces 信息的代码体,作为五个返回值。这些统称为“
setf 扩展”。
像setf 这样的位置变异宏使用宏 lambda 列表来解构位置语法并调用计算这五个部分的代码体。然后使用这五个部分来生成地点访问/更新代码。
不过请注意,setf 扩展的最后两项是存储表单和访问表单。我们无法摆脱这种二元性。如果我们为类似car 的地方定义setf 扩展,我们的访问表单将调用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-temp gensym。
但是,请注意上面只定义了setf 扩展。有了以上内容,我们可以只使用my-cadar 作为一个地方。如果我们尝试将它作为函数调用,它就会丢失。