Warp Speed Introduction to CLOS

This document is an extremely short overview of the Common Lisp Object System (CLOS). Newcomers to CLOS are often intimidated by its apparent complexity and possibly confused by its unusual approach. For a long time I considered CLOS to be “too hairy”, but after working on a large project that made heavy use of CLOS, I found that CLOS is actually one of the simplest and easiest object systems to understand and use. The more complex parts of CLOS can be safely ignored.

Who needs objects?

In CLOS, the objects are the least important thing! CLOS objects are barely more than simple structures. In particular, “methods” are not associated with objects. For the first part of this introduction, we'll ignore them.

Generic Functions

CLOS is all about generic functions. A generic function is an object that you use just like a regular function, but it contains a bunch of methods that determine what happens when you invoke the function. Again, the details here are somewhat complex (because you can program how the generic function figures out which methods to use), but normally you can ignore the details. I'll start with a simple example.

The prepend generic function will take two arguments, an item and some sort of thing to extend. It will return a new thing with the item prepended on the front.

Example:

(prepend #\F "oo")
=>  "Foo"

(prepend 4 '(a b c))
=>  (4 a b c)

The code for prepend is as follows:

(defgeneric prepend (item thing)

  (:method ((item character) (thing string))
    (concatenate 'string (string item) thing))

  (:method (item (thing cons))
    (cons item thing)))

DEFGENERIC defines a generic function. The function's name will be prepend. It will take two arguments. All methods contained in prepend must take two arguments. The first method is for when item is a character and thing is a string, the second method is for when thing is a cons.

You do not have to enumerate all the methods at once. In fact, you should probably put methods close to the relevant class declarations. Use DEFMETHOD to add methods to existing generic functions.

(defmethod prepend ((item string) (thing string))
  (concatenate 'string item thing))

(prepend "Foo" "Bar")
=>  "FooBar"

Which method is called?

When a generic function is invoked, the first thing that happens is that the applicable methods are selected. If the type of the argument matches the type specified by the method, then the method is applicable. Inapplicable methods will not participate further.

Once the applicable methods are found, they are combined. The “standard method combination” is to simply select the most specific method. So if more than one method is applicable, the one that most closely matches the actual types of the arguments is used.

(defgeneric test-which-method (object)
  (:method (object)
      "This is an object.")

  (:method ((object number))
      "This is a number.")

  (:method ((object integer))
     "This is an integer."))

(test-which-method 27.2)
=>  "This is a number"

Why do they call it combination rather than selection? Because the less specific methods are not discarded, they can still be called. This is much like calling the base method in other languages.

(defgeneric test-which-method (object)
  (:method (object)
     (list "This is an object."))

  (:method ((object number))
     (cons "This is a number." (call-next-method)))

  (:method ((object integer))
     (cons "This is an integer." (call-next-method))))

(test-which-method 33)
=>  ("This is an integer." "This is a number." "This is an object.")

Oh yeah, objects

Let's return to objects. Objects are instances of classes. When you define a class, you specify the superclasses that this class will derive from. All methods that apply to any superclass will apply to this class as well. In addition, you supply a set of slots (fields). The class will have the fields you specify in addition to having the fields that are inherited from the superclasses. There is no subtractive inheritance --- private or protected fields --- the system trusts that you know how to behave yourself.

Slots are no good if you cannot manipulate them, so there are slot options that you will want to specify. A typical defclass may look like this:

(defclass my-class (super1 super2)
  ((name :initarg :name
         :initform "No name supplied."
         :reader get-name)
   (tag :initform (gensym) :reader read-tag)
   (value :accessor value-of)))

The name slot may be initialized when you create an instance of my-class, but since there are no :initargs for the other slots, you cannot initialize them. The :initform specifies what value to use should the :initarg not be supplied. The tag slot will be initialized by calling gensym, the value slot will be uninitialized.

The generic-function get-name will be extended with a method that will return the value of the name slot. A setf method will not be defined, so name will be a read-only slot. tag will be read-only as well through a call to read-tag. The value slot is both readable and writable.

(setq foo (make-instance 'my-class :name "Foo"))

(get-name foo)
=>  "Foo"

(value-of foo)
  Error:  slot `value' unbound.

(setf (value-of foo) 22)

(value-of foo)
=>  22

Why aren't the readers and accessors and initargs automatically generated? There are simply too many ways to do that incorrectly --- name collisions, shadowing, package issues --- much better to give the user explicit control.

Summary

Believe it or not, the rest of CLOS is simply variations on these themes, so here ends the introduction. This is nowhere near a complete tutorial, but it should give you an idea of what CLOS is like. Should you wish to learn more, you should peruse some of the more detailed CLOS tutorials on the web or read Sonya Keene's book.


Joe Marshall

Last modified: Wed Apr 04 12:28:13 Pacific Daylight Time 2007