【问题标题】:Common Lisp: Method to minimize code duplication when defining setf expandersCommon Lisp:定义 setf 扩展器时最小化代码重复的方法
【发布时间】:2012-07-13 18:20:00
【问题描述】:

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

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

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

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

【问题讨论】:

    标签: macros common-lisp


    【解决方案1】:

    使用宏可以实现您想要的。

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

    【讨论】:

    • 是的;这行得通;我要稍等片刻,看看是否有替代实现。
    【解决方案2】:

    注意 (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,而#:G3319new-val-temp gensym。

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

    【讨论】:

      【解决方案3】:

      根据 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-12
        • 2020-09-14
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2013-12-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多