Introduction Chapter 1 Chapter 2 Chapter 3 Chapter 4 Chapter 5 Chapter 6 CLOS Intro
Allegro CL version 8.0

ANSI Common Lisp by Paul Graham, Prentice Hall, 1996

The following material excerpted (with permission) from ANSI Common Lisp by Paul Graham. Chapter 11 covers a review of CLOS concepts.

1.   CLOS
2.   Object-Oriented programming
3.   Classes and Instances
4.   Slot Properties
5.   Superclasses
6.   Precedence
7.   Generic Functions
8.   Auxiliary Methods
9.   Method Combination
10. Encapsulation
11. Two Models
12. Summary
13. Exercises

1. CLOS

The Common Lisp Object System, or CLOS, is a set of operators for doing object-oriented programming. Because of their common history it is conventional to treat these operators as a group. Technically, they are in no way distinguished from the rest of Common Lisp: defmethod is just as much (and just as little) an integral part of the language as defun.

2. Object-Oriented programming

Object-oriented programming means a change in the way programs are organized. This change is analogous to the one that has taken place in the distribution of processor power. In 1970, a multi-user computer system meant one or two big mainframes connected to a large number of dumb terminals. Now it is more likely to mean a large number of workstations connected to one another by a network. The processing power of the system is now distributed among individual users instead of centralized in one big computer.

Object-oriented programming breaks up traditional programs in much the same way. Instead of having a single program that operates on an inert mass of data, the data itself is told how to behave, and the program is implicit in the interactions of these new data "objects."

For example, suppose we want to write a program to find the areas of two-dimensional shapes. One way to do this would be to write a single function that looked at the type of its argument and behaved accordingly, as in Figure 11.1:

 (defstruct rectangle
   height width)
 (defstruct circle
   radius)
 (defun area (x)
   (cond ((rectangle-p x)
          (* (rectangle-height x)
             (rectangle-width x)))
         ((circle-p x)
          (* pi (expt (circle-radius x) 2)))))

> (let ((r (make-rectangle)))
    (setf (rectangle-height r) 2
          (rectangle-width r)  3)
    (area r))
6

Figure 11.1

Using CLOS we might write an equivalent program as in Figure 11.2. In the object-oriented model, our program gets broken up into several distinct methods, each one intended for certain kinds of arguments. The two methods in Figure 11.2 implicitly define an area function that works just like the one in Figure 11.1. When we call area, looks at the type of the argument and invokes the corresponding method.

(defclass rectangle ()
  (height width))
(defclass circle ()
  (radius))
