Andrew S.
Andrew S.

Reputation: 467

Usage of DEFSETF

It is just really hard to understand from a standard description, so:

For instance, I'm trying to set a k-th position of some list (ls) to a specific value. Even have a function of my own, that gives acces to k-th elt.

(defun kth-elt (lst k)
 (cond ((> 0 k) nil)
  ((equal 0 k) (car lst))
  ((< 0 k) (kth-elt (cdr lst) (- k 1))))).

Also made a function, that updates that value.

 (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))))

Now i can actually use that, but i wanna understand the difference between it and DEFSETF. Also I still do not understand. how to "teach" defsetf to use these. Thx for help.

Upvotes: 1

Views: 640

Answers (2)

coredump
coredump

Reputation: 38789

Based on your definitions, it is simply:

(defsetf kth-elt kth-upd)

Instead of using kth-upd, you can now use kth-elt and (setf kth-elt). For example:

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

=> (A B C NIL E F)

But the real benefits of using SETF consistently is that you can combine this setter with other ones. Just consider incrementing a value:

(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)

See also this answer from Rainer Joswig for more background on places and SETF.

Setf expander

Note that you are doing the list traversal twice: you first get the current value, then computes the new one; only then, you store the new value, starting from the beginning of the list:

  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

This can also be seen by macroexpansion:

(incf (kth-elt list 3))

... is macroexpanded as:

(LET* ((#:LIST796 LIST) (#:NEW1 (+ 1 (KTH-ELT #:LIST796 3))))
  (KTH-UPD #:LIST796 3 #:NEW1))

Another possible approach might be to use 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))))

The function returns 5 different code parts that can be assembled to access and modify a place. cell and store are local variables introduced using GENSYM.

The variable cell (i.e. the variable named after the fresh symbol bound to cell) will be bound to (nthcdr index list). store contains the value to set in the place. Here, it will be put at the appropriate place by using (setf (car cell) store). Also, the existing value in the place is (car cell). As you can see, we need to manipulate, under the hood, the cons cell we mutate (of course, an error is raised with empty lists). The macroexpansion for (incf (kth list 3)) is:

(LET* ((#:CELL798 (NTHCDR 3 LIST)) (#:STORE797 (+ 1 (CAR #:CELL798))))
  (SETF (CAR #:CELL798) #:STORE797))

The setter function knows how to access the place that holds the value we want to change, and can change it directly, which is more efficent than just a pair of reader/writer functions.

Remark about mutability

SETF is designed around mutable data. If you write an accessor for a key/value store on the network, so that (remote host key) will connect and retrieve a value, and (setf (remote host key) value) sends the new value back, then it is not guaranteed that the remote value is always updated when (remote host key) is used as an intermediate place.

For example, if the value is a list, (push val (remote host key)) will push on the local list created on your host, there is no obligation for setf to actually ensure the result is sent back to the network when it is part of a larger expression. That allows SETF to be efficient by mutating places, at the small cost of requiring you to be more explicit. In the preceding example, you have to write (setf (remote host key) new-list) directly (not as a nested place) to effectively send the new data back.

Upvotes: 5

user5920214
user5920214

Reputation:

As an addendum to coredump's answer, it's worth noting that the following works, and is, in my opinion, much better than using defsetf:

(defun kth-elt (lst k)
  (cond ((> 0 k) nil)
        ((= 0 k) (car lst))
        ((< 0 k) (kth-elt (cdr lst) (- k 1)))))

(defun (setf kth-elt) (new lst k)
  (cond ((> 0 k) nil)
        ((= 0 k) (setf  (car lst) new))
        ((< 0 k) (setf (kth-elt (cdr lst) (- k 1)) new))))

There are cases when you need defsetf but they are not that common.

(kth-elt itself is just a special-case of elt of course: in real life you don't need to write any of this.)

Upvotes: 2

Related Questions