Do you start with a struct or a class?
When creating a new compound data type in Common Lisp, do you make it a struct or a class? Especially if you are still exploring things, and do not know how it will evolve.
For some time after I learned CL, I’d always go for a struct. After all,
defstruct
is so easy to work with – you get so many useful accessors for
free!
(defstruct foo
a
b)
This gives us the type FOO
, the functions MAKE-FOO
, COPY-FOO
and FOO-P
and the slot accessing functions FOO-A
and FOO-B
out of the box.
Compare that to the equivalent defclass
and the only thing you get is the
type. There’s no copying function, the type needs to be passed around to
MAKE-INSTANCE
, TYPEP
and don’t even get me started on the verbosity of
SLOT-VALUE
.
That said, at least this problem can be solved by using macros like WITH-SLOTS
or defclass*
(readily available on Quicklisp).
However, there’s still the issue of performance. Because there’s no dynamic dispatch, structs are usually faster than classes - plus their functions can be inlined, and structs themselves can also be stack allocated.
What’s not there to like?
The big problem with structs, especially when you are still exploring things, is modifications. Change the above struct to the following:
(defstruct foo
x
a
b)
And SBCL will immediately complain with this:
WARNING: change in instance length of class FOO:
current length: 2
new length: 3
debugger invoked on a SIMPLE-ERROR in thread
#<THREAD "main thread" RUNNING {1004AC0203}>:
attempt to redefine the STRUCTURE-OBJECT class FOO incompatibly with the
current definition
Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL.
restarts (invokable by number or by possibly-abbreviated name):
0: [CONTINUE ] Use the new definition of FOO, invalidating
already-loaded code and instances.
1: [RECKLESSLY-CONTINUE] Use the new definition of FOO as if it were
compatible, allowing old accessors to use new
instances and allowing new accessors to use old
instances.
2: [ABORT ] Exit debugger, returning to top level.
For your own sake, just abort (restart 2) or continue (restart 0). In no case shall ye recklessly continue, because then you are just asking for trouble – ok maybe try it just for fun, but don’t do this in production!
Classes, on the other hand, are born to be redefined. Add a new slot, or remove an existing one, your instances will keep working just fine.
And while classes may not be as performant as structs, their performance is good enough most of the time, even more so when you are exploring things. Here’s a good collection of articles on CLOS efficiency.
In conclusion, my opinion on this matter has done a 180-degree turn and today I
default to using a defclass
when exploring new compound types.