Mixins in Common Lisp
Techniques for Modularity in Common Lisp
In object-oriented programming, mixins are classes that export common functionality for reuse by other classes. Mixins are a powerful tool to make your Common Lisp code more modular and reusable across projects. To illustrate this, we will step through a simple example of creating a hash table mixin that can automatically add CLOS objects to a hash table at the time of their creation.
To begin with, a simple example of a mixin is as follows. We take the ‘fundamental’ ice cream class and add some flavor to it with a chocolate chip mixin. In general, chocolate chip is not meant to be eaten on its own, but when added to ice cream, it makes for a blissful experience.
This example was chosen for a reason. During the late (?) 1970s, MIT students and Symbolics Inc. employees frequently enjoyed ice cream at Steve’s Ice Cream Parlour in Somerville, Massachusetts: The owner of the ice cream shop offered a basic flavor of ice cream (vanilla, chocolate, etc.) and blended in a combination of extra items (nuts, cookies, fudge, etc.) and called the item a “mix-in”, his own trademarked term at the time. It was only a matter of time before Howard Cannon (lead developer of Flavors, a hugely influential predecessor to CLOS) and friends applied the concept to Lisp.
History aside, let us now turn to the use of mixins in Common Lisp. As a working example, we will define and use a primitive unit testing framework which stores unit tests as instances of a CLOS class. As we will want to organise and collect these tests into a hash table, we will define and use a hash table mixin, with which we hopefully will be able to illustrate the usefulness of mixins for modularity and code reuse.
Part 1: Creating a Fundamental Class
Our first step is to implement a primitive unit testing framework by defining a FUNDAMENTAL-TEST
class. One possible implementation of our unit testing framework is as follows.
(defclass fundamental-test ()
((test-form :initarg :form
:accessor test-form)
(test-value :initarg :value
:accessor test-value)
(test-result :initform nil
:accessor test-result)
(test-equal :initform #'equal
:initarg :test
:accessor test-equal)))(defgeneric run-test (obj))(defmethod run-test ((obj fundamental-test))
(if (funcall (test-equal obj)
(apply (car (test-form obj))
(cdr (test-form obj)))
(test-value obj))
t
nil))
Part 2: Creating a Hash Table Mixin
Armed with our fundamental test class, we can now develop a mixin to automatically collect test objects into a hash table. Let us begin by defining the HASH-MIXIN
class as follows.
(defclass hash-mixin ()
((key :initarg :key :accessor key)
(table :accessor table
:allocation :class
:initform (make-hash-table :test #'equal))))
As an aside for those less familiar with CLOS, the TABLE
slot above is a shared slot (due to the presence of :ALLOCATION :CLASS
in its keyword arguments) and all instances of HASH-MIXIN
will share the same hash table. This of course is desired behaviour, as we do not want to create a new hash table for every test object as that would defeat the purpose of collecting them into a single searchable data structure.
- Now, looking a bit farther ahead (and beyond this article), we would likely want to have different hash tables for different types of CLOS objects. If we have a
TEST-CLASS
and aBOOK-CLASS
, we likely do not want instances of both classes to share the same hash table. - Hence, we will subclass
HASH-MIXIN
to isolate a hash-table specifically for test objects. The slot definition forTABLE
inTEST-HASH-MIXIN
shadows the same slot in its parent classHASH-MIXIN
.
(defclass test-hash-mixin (hash-mixin)
((table :accessor table :allocation :class
:initform (make-hash-table :test #'equal))))
Part 3: Creating an Aggregate Class
We can now combine our FUNDAMENTAL-TEST
class and our TEST-HASH-MIXIN
class to create an aggregate UNIT-TEST
class as follows. I have also added in a diagram below to illustrate how class inheritance works in this example.
(defclass unit-test (test-hash-mixin fundamental-test) ())
Part 4: Adding in Functionality
We will now implement CRUD methods to enable us to [C]reate, [R]ead, [U]pdate and [D]elete unit tests within the associated hash table.
4.1 Adding Tests to the Hash Table [C]
The following method will automatically add any UNIT-TEST
instances to the hash table contained in TEST-HASH-MIXIN
as soon as they are instantiated. We achieve this by implementing an :AFTER
method that runs every time the primary INITIALIZE-INSTANCE
method is called.
Note that the
MAKE-INSTANCE
method that is commonly used to create CLOS objects itself callsINITIALIZE-INSTANCE
and hence the below is triggered every time aMAKE-INSTANCE
form is evaluated for a class that inherits fromHASH-MIXIN
.
(defmethod initialize-instance :after ((obj hash-mixin) &key)
(setf (gethash (key obj) (table obj)) obj))
4.2 Reading Tests from the Hash Table [R]
Reading tests from the hash table is relatively trivial and implemented by the below.
(defun read-test (obj)
(gethash (key obj) (table obj)))
4.3 Updating Tests in the Hash Table [U]
There are a couple of considerations when implementing functionality to update tests in the hash table. First, for most cases, if the test object has been modified, the hash table will automatically reference the modified object and there is nothing extra for us to do.
However, if the key of the test object is modified, we will need to remove the old key from the hash table and replace it with the new key. We achieve this with the following.
First, and before the key has been modified, we set the value associated with the old key in the hash table to NIL
. Then, and after the key has been modified, we set the value in the hash table associated with the new key to the associated object.
:BEFORE
and:AFTER
methods allow us to add very useful hooks on code that we have no control over / have written somewhere else (e.g. code that updates the key of test objects). Seeing these methods in action was a pretty magical experience for me.
(defmethod (setf key) :before (key (obj hash-mixin))
(setf (gethash (key obj) (table obj)) nil))(defmethod (setf key) :after (key (obj hash-mixin))
(setf (gethash (key obj) (table obj)) obj))
4.4 Deleting Tests in the Hash Table [D]
Deleting tests is very easy and is achieved by the following.
(defun delete-test (obj)
(setf (gethash (key obj) (table obj)) nil))
Part 5. Bringing it all together
Thus, with only a few lines of code, we have been able to add (CRUD) functionality to store and update objects within a hash table. Furthermore, this functionality is modular and can readily be applied to any CLOS class. Although there are some nuances in the above code and many ways to do this better, hopefully this example gives you a sneak peak into the power of mixins. In subsequent articles, we will build upon the basic concepts here and introduce some more intermediate lisp concepts.
The full code for this article can be found on my GitHub repo. I hope you enjoyed reading, and more importantly, I hope you add mixins to your code where appropriate! Thanks for reading and until next time, stay well.