Previous Table of Contents Next


The MOP is subtle and discussing it is difficult because it is easy to get mixed up between the objects in the typical object-oriented programs and the metaobjects used in CLOS. However, consider the following:

  Every Lisp object is an instance of a class. Given an object, calling class-of object returns the object’s class.
  The class of the object determines its structure and behavior by specifying its slots and their properties. Every instance of a particular class has the same set of slots and properties. Any method that specializes on a given class applies to every instance of the class.
  The class has a set of superclasses from which it inherits structure and behavior. Slots are inherited from the superclasses, as are methods.

In the metaobject programming,

  A class is itself an object. Given a class, class-of class returns the metaclass. Instances of metaclasses are class objects.
  The metaclass determines the structure and behavior of any class that is an instance of the metaclass. The MOP is the set of structure and behavior that applies to standard-class, the root metaclass that implements default CLOS behavior.
  Custom object systems are typically built by subclassing standard-class and overriding the default behavior. This way, you can override some aspects of the behavior while inheriting the rest of it.

It’s worth adding a note on efficiency. It seems that the MOP might add a tremendous amount of overhead into the object system. However, implementations of Common Lisp normally implement a large number of optimizations specific for the semantics of standard-class. If you start using the MOP to build custom metaclasses, then some of the optimizations applied to standard-class are overridden by the custom behavior. In some cases (e.g., in the case of lazy evaluation and dependency tracking discussed in section 5.4.9.1), the custom behavior is more efficient for the type of problem being solved. In all cases clarity, maintainability, and extensibility of the source code are dramatically improved.

5.4. Programming in CLOS

This section gives some of the more detailed rules for CLOS programming and delves into areas where CLOS is somewhat different from most other object-oriented languages.

5.4.1. Slot Properties

There are two default types of slots: Slots may be local, in which case they are of type instance, or they may be of type class, in which case they are shared by all instances of the class.

To give an example, I’ll redefine the class square:

    (defclass square (convex-region)
       ((number-of-sides :initform 4 :allocation :class)
        (lower-left :initform (make-point :x 0 :y 0)
                :initarg :lower-left
                :accessor lower-left)
        (upper-right:initform (make-point :x 1 y 1)
                :initarg :upper-right
                :accessor upper-right)))

number-of-sides is declared as a class-allocated slot. All instances share this slot. A diagram of the situation is

5.4.2. Accessors and slot-value

The implementation of accessors is always done by using slot-value. slot-value accesses the slot directly, without calling the accessor methods. As discussed previously, exposing this as part of the interface makes it easy to break the interface when the internal representation changes. In standard CLOS programming, user code only uses slot-value for a couple reasons: defining custom accessors and debugging.

5.4.2.1. Using with-accessors and with-slots

CLOS provides two convenient macros for accessing slots in code bodies by treating the slots as variable names: with-accessors and with-slots.

We discuss with-accessors first. In code composing the body of a with-accessors macro, using a variable has exactly the same effect as calling the corresponding accessor function:

   (defgeneric area (figure)
      ;; returns the area of a figure
      )

is an interface defined to calculate the area of a figure. You can define the area method on a square as follows:

   (defmethod area ((square square))
      (with-accessors ((l lower-left)
                  (u upper-right))
                 square
         (let ((xlow (x-coordinate l)
              (xhigh x-coordinate u))
              (ylow (y-coordinate l))
              (yhigh (y-coordinate u)))
           (* (- xhigh xlow) (- yhigh ylow))))

is exactly equivalent to

   (defmethod area ((square square))
      (let ((xlow (x-coordinate (lower-left square))
           (xhigh x-coordinate (upper-right square))
           (ylow (y-coordinate (lower-left square))
           (yhigh (y-coordinate (upper-right square)))
        (* (- xhigh xlow) (- yhigh ylow))))

As another example,

   (defgeneric grow-figure (figure amount)
      ;; grows the area of a figure by a relative amount
      )

is an interface to return a new figure with its area increased by amount. The implementation for a method specialized on a square using with-accessors is

   (defmethod grow-figure ((square square))
      (with-accessors ((l lower-left)
                  (u upper-right))
                 square
       (let ((xlow (x-coordinate l)
            (xhigh (x-coordinate u))
            (ylow (y-coordinate l))
            (yhigh (y-coordinate u))
            (adj (sqrt amount)))
         (setf u (make-point :x (+ xlow (* (- xhigh xlow) adj))
                       :y (+ ylow (* (- yhigh ylow) adj))
         )))))

Inside with-accessors, the form

   (setf u expression)

is exactly equivalent to

   (setf (upper-right square) expression)

The macro with-slots is exactly analogous to with-accessors. The following:

   (defmethod distance ((p point))
      (with-slots (x y)
             p
       (sqrt (+ (* x x) (* y y)))))

is equivalent to this:

   (defmethod distance ((p point))
      (sqrt (+ (* (slot-value p ‘x) (slot-value p ‘x))
                 (* (slot-value p ‘y) (slot-value p ‘y)))))


Previous Table of Contents Next