触发有关setf扩展器的问题:defining setf-expanders in Common Lisp
当为用户定义的getter编写setf扩展器时,我常常发现getter和setter中存在代码重复,就如何检索属性而言。例如:
CL-USER>
(defun new-car (lst)
(car lst))
NEW-CAR
CL-USER>
(defun (setf new-car) (new-value lst)
(setf (car lst) new-value))
(SETF NEW-CAR)
CL-USER>
(defparameter *lst* (list 5 4 3))
*LST*
CL-USER>
*lst*
(5 4 3)
CL-USER>
(setf (new-car *lst*) 3)
3
CL-USER>
*lst*
(3 4 3)
CL-USER>
注意(car lst)形式(已经定义了setf扩展器的实际存取器)是如何进行的。这总是让我感到恼火。如果能够在第一次定位时说出来,那就太好了,“嘿,我正在定义一个吸气剂,但我也希望它有一个典型的setf扩展器。”
有没有办法用普通的lisp标准来表达这个?有没有其他人担心这个问题,并定义了一个这样做的宏?
要清楚,我在这里想要的是一种定义getter和典型setter的方法,其中getter编译为已经有setter((car lst)的常见lisp表单的方式)代码中只有一次。
我也理解有些时候你不想这样做,b / c setter在设置值之前需要执行一些副作用。或者它是一个实际设置多个值的抽象,或者其他什么。在这种情况下,这个问题不那么重要。我在这里讨论的是setter执行标准操作的情况,并设置getter的位置。
答案 0 :(得分:4)
使用宏可以实现您想要的功能。
(defmacro define-place (name lambda-list sexp)
(let ((value-var (gensym)))
`(progn
(defun ,name ,lambda-list
,sexp)
(defun (setf ,name) (,value-var ,@lambda-list)
(setf ,sexp ,value-var)))))
(define-place new-chr (list)
(car list))
有关宏的更多信息可以在Peter Seibel的书Practical Common Lisp中找到。 Paul Graham的书“ANSI Common Lisp”的第10章是另一个参考。
答案 1 :(得分:1)
注意(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)
Voilà;您不再有对car
的重复呼叫; getter调用car
,setter rplaca
。
请注意,我们必须手动返回new-value
,因为rplaca
返回lst
。
您会发现,在许多Lisps中,setf
的内置car
扩展器使用了一个替代函数(也许名为sys:rplaca
或其上的变体),该函数返回分配的值。
在Common Lisp中定义新类型的场所时,通常将代码重复最小化的方法是使用define-setf-expander
。
通过此宏,我们将新的地点符号与两个项目相关联:
setf
扩展”。诸如setf
之类的位置更改宏使用宏lambda列表来分解位置语法,并调用计算这五段代码的代码体。然后使用这五个片段生成场所访问/更新代码。
不过,请注意,setf
扩展的最后两项是商店表单和访问表单。我们无法摆脱这种双重性。如果我们为类似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(我的工作地点)),则会浪费大量的遍历列表结构,因为调用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
用作场所。如果我们尝试将其作为函数调用,它将丢失。
答案 2 :(得分:0)
根据Mark的方法,Rainer在macro-function上的帖子以及Amalloy在transparent macrolet上的帖子,我想出了这个:
(defmacro with-setters (&body body)
`(macrolet ((defun-mod (name args &body body)
`(,@(funcall (macro-function 'defun)
`(defun ,name ,args ,@body) nil))))
(macrolet ((defun (name args &body body)
`(progn
(defun-mod ,name ,args ,@body)
(defun-mod (setf ,name) (new-val ,@args)
(setf ,@body new-val)))))
(progn
,@body))))
使用:
Clozure Common Lisp Version 1.8-r15286M (DarwinX8664) Port: 4005 Pid: 41757
; SWANK 2012-03-06
CL-USER>
(with-setters
(defun new-car (lst)
(car lst))
(defun new-first (lst)
(first lst)))
(SETF NEW-FIRST)
CL-USER>
(defparameter *t* (list 5 4 3))
*T*
CL-USER>
(new-car *t*)
5
CL-USER>
(new-first *t*)
5
CL-USER>
(setf (new-first *t*) 3)
3
CL-USER>
(new-first *t*)
3
CL-USER>
*t*
(3 4 3)
CL-USER>
(setf (new-car *t*) 9)
9
CL-USER>
*t*
(9 4 3)
在生产代码中使用此宏之前,应该注意一些变量捕获问题。