We continue our overview of Lisp features possibly missed by the beginner arriving from C++ with define-setf-expander. In C, you had the concept of lvalues, assignable expressions such as a variable name or a pointer or array dereference. In C++, you got to things like operator=, to make assignable operations to objects, or functions that returned a modifiable reference, which is usable as an lvalue.
In Lisp, a similar concept is the setf expander. The setf macro is the common means by which an existing place has its value modified. In that way, it fills the niche used by the assignment operator in C/C++. Lisp also has things that you would think of as lvalues, setf cannot be used to assign a value to arbitrary expressions.
When the programmer wants to generalize setf to situations that are more complex than a simple setf-able place, define-setf-expander is one way to do it. However, before you spend too much effort on this feature, check out defsetf, which I will cover in an upcoming post. The defsetf macro, when the situation is simple enough to allow its use, is less complicated.
Here’s an example of define-setf-expander at work. I define a function, max-value, that, when given an array of numbers, returns the largest number in the array, and a second return value holding the list of indices where those values can be found. Imagine I also have a context where I want to replace all instances of the largest value in the array with a new value, using setf. This is where a setf-expander is helpful.
(defun max-value (arr) "Returns 2 values. The first is the maximum scalar value in the array of numbers. The second is a list of lists of array indices." (labels ((convert-to-indices (row-major-index dimensions) (let (rv) (dolist (dim (reverse dimensions)) (multiple-value-bind (div rem) (floor row-major-index dim) (push rem rv) (setf row-major-index div))) rv))) (let* ((adim (array-dimensions arr)) (num-elements (array-total-size arr)) max-val indices) (dotimes (i num-elements) (let ((val (row-major-aref arr i))) (cond ((or (not max-val) (> val max-val)) (setf max-val val indices (list (convert-to-indices i adim)))) ((= val max-val) (push (convert-to-indices i adim) indices))))) (values max-val indices)))) (define-setf-expander max-value (arr &environment env) (multiple-value-bind (tmpspace old-values new-values setting-form getting-form) (get-setf-expansion arr env) (declare (ignore new-values setting-form)) (let ((nval (gensym))) (values tmpspace old-values `(,nval) `(multiple-value-bind (val indices) (max-value ,getting-form) (declare (ignore val)) (dolist (one-ind indices) (let ((linear-ind (apply 'array-row-major-index ,getting-form one-ind))) (setf (row-major-aref ,getting-form linear-ind) ,nval))) ,nval) `(max-value ,getting-form))))) (defun demonstrate () (let ((test-array (make-array '(4 3) :initial-contents '((0 1 2) (3 4 11) (11 7 8) (9 10 11))))) (format t "Input array is: ~A~%" test-array) (multiple-value-bind (max-val indices) (max-value test-array) (format t "The maximum value is: ~A~%" max-val) (format t "Appearing at indices: ~A~%" indices)) (format t "We setf the max-value to 50.~%") (setf (max-value test-array) 50) (format t "The array is now: ~A~%" test-array)))
The output of the demonstrate function is:
CL-USER> (demonstrate) Input array is: #2A((0 1 2) (3 4 11) (11 7 8) (9 10 11)) The maximum value is: 11 Appearing at indices: ((3 2) (2 0) (1 2)) We setf the max-value to 50. The array is now: #2A((0 1 2) (3 4 50) (50 7 8) (9 10 50)) NIL
I will point out that the define-setf-expander macro is not something that I have frequently seen used, it’s a helpful tool for certain unusual cases, but is not commonly needed. In most contexts, the programmer will write a special-purpose function to perform the effects of the setf, but when the setf is buried in an existing macro, this can be a useful way to re-use the macro without having to write special-case code all through it.