Overview

Abstract, Final, and Singleton Metaclasses for CLOS

A common complaint about CLOS is that it is somehow 'not properly object-oriented'. As usual with such complaints it's often hard to find out what is really meant. However it is clear that one of the things people don't like is that CLOS is too liberal. Politically-correct object-oriented languages differ in a number of ways from CLOS, among them:

  • They have single-dispatch;
  • They either have single inheritance or make multiple inheritance very hard to use;
  • They specify all the methods of a class within the class definition;
  • They mingle in various kinds of mandatory data hiding with the object system;
  • They allow classes to be defined with various characteristics, such as being 'abstract' -- instances of the class may not be made, though instances of subclasses may be, or 'final' -- the class may not be subclassed.

Most of these aspects of 'modern' object-oriented languages are simply beneath contempt. They seem to be restrictions placed on the language out of some misplaced desire for an efficient implementation without doing very much work, restrictions because it's impossible to trust programmers, especially with classes they didn't write, restrictions because it's just nice to be restricted and not to have to think too hard, or a gratuitous overloading of several ideas into one construct.

CLOS is quite different. CLOS assumes some intelligence and maturity in its users, and provides something much more like a toolkit for constructing object-oriented programs that actually fit the domain rather than having to deform the domain into some ludicrously restrictive framework.

Of course, this doesn't help, because everyone now 'knows' that an object system has to have all these properties, so CLOS is just inherently no good. Well, all is not lost: CLOS is a toolkit for constructing object-oriented programs that fit the domain, and one of the domains it can produce programs to fit is the domain of 'modern' object-oriented languages. It's fairly obvious for instance that by a little judicous use of the package system and macros you could easily define a single-dispatch, all-methods-are-in-the-class language based on CLOS. Data hiding can also be done by devious use of gensymed slot names, and so on. Almost everything is possible

Finally, you can use the MOP to change CLOS in all-sorts of interesting ways, most of which are simply beyond what 'modern' systems can imagine. Here is a small example of this. By defining suitable metaclasses, it is almost trivial to add 'abstract' and 'final' classes to CLOS.

Abstract classes

A class which can be subclassed, but not directly implemented. Defined using either the metaclass abstract-class or the macro define-abstract-class.

(define-abstract-class container ()
  ;; May not be instantiated
  ((children :initarg :children
         :accessor children
         :initform '())))

(define-abstract-class contained ()
  ;; May not be instantiated
  ((parent :initarg :parent
         :accessor parent
         :initform nil)))

(defclass exchange-object (container contained)
  ;; may be instantiated
  ())

Final classes

A class which may not be further subclassed. Defined either using the metaclass final-class or the macro define-final-class.

(defclass exchange-object (container contained)
  ;; may be instantiated and subclassed
  ())

(define-final-class mux (exchange-object)
  ;; may be instantiated but not subclassed
  ((id :initarg :id
       :reader id)))

(defclass shelf (exchange-object)
  ;; may be instantiated but not subclassed
  ()
  (:metaclass final-class))

Both abstract and final classes are implemented very simply by subclassing standard-class and writing suitable methods on make-instance and validate-superclass. Although the MOP is not completely standardised, the implementation here works on three commercial, and one non-commercial, CLs with only minor conditionalisation.

Singleton classes

Along the lines of abstract classes above, it's quite easy using the MOP to create classes which only have a single instance. For these classes, make-instance always returns the same object. It's somewhat questionable whether this is a good thing to do, since it violates a lot of expectations - expecting (eq (make-instance 'foo) (make-instance 'foo)) to be true is almost as strange as expecting (eq (cons 1 1) (cons 1 1)) to be true. However, it's easy enough to do, and it gives another example of using the MOP to alter CLOS.

Example:

(in-package :cl-user)

(use-package :org.tfeb.hax.singleton-classes)

(defclass foo ()
  ((x :initform (progn
                  (format *debug-io* "~&Initialising x~%")
                  1)))
  (:metaclass singleton-class))