DEFSETF 的用途

And*_* S. 1 lisp common-lisp

从标准描述中很难理解,所以:

例如,我试图将某个列表 (ls) 的第 k 个位置设置为特定值。甚至有我自己的功能,可以访问第 k 个 elt。

(defun kth-elt (lst k)
 (cond ((> 0 k) nil)
  ((equal 0 k) (car lst))
  ((< 0 k) (kth-elt (cdr lst) (- k 1))))).
Run Code Online (Sandbox Code Playgroud)

还创建了一个函数来更新该值。

 (defun kth-upd (lst k new)
  (cond ((> 0 k) nil)
    ((equal 0 k) (setf  (car lst) new))
    ((< 0 k) (kth-upd (cdr lst) (- k 1) new))))
Run Code Online (Sandbox Code Playgroud)

现在我实际上可以使用它,但我想了解它和 DEFSETF 之间的区别。另外我还是不明白。如何“教”defsetf 使用这些。谢谢帮助。

cor*_*ump 5

根据您的定义,它很简单:

(defsetf kth-elt kth-upd)
Run Code Online (Sandbox Code Playgroud)

kth-upd您现在可以使用kth-eltand来代替使用(setf kth-elt)。例如:

(let ((list (copy-list '(a b c d e f))))
  (setf (kth-elt list 3) nil)
  list)

=> (A B C NIL E F)
Run Code Online (Sandbox Code Playgroud)

但一致使用的真正好处SETF是您可以将此设置器与其他设置器结合使用。只需考虑增加一个值:

(let ((list (make-list 10 :initial-element 0)))
  (incf (kth-elt list 3))
  (incf (kth-elt list 5) 20)
  list)

=> (0 0 0 1 0 20 0 0 0 0)
Run Code Online (Sandbox Code Playgroud)

另请参阅Rainer Joswig 的回答,了解有关地点和 SETF 的更多背景信息。

塞特夫膨胀机

请注意,您要执行两次列表遍历:首先获取当前值,然后计算新值;只有这样,您才能从列表的开头开始存储新值:

  0: (KTH-ELT (0 0 0 0 0 0 0 0 0 0) 3)
    1: (KTH-ELT (0 0 0 0 0 0 0 0 0) 2)
      2: (KTH-ELT (0 0 0 0 0 0 0 0) 1)
        3: (KTH-ELT (0 0 0 0 0 0 0) 0)
        3: KTH-ELT returned 0
      2: KTH-ELT returned 0
    1: KTH-ELT returned 0
  0: KTH-ELT returned 0
  0: (KTH-UPD (0 0 0 0 0 0 0 0 0 0) 3 1)
    1: (KTH-UPD (0 0 0 0 0 0 0 0 0) 2 1)
      2: (KTH-UPD (0 0 0 0 0 0 0 0) 1 1)
        3: (KTH-UPD (0 0 0 0 0 0 0) 0 1)
        3: KTH-UPD returned 1
      2: KTH-UPD returned 1
    1: KTH-UPD returned 1
  0: KTH-UPD returned 1
Run Code Online (Sandbox Code Playgroud)

这也可以通过宏展开看出:

(incf (kth-elt list 3))
Run Code Online (Sandbox Code Playgroud)

...宏展开为:

(LET* ((#:LIST796 LIST) (#:NEW1 (+ 1 (KTH-ELT #:LIST796 3))))
  (KTH-UPD #:LIST796 3 #:NEW1))
Run Code Online (Sandbox Code Playgroud)

另一种可能的方法可能是使用DEFINE-SETF-EXPANDER

(define-setf-expander kth (list index)
  (alexandria:with-gensyms (store cell)
    (values `(,cell)
            `((nthcdr ,index ,list))
            `(,store)
            `(setf (car ,cell) ,store)
            `(car ,cell))))
Run Code Online (Sandbox Code Playgroud)

该函数返回 5 个不同的代码部分,可以组合这些代码部分来访问和修改某个位置。cellstore是使用 引入的局部变量GENSYM

变量cell(即以绑定到 的新鲜符号命名的变量cell)将绑定到(nthcdr index list)store包含要在该位置设置的值。在这里,将使用 将其放在适当的位置(setf (car cell) store)。另外,该地点的现有价值为(car cell)。正如您所看到的,我们需要在幕后操纵我们变异的 cons 单元(当然,空列表会引发错误)。的宏展开式为(incf (kth list 3))

(LET* ((#:CELL798 (NTHCDR 3 LIST)) (#:STORE797 (+ 1 (CAR #:CELL798))))
  (SETF (CAR #:CELL798) #:STORE797))
Run Code Online (Sandbox Code Playgroud)

setter函数知道如何访问保存我们想要更改的值的地方,并且可以直接更改它,这比仅仅一对reader/writer函数更有效。

关于可变性的备注

SETF 是围绕可变数据设计的。如果您为网络上的键/值存储编写访问器,以便(remote host key)连接并检索值,并将(setf (remote host key) value)新值发回,则不能保证远程值在(remote host key)用作中间位置时始终更新。

例如,如果该值是一个列表,(push val (remote host key))则将推送在主机上创建的本地列表,当结果是较大表达式的一部分时,没有义务setf实际确保将结果发送回网络。这使得 SETF 通过改变位置来提高效率,但代价是要求您更加明确。在前面的示例中,您必须(setf (remote host key) new-list)直接写入(而不是作为嵌套位置)才能有效地将新数据发送回。