Common Lisp:定义setf扩展器时最小化代码重复的方法

时间:2012-07-13 20:58:44

标签: macros common-lisp

触发有关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的位置。

3 个答案:

答案 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

通过此宏,我们将新的地点符号与两个项目相关联:

  • 一个宏lambda列表,用于定义该地点的语法。
  • 一组代码,用于计算并返回five pieces信息,作为五个返回值。这些统称为“ 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#:G3319new-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)

在生产代码中使用此宏之前,应该注意一些变量捕获问题。