0

从这个关于 setf 扩展器的问题触发:在 Common Lisp 中定义 setf-expanders

在为用户定义的 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 扩展器的实际访问器,是如何在两个 defun 中的。这一直让我有些恼火。能够在第一个 defun 上说,'嘿,我正在定义一个作为 getter 的 defun,但我也希望它有一个典型的 setf 扩展器',这将是很好的。

通用 lisp 标准有什么方法可以表达这一点吗?有没有其他人担心这个问题,并定义了一个宏来做到这一点?

需要明确的是,我在这里想要的是一种定义 getter 和典型 setter 的方法,其中 getter 编译为已经具有 setter 的常见 lisp 形式(例如,(car lst))只在编码。

我也明白有时你不想这样做,b/c setter 需要在设置值之前执行一些副作用。或者它是一个实际上设置多个值的抽象,或者其他什么。这个问题在那种情况下不太相关。我在这里谈论的是 setter 做标准的事情,只是设置 getter 的地方的情况。

4

3 回答 3

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 章是另一个参考。

于 2012-07-14T05:41:51.620 回答
1

请注意 (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 formaccess 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,并且#:G3319new-val-tempgensym。

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

于 2020-01-10T22:27:06.097 回答
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)

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

于 2012-07-14T07:05:45.447 回答