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 to Hacker News contributor phoe-krk for correcting me. I
was also slightly loose in my use of specialisation, which I’ve
tightened up.