Note how the (car lst) form, the actual accessor that already has a setf expander defined, is in both defuns.
But that's only apparently true before macro expansion. In your setter, the (car lst)
form is the target of an assignment. It will expand to something else, like the call to some internal function that resembles rplaca
:
You can do a similar thing manually:
(defun new-car (lst)
(car lst))
(defun (setf new-car) (new-value lst)
(rplaca lst new-value)
new-value)
Voilà; you no longer have duplicate calls to car
; the getter calls car
, and the setter rplaca
.
Note that we manually have to return new-value
, because rplaca
returns lst
.
You will find that in many Lisps, the built-in setf
expander for car
uses an alternative function (perhaps named sys:rplaca
, or variations thereupon) which returns the assigned value.
The way we generally minimize code duplication when defining new kinds of places in Common Lisp is to use define-setf-expander
.
With this macro, we associate a new place symbol with two items:
- a macro lambda list which defines the syntax for the place.
- a body of code which calculates and returns five pieces of information, as five return values. These are collectively called the "
setf
expansion".
The place-mutating macros like setf
use the macro lambda list to destructure the place syntax and invoke the body of code which calculates those five pieces. Those five pieces are then used to generate the place accessing/updating code.
Note, nevertheless, that the last two items of the setf
expansion are the store form and the access form. We can't get away from this duality. If we were defining the setf
expansion for a car
-like place, our access form would invoke car
and the store form would be based on rplaca
, ensuring that the new value is returned, just like in the above two functions.
However there can exist places for which a significant internal calculation can be shared between the access and the store.
Suppose we were defining my-cadar
instead of my-car
:
(defun new-cadar (lst)
(cadar lst))
(defun (setf new-cadar) (new-value lst)
(rplaca (cdar lst) new-value)
new-value)
Note how if we do (incf (my-cadar place)), there is a wasteful duplicate traversal of the list structure because cadar
is called to get the old value and then cdar
is called again to calculate the cell where to store the new value.
By using the more difficult and lower level define-setf-expander
interface, we can have it so that the cdar
calculation is shared between the access form and the store form. So that is to say (incf (my-cadar x))
will calculate (cadr x)
once and store that to a temporary variable #:c
. Then the update will take place by accessing (car #:c)
, adding 1 to it, and storing it with (rplaca #:c ...)
.
This looks like:
(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))))
Test:
[1]> (macroexpand '(incf (my-cadar x)))
(LET* ((#:G3318 (CDAR X)) (#:G3319 (+ (CAR #:G3318) 1)))
(PROGN (RPLACA #:G3318 #:G3319) #:G3319)) ;
T
#:G3318
comes from cell-temp
, and #:G3319
is the new-val-temp
gensym.
However, note that the above defines only the setf
expansion. With the above, we can only use my-cadar
as a place. If we try to call it as a function, it is missing.