(defmethod area ((x rectangle))
  (* (slot-value x 'height) 
     (slot-value x 'width)))
(defmethod area ((x circle))
  (* pi (expt (slot-value x 'radius) 2)))

> (let ((r (make-instance 'rectangle)))
    (setf (slot-value r 'height) 2
          (slot-value r 'width)  3)
    (area r))
6

Figure 11.2

Together with this way of breaking up functions into distinct methods, object-oriented programming implies inheritance---both of slots and methods. The empty list given as the second argument in the two defclasses in Figure 11.2 is a list of superclasses. Suppose we define a new class of colored objects, and then a class of colored circles that has both colored and circle as superclasses:

(defclass colored ()
  (color))

(defclass colored-circle (circle colored)
  ())

When we make instances of colored-circle, we will see two kinds of inheritance:

  1. Instances of colored-circle will have two slots: radius, which is inherited from the circle class, and color, which is inherited from the colored class.
  2. Because there is no area method defined explicitly for instances of colored-circle, if we call area on an instance of colored-circle, we will get the method defined for the circle class.

In practical terms, object-oriented programming means organizing a program in terms of methods, classes, instances, and inheritance. Why would you want to organize programs this way? One of the claims of the object-oriented approach is that it makes programs easier to change. If we want to change the way objects of class ob are displayed, we just change the display method of the ob class. If we want to make a new class of objects like obs but different in a few respects, we can create a subclass of ob; in the subclass, we change the properties we want, and all the rest will be inherited by default from the ob class. And if we just want to make a single ob that behaves differently from the rest, we can create a new child of ob and modify the child's properties directly. If the program was written carefully to begin with, we can make all these types of modifications without even looking at the rest of the code.

3. Classes and Instances

You go through two steps to create structures: you call defstruct to lay out the form of a structure, and a specific function like make-point to make them. Creating instances requires two analogous steps. First we define a class, using defclass:

(defclass circle ()
  (radius center))

This definition says that instances of the circle class will have two slots (like fields in a structure), named radius and center respectively.

To make instances of this class, instead of calling a specific function, we call the general make-instance with the class name as the first argument:

> (setf c (make-instance 'circle))
#<Circle #XC27496>


To set the slots in this instance, we can use setf with slot-value:

> (setf (slot-value c 'radius) 1)
1

Like structure fields, the values of uninitialized slots are undefined.

4. Slot Properties

The third argument to defclass must be a list of slot definitions. The simplest slot definition, as in the example above, is a symbol representing its name. In the general case, a slot definition can be a list of a name followed by one or more properties. Properties are specified like keyword arguments.

By defining an :accessor for a slot, we implicitly define a function that refers to the slot, making it unnecessary to call slot-value. If we update our definition of the circle class as follows,

(defclass circle ()
  ((radius :accessor circle-radius)
   (center :accessor circle-center)))

then we will be able to refer to the slots as circle-radius and circle-center respectively:

> (setf c (make-instance 'circle))
#<Circle #XC5C726>
> (setf (circle-radius c) 1)
1
> (circle-radius c)
1

By specifying a :writer or a :reader instead of an :accessor, we could get just the first half of this behavior, or just the second.

To specify a default value for a slot, we have to give an :initform argument. If we want to be able to initialize the slot in the call to make-instance, we define a parameter name as an :initarg. With both added, our class definition might become:

(defclass circle ()
  ((radius :accessor circle-radius
           :initarg :radius
           :initform 1)
   (center :accessor circle-center
           :initarg :center
           :initform (cons 0 0))))

Now when we make an instance of a circle we can either pass a value for a slot using the keyword parameter defined as the slot's :initarg, or let the value default to that of the slot's :initform.

> (setf c (make-instance 'circle :radius 3))
#<Circle #XC2DE0E>
> (circle-radius c)
3
> (circle-center c)
(0 . 0)

We can specify that some slots are to be shared---that is, their value is the same for every instance. We do this by declaring the slot to have :allocation :class. (The alternative is for a slot to have :allocation :instance, but since this is the default there is no need to say so explicitly.) When we change the value of such a slot in one instance, that slot will get the same value in every other instance. So we would want to use shared slots to contain properties that all the instances would have in common.

For example, suppose we wanted to simulate the behavior of a flock of tabloids. In our simulation we want to be able to represent the fact that when one tabloid takes up a subject, they all do. We can do this by making all the instances share a slot. If the tabloid class is defined as follows,

(defclass tabloid ()
  ((top-story :accessor tabloid-story
              :allocation :class)))

then if we make two instances of tabloids, whatever becomes front-page news to one instantly becomes front-page news to the other:

> (setf daily-blab       (make-instance 'tabloid)
        unsolicited-mail (make-instance 'tabloid))
#<Tabloid #XC2AB16>
> (setf (tabloid-story daily-blab) 'adultery-of-senator)
ADULTERY-OF-SENATOR
> (tabloid-story unsolicited-mail)
ADULTERY-OF-SENATOR

The :documentation property, if given, should be a string to serve as the slot's documentation. By specifying a :type, you are promising that the slot will only contain elements of that type.

5. Superclasses

The second argument to defclass is a list of superclasses. A class inherits the union of the slots of its superclasses. So if we define the class screen-circle to be a subclass of both circle and graphic,

(defclass graphic ()
  ((color   :accessor graphic-color   
            :initarg :color)
   (visible :accessor graphic-visible 
            :initarg :visible
            :initform t)))
 
(defclass screen-circle (circle graphic)
  ())

then instances of screen-circle will have four slots, two inherited from each superclass. A class does not have to create any new slots of its own; screen-circle exists just to provide something instantiable that inherits from both circle and graphic.

The accessors and initargs work for instances of screen-circle just as they would for instances of circle or graphic:

> (graphic-color (make-instance 'screen-circle
                                :color 'red :radius 3))
RED

We can cause every screen-circle to have some default initial color by specifying an initform for this slot in the defclass:

(defclass screen-circle (circle graphic)
  ((color :initform 'purple)))

Now instances of screen-circle will be purple by default:

> (graphic-color (make-instance 'screen-circle))
PURPLE

6. Precedence

We've seen how classes can have multiple superclasses. When there are methods defined for several of the classes to which an instance belongs, Lisp needs some way to decide which one to use. The point of precedence is to ensure that this happens in an intuitive way.

For every class there is a precedence list: an ordering of itself and its superclasses from most specific to least specific. In the examples so far, precedence has not been an issue, but it can become one in bigger programs. Here's a more complex class hierarchy:

(defclass sculpture () (height width depth))

(defclass statue (sculpture) (subject))

(defclass metalwork () (metal-type))

(defclass casting (metalwork) ())

(defclass cast-statue (statue casting) ())

Figure 11.3 contains a network representing cast-statue and its superclasses.

To build such a network for a class, start at the bottom with a node representing that class. Draw links upward to nodes representing each of its immediate superclasses, laid out from left to right as they appeared in the calls to defclass. Repeat the process for each of those nodes, and so on, until you reach classes whose only immediate superclass is standard-object---that is, classes for which the second argument to defclass was (). Create links from those classes up to a node representing standard-object, and one from that node up to another node representing the class t. The result will be a network that comes to a point at both top and bottom, as in Figure 11.3.

clos-1.bmp (430366 bytes)

The precedence list for a class can be computed by traversing the corresponding network as follows:

  1. Start at the bottom of the network.
  2. Walk upward, always taking the leftmost unexplored branch.
  1. If you are about to enter a node and you notice another path entering the same node from the right, then instead of entering the node, retrace your steps until you get to a node with an unexplored path leading upward. Go back to step 2.
  2. When you get to the node representing t, you're done. The order in which you first entered each node determines its place in the precedence list.

One of the consequences of this definition (in fact, of rule 3) is that no class appears in the precedence list before one of its subclasses.

The arrows in Figure 11.3 show how it would be traversed. The precedence list determined by this graph is: cast-statue, statue, sculpture, casting, metalwork, standard-object, t. Sometimes the word specific is used as shorthand to refer to the position of a class in a given precedence list. The preceding list runs from most specific to least specific.

The main point of precedence is to decide what method gets used when a generic function is invoked. This process is described in the next section. The other time precedence matters is when a slot with a given name is inherited from several superclasses.

7. Generic Functions

A generic function is a function made up of one or more methods. Methods are defined with defmethod, which is similar in form to defun:

(defmethod combine (x y)
  (list x y))

Now combine has one method. If we call combine at this point, we will get the two arguments in a list:

> (combine 'a 'b)
(A B)

So far we haven't done anything we could not have done with a normal function. The unusual thing about a generic function is that we can continue to add new methods for it.

First, we define some classes for the new methods to refer to:

(defclass stuff () ((name :accessor name :initarg :name)))
(defclass ice-cream (stuff) ())
(defclass topping (stuff) ())

This defines three classes: stuff, which is just something with a name, and ice-cream and topping, which are subclasses of stuff.

Now here is a second method for combine:

(defmethod combine ((ic ice-cream) (top topping))
  (format nil "~A ice-cream with ~A topping."
          (name ic)
          (name top)))

In this call to defmethod the parameters are specialized: each one appears in a list with the name of a class. The specializations of a method indicate the kinds of arguments to which it applies. The method we just defined will only be used if the arguments to combine are instances of ice-cream and topping respectively.

How does Lisp decide which method to use when a generic function is called? It will use the most specific method for which the classes of the arguments match the specializations of the parameters. Which means that if we call combine with an instance of ice-cream and an instance of topping, we'll get the method we just defined:

> (combine (make-instance 'ice-cream :name 'fig)
           (make-instance 'topping :name 'treacle))
"FIG ice-cream with TREACLE topping."

But with any other arguments, we'll get the first method we defined:

> (combine 23 'skiddoo)
(23 SKIDDOO)

Because neither of the parameters of the first method is specialized, it will always get last priority, yet will always get called if no other method does. An unspecialized method acts as a safety net, like an otherwise clause in a case expression.

Any combination of the parameters in a method can be specialized. In this method only the first argument is:

(defmethod combine ((ic ice-cream) x)
  (format nil "~A ice-cream with ~A."
              (name ic)
              x))

If we call combine with an instance of ice-cream and an instance of topping, we'll still get the method that's looking for both, because it's more specific:

> (combine (make-instance 'ice-cream :name 'grape)
           (make-instance 'topping :name 'marshmallow))
"GRAPE ice-cream with MARSHMALLOW topping."

However, if the first argument is ice-cream and the second argument is anything but topping, we'll get the method we just defined above:

> (combine (make-instance 'ice-cream :name 'clam)
           'reluctance)
"CLAM ice-cream with RELUCTANCE."

When a generic function is called, the arguments determine a set of one or more applicable methods. A method is applicable if the arguments in the call come within the specializations of all its parameters.

If there are no applicable methods we get an error. If there is just one, it is called. If there is more than one, the most specific gets called. The most specific applicable method is determined based on the class precedence for the arguments in the call. The arguments are examined left to right. If the first parameter of one of the applicable methods is specialized on a more specific class than the first parameters of the other methods, then it is the most specific method. Ties are broken by looking at the second argument, and so on.

In the preceding examples, it is easy to see what the most specific applicable method would be, because all the objects have a single line of descent. An instance of ice-cream is, in order, itself, ice-cream, stuff, a standard-object, and a member of the class t.

Methods don't have to be specialized on classes defined by defclass. They can also be specialized on types (or more precisely, the classes that mirror types). Here is a method for combine that's specialized on numbers:

(defmethod combine ((x number) (y number))
  (+ x y))

Methods can even be specialized on individual objects, as determined by eql:

(defmethod combine ((x (eql 'powder)) (y (eql 'spark)))
  'boom)

Specializations on individual objects take precedence over class specializations.

Methods can have parameter lists as complex as ordinary Common Lisp functions, but the parameter lists of all the methods that compose a generic function must be congruent. They must have the same number of required parameters, the same number of optional parameters (if any), and must either all use &rest or &key, or all not use them. The following pairs of parameter lists are all congruent,

x)             (a)
(x &optional y) (a &optional b)
(x y &rest z)   (a b &key c)
(x y &key z)    (a b &key c d)

and the following pairs are not:

(x)             (a b)
(x &optional y) (a &optional b c)
(x &optional y) (a &rest b)
(x &key x y)    (a)

Only required parameters can be specialized. Thus each method is uniquely identified by its name and the specializations of its required parameters. If we define another method with the same qualifiers and specializations, it overwrites the original one. So by saying

(defmethod combine ((x (eql 'powder)) (y (eql 'spark)))
  'kaboom)

we redefine what combine does when its arguments are powder and spark.

8. Auxiliary Methods

Methods can be augmented by auxiliary methods, including before-, after-, and around-methods. Before-methods allow us to say, ``But first, do this.'' They are called, most specific first, as a prelude to the rest of the method call. After-methods allow us to say, ``P.S. Do this too.'' They are called, most specific last, as an epilogue to the method call. Between them, we run what has till now been considered just the method, but is more precisely known as the primary method. The value of this call is the one returned, even if after-methods are called later.

Before- and after-methods allow us to wrap new behavior around the call to the primary method. Around-methods provide a more drastic way of doing the same thing. If an around-method exists, it will be called instead of the primary method. Then, at its own discretion, the around-method may itself invoke the primary method (via the function call-next-method, which is provided just for this purpose).

This is called standard method combination. In standard method combination, calling a generic function invokes

  1. The most specific around-method, if there is one.
  2. Otherwise, in order,
  1. All before-methods, from most specific to least specific.
  2. The most specific primary method.
  3. All after-methods, from least specific to most specific.

The value returned is the value of the around-method (in case 1) or the value of the most specific primary method (in case 2).

Auxiliary methods are defined by putting a qualifying keyword after the method name in the call to defmethod. If we define a primary speak method for the speaker class as

(defclass speaker () ())
 
(defmethod speak ((s speaker) string)
  (format t "~A" string))

then calling speak with an instance of speaker just prints the second argument:

> (speak (make-instance 'speaker)
         "I'm hungry")
I'm hungry
NIL

By defining a subclass intellectual, which wraps before- and after-methods around the primary speak method,

(defclass intellectual (speaker) ())
 
(defmethod speak :before ((i intellectual) string)
  (princ "Perhaps "))
 
(defmethod speak :after ((i intellectual) string)
  (princ " in some sense"))

we can create a subclass of speakers that always have the last (and the first) word:

> (speak (make-instance 'intellectual)
         "I'm hungry")
Perhaps I'm hungry in some sense
NIL

As the preceding outline of standard method combination noted, all before- and after-methods get called.

So if we define before- or after-methods for the speaker superclass,

(defmethod speak :before ((s speaker) string)
  (princ "I think "))

they will get called in the middle of the sandwich:

> (speak (make-instance 'intellectual)
         "I'm hungry")
Perhaps I think I'm hungry in some sense
NIL

Regardless of what before- or after-methods get called, the value returned by the generic function is the value of the most specific primary method---in this case, the nil returned by format.

This changes if there are around-methods. If there is an around-method specialized for the arguments passed to the generic function, the around-method will get called first, and the rest of the methods will only run if the around-method decides to let them. An around- or primary method can invoke the next method by calling call-next-method. Before doing so, it can use next-method-p to test whether there is a next method to call.

With around-methods we can define another, more cautious, subclass of speaker:

(defclass courtier (speaker) ())
 
(defmethod speak :around ((c courtier) string)
  (format t "Does the King believe that ~A? " string)
  (if (eql (read) 'yes)
      (if (next-method-p) (call-next-method))
      (format t "Indeed, it is a preposterous ~ 
 idea.~%"))
  'bow)

When the first argument to speak is an instance of the courtier class, the courtier's tongue is now guarded by the around-method:

> (speak (make-instance 'courtier) "kings will last")
Does the King believe that kings will last? yes
I think kings will last
BOW
> (speak (make-instance 'courtier) "the world is round")
Does the King believe that the world is round? no
Indeed, it is a preposterous idea.
BOW

Note that, unlike before- and after-methods, the value returned by the around-method is returned as the value of the generic function.

9. Method Combination

In standard method combination the only primary method that gets called is the most specific (though it can call others via call-next-method). Instead we might like to be able to combine the results of all applicable primary methods.

It's possible to define methods that are combined in other ways---for example, for a generic function to return the sum of all the applicable primary methods. Operator method combination can be understood as if it resulted in the evaluation of a Lisp expression whose first element was some operator, and whose arguments were calls to the applicable primary methods, in order of specificity. If we defined the price generic function to combine values with +, and there were no applicable around-methods, it would behave as though it were defined:

(defun price (\&rest args) 
  (+ (apply [most specific primary method] args)
     .
     .
     .
     (apply [least specific primary method] args)))

If there are applicable around-methods, they take precedence, just as in standard method combination. Under operator method combination, an around-method can still call the next method via call-next-method. However, primary methods can no longer use call-next-method.

We can specify the type of method combination to be used by a generic function with a :method-combination clause in a call to defgeneric:

(defgeneric price (x)
  (:method-combination +))

Now the price method will use + method combination; any defmethod for price must have + as the second argument. If we define some classes with prices,

(defclass jacket () ())
(defclass trousers () ())
(defclass suit (jacket trousers) ())
 
(defmethod price + ((jk jacket)) 350)
(defmethod price + ((tr trousers)) 200)

then when we ask for the price of an instance of suit, we get the sum of the applicable price methods:

> (price (make-instance 'suit))
550

The following symbols can be used as the second argument to defmethod or in the :method-combination option to defgeneric:

+   and   append   list   max   min   nconc   or   progn

You can also use standard, which yields standard method combination.

Once you specify the method combination a generic function should use, all methods for that function must use the same kind. Now it would cause an error if we tried to use another operator (or :before or :after) as the second argument in a defmethod for price. If we want to change the method combination of price, we must remove the whole generic function by calling fmakunbound.

10. Encapsulation

Object-oriented languages often provide some way of distinguishing between the actual representation of objects and the interface they present to the world. Hiding implementation details brings two advantages: you can change the implementation without affecting the object's outward appearance, and you prevent objects from being modified in potentially dangerous ways. Hidden details are sometimes said to be encapsulated.

Although encapsulation is often associated with object-oriented programming, the two ideas are really separate. You can have either one without the other.

In Common Lisp, packages are the standard way to distinguish between public and private information. To restrict access to something, we put it in a separate package, and only export the names that are part of the external interface.

We can encapsulate a slot by exporting the names of the methods that can modify it, but not the name of the slot itself. For example, we could define a counter class and associated increment and clear methods as follows:

(defpackage "CTR"
            (:use "COMMON-LISP")
            (:export "COUNTER" "INCREMENT" "CLEAR"))

(in-package ctr)

(defclass counter () ((state :initform 0)))

(defmethod increment ((c counter))
  (incf (slot-value c 'state)))

(defmethod clear ((c counter))
  (setf (slot-value c 'state) 0))

Under this definition, code outside the package would be able to make instances of counter and call increment and clear, but would not have legitimate access to the name state.

If you want to do more than just distinguish between the internal and external interface to a class, and actually make it impossible to reach the value stored in a slot, you can do that too. Simply unintern its name after you've defined the code that needs to refer to it:

(unintern 'state)

Then there is no way, legitimate or otherwise, to refer to the slot from any package.

11. Two Models

Object-oriented programming is a confusing topic partly because there are two models of how to do it: the message-passing model and the generic function model. The message-passing model came first. Generic functions are a generalization of message-passing.

In the message-passing model, methods belong to objects, and are inherited in the same sense that slots are. To find the area of an object, we send it an area message,

tell obj area

and this invokes whatever area method obj has or inherits.

Sometimes we have to pass additional arguments. For example, a move method might take an argument specifying how far to move. If we wanted to tell obj to move 10, we might send it the following message:

tell obj move 10

If we put this another way,

(move obj 10)

the limitation of the message-passing model becomes clearer. In message-passing, we only specialize the first parameter. There is no provision for methods involving multiple objects---indeed, the model of objects responding to messages makes this hard even to conceive of.

In the message-passing model, methods are of objects, while in the generic function model, they are specialized for objects. If we only specialize the first parameter, they amount to exactly the same thing. But in the generic function model, we can go further and specialize as many parameters as we need to. This means that, functionally, the message-passing model is a subset of the generic function model. If you have generic functions, you can simulate message-passing by only specializing the first parameter.

12. Summary

  1. In object-oriented programming, the function f is defined implicitly via the f methods of the objects that have them. Objects inherit methods from their parents.
  2. Defining a class is like defining a structure, but more verbose. A shared slot belongs to a whole class.
  3. A class inherits the slots of its superclasses.
  4. The ancestors of a class are ordered into a precedence list. The precedence algorithm is best understood visually.
  5. A generic function consists of all the methods with a given name. A method is identified by its name and the specializations of its parameters. Argument precedence determines the method used when a generic function is called.
  6. Methods can be augmented by auxiliary methods. Standard method combination means calling the around-method, if there is one; otherwise the before-, most specific primary, and after-methods.
  7. In operator method combination, all the primary methods are treated as arguments to some operator.
  8. Encapsulation can be done via packages.
  9. There are two models of object-oriented programming. The generic function model is a generalization of the message-passing model.

13. Exercises

  1. Define accessors, initforms, and initargs for the classes defined in Figure 11.2. Rewrite the associated code so that it no longer calls slot-value.
  2. Suppose that a number of classes are defined as follows:
(defclass a (c d) ...) (defclass e () ...)
(defclass b (d c) ...) (defclass f (h) ...)
(defclass c () ..) (defclass g (h) ...)
(defclass d (e f g) ...) (defclass h () ...)
  1. Draw the network representing the ancestors of a, and list the classes an instance of a belongs to, from most to least specific.
  2. Do the same for b.
  1. Suppose that you already have the following functions:

Using these functions (and not compute-applicable-methods or find-method), define a function most-spec-app-meth that takes a generic function and a list of the arguments with which it has been called, and returns the most specific applicable method, if any.

  1. Without changing the behavior of the generic function area (Figure 11.2) in any other respect, arrange it so that a global counter gets incremented each time area is called.
  2. Give an example of a problem that would be difficult to solve if only the first argument to a generic function could be specialized.

Copyright © 1996 by Paul Graham, used with permission; modifications copyright © 2004, Franz Inc. Oakland, CA., USA. All rights reserved.

Introduction Chapter 1 Chapter 2 Chapter 3 Chapter 4 Chapter 5 Chapter 6 CLOS Intro
Allegro CL version 8.0