I read a lot about generic functions in CL. I get it. And I get why they are valuable.
Mainly, I use them for when I want to execute a similar action with different data types, like this:
(defgeneric build-url (account-key)
(:documentation "Create hunter api urls"))
(defmethod build-url ((key number))
"Build lead api url"
(do-something...))
(defmethod build-url ((key string))
"build campaign api url"
(do-somthing... ))
In this example, campaign-url
and lead-url
are structures (defstruct).
My question is, at a high level, how do classes add value to the way generic functions + structures work together?
Structures historically predate classes, are more restricted and more "static" than classes: once a structure is defined, the compiler can generate code that accesses its slots efficiently, can assume their layout is fixed, etc. There is a lot of inlining or macro-expansion done that makes it necessary to rebuild everything from scratch when the structure changes. Being able to redefine a struct at runtime is not something defined by the standard, it is merely implementations trying to be nice.
On the other hand, classes have more features and are easier to manipulate at runtime. Suppose you write this class:
(defclass person ()
((name :initarg :name :reader .name)))
And you instantiate it:
(defparameter *someone* (make-instance 'person :name "Julia O'Caml"))
It is possible now to update the class definition:
(defparameter *id-counter* 0)
(defun generate-id ()
(incf *id-counter*))
(defclass person ()
((name :initarg :name :reader .name)
(dob :initarg :date-of-birth :reader .date-of-birth)
(%id :reader .id :initform (generate-id))))
And now, *someone*
, which existed already, has two additional fields, dob
that is unbound, and %id
that is automatically initialized to 1. There is a whole section about Object Creation and Initialization (7.1) that defines how objects can be redefined, change class, etc.
Moreover, this mechanism is not fixed, a lot of the steps described above rely on generic functions. It is possible to define how an object is allocated, initialized, etc. The concept was standardized as what is known as the Meta-Object Protocol, which also introduces the concept of metaobject, the object representing a class: usually a class has a name, parent classes, slots, etc. but you can add new members to a class, or change how instance slots are organized (maybe your just need a global handle and a connection, and the actual instance slots are stored in another process?).
Note also that once CLOS/MOP was defined, it was also eventually possible to define structures in this framework: in the standard , defstruct
(without a :type
option) defines classes with a structure-class
metaclass. Still, they do not behave like standard-class
because as said above they are more restricted, and as such are subject to more aggressive compilation optimizations (in general).
Structures are nice if you need to program like in C and you are ok with recompiling all your code when the structure changes. It is however premature optimization to use them in all cases. It is possible to use a lot of standard objects without noticing much slowness nowadays (a bit like Python).