Singleton Classes with MOP
A Applied Introduction to the Metaobject Protocol
The Metaobject Protocol (MOP) for the Common Lisp Object System (CLOS) is one of the least bothered with but most interesting aspects of Common Lisp. As an introductory excursion (for both myself and the reader) into the workings of MOP and CLOS, this post presents an implementation of Singletons by means of a custom metaclass.
For The Unprepared Reader
If you have neither knowledge of CLOS nor Common Lisp, there is no better introduction than Practical Common Lisp.
If you have some exposure to Lisp or Common Lisp, but need a refresher on CLOS, chapter 16 and chapter 17 of the same text are superb primers.
The Effect To Be Produced
A singleton is a class having at most a single instance, hence the name. Operationally, instantiating a singleton class twice will yield the exact same object. Here is what this means in code:
;; instances are identical
(eq (make-instance 'unique-thing)
(make-instance 'unique-thing))
; T
Note the use of EQ
in the above. The instances are not merely equivalent, but identical, occupying precisely the same location in memory.
In case you're curious, singletons do have legitimate uses. If, for example, your class models some state that takes control of external resources that can only reasonabley be controlled by one thing at a time, then a singleton may be appropriate. An example might be a class that controls a robot arm or something like that - it may not make sense to have two controllers for the same arm at the same time.
Metaclasses
Metaclasses are one of the core concepts at work in the MOP. Just as objects are instances of classes, classes are instances of metaclasses. In Common Lisp, the default metaclass is standard-class, but the programmer can create and use custom metaclasses.
Being able to define classes as instances of custom metaclasses gives the programmer some interesting powers. You can, for example, supply a class with alternate default suerpclasses. Or, and this is of interest for the forthcoming implementation of singletons, you can ensure that a class has a particular class-level slot.
Using A Custom Metaclass
Provided that a metaclass called singleton
has been defined, a new class that uses singleton
as its metaclass can be subsequently defined like so:
;; using a metaclass
(defclass unique-thing ()
((name
:accessor unique-thing-name
:initform ""
:initarg :name))
(:documentation "A unique thing")
(:metaclass singleton)) ; specify the metaclass here.
The only difference between the above definition and a normal DEFCLASS
form is the use of the class option :metaclass
.
Defining A Metaclass
Metaclasses are actually ordinary classes and, as such, can be defined with DEFCLASS
. To implement a singleton, the class needs a slot to hold the singular instance of the object-level classes that use singleton
as their metaclass.
;; define the singleton class
(defclass singleton (standard-class)
((instance
:initform nil
:documentation "The instance for this class")))
Only one thing stands out in the above DEFCLASS
form: the inclusion of STANDARD-CLASS
among the direct superclasses list. If this step had been left out, the singleton
class would be a subclass of STANDARD-OBJECT
instead, which is the default superclass supplied by DEFCLASS
.
It is worth thinking through why you must specify STANDARD-CLASS
as a superclass: it is because the instances of singleton
are themselves classes, not objects. Hence you extend from STANDARD-CLASS
instead of STANDARD-OBJECT
.
In order to let Lisp know that a new metaclass is available, the programmer must manually validate its superclass relationships. Validation is performed by specializing the generic function CLOSER-MOP:VALIDATE-SUPERCLASS
for the new metaclass.
;; specialize valdiate-superclass
(defmethod closer-mop:validate-superclass
((subclass singleton) (superclass standard-class))
t)
(Oh, if you have not already done so, load closer-mop in quicklisp.)
Enforcing The Rules for Singletons
Finally, to ensure the correct behavior of singleton classes, the make-instance
generic function is specialized:
;; specialise make-instance for instances of singleton
(defmethod make-instance :around ((class singleton) &key)
(if (slot-value class 'instance)
(slot-value class 'instance)
(setf (slot-value class 'instance)
(call-next-method))))
It works like this: if there is already an instance, return it, otherwise use call-next-method
to proceed with the standard make-instance
process, save new instance to the 'instance
slot, and return it.
Now it is possible to define classes with metaclass singleton
, and such classes will be able to instantiate at most a single object.
I hope you enjoyed that first taste of the CLOS MOP.