Mixins 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 a BOOK-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 for TABLE in TEST-HASH-MIXIN shadows the same slot in its parent class HASH-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-MIXINclass 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 calls INITIALIZE-INSTANCE and hence the below is triggered every time a MAKE-INSTANCE form is evaluated for a class that inherits from HASH-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.

--

--

--

Masters in Quantitative Finance. Writing Computer Science articles and notes on topics that interest me, with a tendency towards writing about Lisp & Swift

Love podcasts or audiobooks? Learn on the go with our new app.

Recommended from Medium

Tips for Android App Development Success

DevOps: Why Traceability Should Be a Priority

How to Easily Monitor Ant Media Server Instances in 3 Steps?

A Week Later, I Found a Fabulously Inelegant Solution

Why the days of Mobile IoT Apps are Numbered? Long live Voice

Creating your own OpenFaas Connector

On free software and opensource development

Intro to Higher Kinded Types in Haskell

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Ashok Khanna

Ashok Khanna

Masters in Quantitative Finance. Writing Computer Science articles and notes on topics that interest me, with a tendency towards writing about Lisp & Swift

More from Medium

Calculate size of Root/Sub folders

Async/await inference in Firefly

BINARY HEAPS (with code in C++)

A Max-Heap example. Notice that it is a complete tree and parents’ keys are larger than children’s keys.

How to implement Matrix Multiplication using Map-Reduce?