Class slots that work with classes and instances in CLOS
I recently had a use case where I wanted to associate a constant value with a class and its instances – but I needed to be able to get the value without having an instance to hand. This turns out to be solvable in CLOS.
In languages like Java you can associate class variables with classes, which can then be accessed without having an instance of the class. CLOS also has class-allocated slots, for example:
(defclass A ()
((instance-slot
:initform 1)
(class-slot
:allocation :class
:initform 2))
(:documentation "A class with instance- and class-allocated slots."))
An instance of A
has two slots: instance-slot
stored per-instance, and class-slot
stored only once and shared amongst all instances.
This is close to Java’s notion of class variables, but one still
needs an instance against which to call the method. (Seibel makes
this point in chapter 17 of “Practical Common Lisp”.)
One could just create a basic object and retrieve the slot:
(slot-value (make-instance 'A) 'class-slot)
but that’s inelegant and could potentially trigger a lot of
unnecessary execution (and errors) if there are constructors (overridden initialize-instance
methods) for A
. One could use the
metaobject protocol to introspect on the slot, but that’s quite
involved and still allows the slot to be changed, which isn’t part
of this use case.
What I really want is to be able to define a generic function such as class-slot
– but specialised against the class A
rather than
against the instances of A
. I thought this would need a metaclass
to define the method on, but it turned out that generic functions
are powerful enough on their own.
The trick is to first define a generic method:
(defgeneric class-slot (classname)
"Access the class slot on class.")
As the argument name suggests, we’re planning on passing a class
name to this method, not an instance. To set the value for A
, we
specialise the method as working on exactly the class A:
(defmethod class-slot ((classname (eql 'A)))
2)
The eql
specialiser selects this method only when exactly this
object is passed in – that is to say, the name of A
.
But what if we have an instance of A
? The same generic function
can still be used, but instead we specialise it against objects
of class A
in the usual way:
(defmethod class-slot ((a A))
(class-slot (class-name (class-of a))))
If we now pass an instance of A
, we extract its class name and then
re-call the same generic function, passing it the class name
instead of the object itself (which it doesn’t need, because the
slot value is independent of the actual object). This will select
the correct specialisation and return the slot value.
This approach works if we generate sub-classes of A
: we just use
eql
to specialise the generic function to the class we’re
interested in. It also works fine with packages, since the
undecorated symbol passed to the specialiser will be expanded
correctly according to what symbols are in scope. However, the
value is only associated with a single class, and isn’t inherited.
That’s not a massive limitation for my current use case, but would
be in general, I think.
This approach critically relies on an easily-forgotten property of
Lisp: values have types, but variables don’t, and we can
specialise the same generic function against any value or type.
The pattern makes use of this to avoid actually storing the value of class-slot
anywhere, which as a side effect avoids the problem
of someone accidentally assigning a new value to it. It’s an
example of how powerful generic functions are: more so than the
method tables and messages found in most O-O languages. And it’s
sufficiently structured that it’s crying-out for a couple of
macros to define these kinds of class slots.
UPDATED 2024-06-29: Fixed the typo in the class definition to use
:initform
and not :initarg
. Thanks to @vindarel for pointing this
out to me.