We talked about define-setf-expander. That’s a fairly powerful macro that allows setf to operate on things that aren’t what the C++ programmer would think of as an “lvalue”. There is also defsetf. Less powerful, but simpler to use, so it’s generally preferred when its use is possible.
There are two forms of defsetf, a short form for very simple cases, and a long form for slightly longer cases.
If you have an access function that takes a certain number of arguments, and a setting form that takes exactly one more argument, the new value to substitute, and if that second function returns this new value, then you can use the short form. Here’s an example:
(defparameter *num-counters* 10)
(defparameter *counter-array* (make-array *num-counters*
:initial-element 0))
(defun get-next-counter (index)
(assert (<= 0 index (1- *num-counters*)))
(incf (aref *counter-array* index)))
(defun set-counter-value (index val)
(assert (<= 0 index (1- *num-counters*)))
(assert (integerp val))
(setf (aref *counter-array* index) (1- val))
val)
(defsetf get-next-counter set-counter-value)
(defun demonstrate ()
(format t "Next counter 0: ~D~%" (get-next-counter 0))
(format t "Next counter 0: ~D~%" (get-next-counter 0))
(format t "Next counter 0: ~D~%" (get-next-counter 0))
(format t "Next counter 1: ~D~%" (get-next-counter 1))
(format t "Setting next counter 0 value to 100~%")
(setf (get-next-counter 0) 100)
(format t "Next counter 0: ~D~%" (get-next-counter 0))
(format t "Next counter 1: ~D~%" (get-next-counter 1)))
With output:
CL-USER> (demonstrate)
Next counter 0: 1
Next counter 0: 2
Next counter 0: 3
Next counter 1: 1
Setting next counter 0 value to 100
Next counter 0: 100
Next counter 1: 2
NIL
When the arguments list to the function you’re working with is a bit more complicated, such as if it has optional arguments, the long form may still be suitable:
(defparameter *test-set* (list :A :B :C :C :D :C :A :B :A :C))
(defun member-in-test-set (sym &key (skip 0))
(let ((rv (member sym *test-set*)))
(do ()
((or (not rv) (= skip 0)) rv)
(setf rv (member sym (cdr rv)))
(decf skip))))
(defun set-member-in-test-set (sym new-sym &key (skip 0))
(let ((rv (member sym *test-set*)))
(do ()
((or (not rv) (= skip 0)))
(setf rv (member sym (cdr rv)))
(decf skip))
(when rv
(rplaca rv new-sym))
new-sym))
(defsetf member-in-test-set (sym &key (skip 0)) (new-sym)
`(set-member-in-test-set ,sym ,new-sym :skip ,skip))
(defun demonstrate ()
(format t "Test set is: ~S~%" *test-set*)
(format t "Setting the second occurence of :A to :Y~%")
(setf (member-in-test-set :A :skip 1) :Y)
(format t "Test set is: ~S~%" *test-set*))
Giving output:
CL-USER> (demonstrate)
Test set is: (:A :B :C :C :D :C :A :B :A :C)
Setting the second occurence of :A to :Y
Test set is: (:A :B :C :C :D :C :Y :B :A :C)
NIL
As I mentioned in the article about define-setf-expander, you might initially ask why bother writing a setf expander when the programmer can simply call the appropriate modification functions directly. The most likely reason you’d use an expander instead is that you have existing macros that make use of setf for some work, and you’d like to use it to setf one of its passed arguments, but that argument doesn’t look like an lvalue. Rather than writing runtime checks against type and changing the macro to account for all such cases, you can simplify your code by writing an setf expander and hiding the special-case code underneath it.