My mental model of setf was wrong

I realised recently that I’ve been thinking about setf all wrong.

Lisp lets programs define new setf forms for assignment. The most common example is from CLOS, where a class like this:

    (defclass A ()
      ((var
        :accessor a-var)))

will give rise to a class and two functions, a-var to read the value of the var slot on an instance of A, and a setf target used as (setf (a-var instance) 24) to set the var slot of instance.

It’s natural to read that like as executing (a-var instance) to retrieve a location, and setf using this location to assign to. The documentation reinforces this view, talking about “generalised places” as the targets for setf to store things. My mental model was strengthened by idioms like (setf (car pair) 23) to set the car of a pair or list, and (setf (cdr pair) '(1 2 3) to set the cdr. The first argument is a locator expression returning the place to update, and the second argument is the new value to put there.

Natural. But wrong.


The thing I missed is that setf is a macro: it can access the structure of its arguments but not their values. You can’t write code like this:

     (let* ((l (list 1 2 3))
            (h (car l)))
       (setf h 23))

and expect the car of l to be updated, which would make sense if setf were working on a location, because h would be that location. But it isn’t.

What actually happens is that the setf macro looks, at compile time, at the structure of its first (locator) argument, and uses that to dispatch to a method. Using the slot accessor above, the setf form expands to something like:

     (defmethod (setf a-var) (v (a A))
       (setf (slot-value a 'a-var) a))

This is a method with two pieces of selection: specialised on the type of an argument (A), and named with the selector used to within the locator (a-var). It’s definition expands to another setf, this time specialised against slot-value and an instance of standard-object. Specialising on the selector explains why we need that selector to be present syntactically at compile time.

My mistake was thinking that the similarity between access form and setf form was necessary and functional – and it isn’t either. This has some interesting consequences.

The selector is entirely arbitrary

If we don’t like using car to indicate the head of a list – and some people don’t – we could in principle define a new specialisation such as:

     (defmethod (setf head) (v (l list))
       (rplaca l v))

and use it as (setf (head l) 45) even though head isn’t a defined function. All we need is a selector symbol.

There can be more arguments

Ever since I first encountered them I wondered why the lambda lists for new setf specialisations was so strange: the new value and then the arguments – but not the selector – of the place to be updated? Once you get a better mental model, the reason becomes obvious: there can be multiple arguments to the setf locator, possibly actually a variable number, alongside the selector, so we need to be able to find the new value reliably. The easiest way is to put it at the front of the lambda list.

There’s actually a common example of this sitting in plain sight that I’d missed. You access the elements of a Lisp array using the aref function, which takes the array and the index, such as (aref a 23). The corresponding setf form looks like (setf (aref a 23) 0), with the locator taking several arguments like the function. But it isn’t calling the function: it’s decomposing a pattern that looks exactly like the function call for convenience, and which passes several arguments to the specialised method that will look something like:

     (defmethod (setf aref) (v (a array) (i integer))
       ...)

The new value is reliably in the first argument position, with the rest of the locator arguments after it.

You can specialise by value too

Since the setf forms are just methods, you could if you wanted to specialise them on the type of the new value as well as on the locator. As a trivial example:

     (defmethod (setf assign-head) ((v integer) (l list))
       (format t "Assigned an integer ~s" v)
       (setf (car l) v))

     (defmethod (setf assign-head) ((s string) (l list))
       (format t "Assigned a string ~s" s)
       (setf (car l) s))

     (setf (assign-head '(1 2 3)) "zero")
Assigned a string "zero"

Obviously there are better ways to do this, but it’s a good example of the flexibility that comes from setf not really being all that special a form at all: just a creative use of the power of generic functions.

Can we build our own setf-like macros?

Yes: setf is entirely constructable within “ordinary” Lisp.

There are two parts to the construction. Firstly, we need the name of the method that underlies a particular selector.

We can build our own functions with names like this, although not using defun.

     (defvar *weird-name* (make-symbol "(1 2 3)"))

     (setf (symbol-function *weird-name*)
           (lambda (a)
             (print (format nil "We did *weird-name* on ~s" a))))

     (funcall *weird-name* "a string")

"We did *weird-name* on \"a string\""

For setf, the style of name used for the methods implementing the different choices is (setf selector) – a function named by a list – where selector is the symbol at the head of locator list. (Some Lisps construct a symbol from the list elements, rather than using it directly. I’m not sure what, if anything, the Common Lisp language definition says about how this should work.)

For the second part of the construction, setf takes the locator, synthesises the function name symbol using the selector, and calls a generic function with this name, passing the new value and the rest of the locator as arguments.

So to define a new construct our-setf we might do something like:

     (defmacro our-setf (locator new-value)
       (let* ((selector (car locator))
              (our-setf-function-name (make-symbol (format nil "(our-setf ~a)"
                                                           selector))))
         `(apply (symbol-function ,our-setf-function-name)
                 (cons ,new-value ,@(cdr locator)))))

When called as something like (our-setf (head '(1 2 3)) 0) the macro will code to call a method (our-setf head) (as a symbol), passing it (0 '(1 2 3)) as arguments and allowing the machinery of generic functions to determine which method is actually called. We define these methods of the form (our-setf head) and specialise them as required.

(It’s actually a bit more complicated than this because we need to define a generic function for (our-setf head). We have to go backstage and programmatically define the generic function. But the idea remains the same.)


After all this, my mental model of setf is a lot clearer – and, I hope, closer the reality at least. It combines a highly structured use of macros, synthesised function names, and generic functions – and no special machinery at all.

However, there’s some subtlety at play too, not obvious at first acquaintance. We don’t want our synthesised function names to accidentally capture the names of user-supplied code. It’s possible that using a naming style like setf-car would do just this, and a program happens to define a function with this name. But the names setf synthesises are lists, unlikely to be captured accidentally, which lets us define the specialised methods “as normal” even though some of the other parts of the process have to happen backstage.

This shows the power of macros and generic functions. It also shows how deeply the latter are embedded into Lisp. They’re usually thought of as part of CLOS, but they actually have little explicit relationship to class and objects at all, and have been woven all through Lisp to build flexible code structures.

UPDATED 2023-07-30: I incorrectly said originally that one couldn’t use forms like (defun (setf abc) ...): you can, just as with defmethod and defgeneric, and name a function using a list. Thanks for Hacker News contributor phoe-krk for correcting me. I was also slightly loose in my use of specialisation, which I’ve tightened up.