请注意 (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-temp
gensym。
但是,请注意,上面只定义了setf
扩展。有了以上,我们只能my-cadar
作为一个地方使用。如果我们尝试将它作为函数调用,它就会丢失。