This chapter continues the treatment of CLOS (the Common Lisp Object System) that we began in Chapter 7, in which we saw how objects store data. In this chapter we'll learn about how objects get their behaviors.
A generic function provides behavior based upon the type of an object. The behavior is selected according to the types of the arguments to the generic function. The generic function dispatches control to a particular method that provides the best match to the argument types that you use to invoke the generic function.
You define a method using Lisp's DEFMETHOD macro. In its
simplest form, DEFMETHOD expects a name and a specialized
lambda list. The specialized lambda list is similar to the list of formal
parameters you supply for a LAMBDA or DEFUN form;
the difference is that you can specify the type of each parameter. The
method will only be invoked when the generic function call (which looks
exactly like a function call) specifies parameters that are
of matching types. To specialize a parameter in a DEFMETHOD
form, simply name the parameter and its type in a list. For example:
(defmethod method1 ((param1 number) (param2 string)) ...) (defmethod method2 ((param2 float) (param2 sequence)) ...)
You can also leave the type of a parameter unspecified by just giving its name; this kind of parameter will match any data type. In this example, the first parameter is not specialized:
(defmethod method3 (param1 (param2 vector)) ...)
Note that the parameter types do not have to be CLOS class types. If you want to specialize a method to one particular CLOS class, you can specialize one of the arguments to that class, as we saw in the first figure in this chapter. If you have specialized one parameter to a CLOS class, and leave the other parameters unspecialized, then you've emulated the single-dispatch method common to certain "classic" object-based programming languages. In this limited case you can think of a method as being associated with a class. We'll see in the next section that this association breaks down when we associate a method with multiple classes.
You're probably wondering how generic functions get created, when all you do is to define methods. When you define a method, Lisp creates a generic function if one does not already exist. When Lisp creates this generic function for you, it makes note of the name, the number of required and optional arguments, and the presence and names of keyword parameters. When you create another method of the same name, it must agree with the generic function on the details of the parameters which were recorded in the generic function. This agreement is called lambda list congruence -- Lisp will signal an error if you attempt to create a new method with a non-congruent lambda list.
A multimethod is a method that is selected based upon the types of two or more of its arguments. When you have a method that is selected for two or more classes, then we can't really say that a class (or an object, which is an instance of that class) "has" a particular method.
The association of methods to classes gets even more tenuous when we consider that one or more of a method's arguments may specialize on an object that is not a class. If we expect to find some convenient way to say that a method "belongs to" a particular class, we're simply not going to find it.
Simpler (and less capable) object systems than CLOS do associate a method with a particular class. CLOS does not. This is an important point, so let me rephrase it: CLOS methods are not a part of any class for which they may provide services.
A method is a part of a generic function. The generic function analyzes the actual parameters and selects a method to invoke based upon a match between actual parameters and specialized lambda lists in the method definitions. And, to reiterate the point made in the previous paragraph, generic functions are not a part of of any classes upon which the generic function's methods operate.
When you define a method, the types of its parameters (in the
specialized lambda list) declare that the method may be invoked only
by parameters of the same, or more specific, types. For example, if
a parameter is specialized on the type NUMBER, it can
match INTEGER, FIXNUM, FLOAT,
BIGNUM, RATIONAL, COMPLEX, or
any other proper subtype of NUMBER.
But what if you define two methods that could match the same types? Consider the following definitions.
(defmethod op2 ((x number) (y number)) ...) ; method 1 (defmethod op2 ((x float) (y float)) ...) ; method 2 (defmethod op2 ((x integer) (y integer)) ...) ; method 3 (defmethod op2 ((x float) (y number)) ...) ; method 4 (defmethod op2 ((x number) (y float)) ...) ; method 5
A call of the form (OP2 11 23) potentially matches
methods 1 and 3, because the arguments are both of type INTEGER,
which is a subtype of NUMBER. CLOS resolves ambiguity
by choosing the more specific match, thus method 3 is selected for
a call of (OP2 11 23).
The same resolution strategy chooses method 5 for (OP2 13
2.9), method 4 for (OP2 8.3 4/5), and method 1
for (OP2 5/8 11/3). The general rule is that CLOS selects
a method based upon the most specific matching types, and an argument
is always more specific than the arguments to its right. The second
part of this rule means that arguments on the left serve as tiebreakers
for those futher to the right. Consider these methods:
(defmethod Xop2 ((x number) (y number)) ...) ; method 1 (defmethod Xop2 ((x float) (y number)) ...) ; method 2 (defmethod Xop2 ((x number) (y float)) ...) ; method 3
A call of (XOP2 5.3 4.1) will invoke method 2. Both
method 2 and method 3 are more specific than method 1. Method 2 has
a more specialized type in the first argument position when compared
to method 3, so method 2 is the one that is invoked.
In addition to dispatching based upon argment types, CLOS can dispatch based upon specific objects.
? (defmethod idiv ((numerator integer) (denominator integer))
(values (floor numerator denominator)))
#<STANDARD-METHOD IDIV (INTEGER INTEGER)>
? (defmethod idiv ((numerator integer) (denominator (eql 0)))
nil)
#<STANDARD-METHOD IDIV (INTEGER (EQL 0))>
? (idiv 4 3)
1
? (idiv 6 2)
3
? (idiv 4 0)
NIL
Here we've specialized on the integer 0. You can specialize on any
object that can be distinguished using the EQL predicate.
Numbers, symbols and object instances can all be tested in this way.
See Chapter 17 for more information on the
EQL predicate.
A class is a type, and a subclass is a subtype. So when you define these classes:
(defclass c1 () ...) (defclass c2 (c1) ...)
then C2 is a subclass of C1. If you then use
the types C1 and C2 as specializers in a method
definition, C2 will be a more specific type (see the previous
section) than C1.
But what if you define classes that inherit from more than one class? How do you decide which class is more specific than another? Consider the following class definitions:
(defclass c1 () ...) (defclass c2 () ...) (defclass c3 (c1) ...) (defclass c4 (c2) ...) (defclass c5 (c3 c2) ...) (defclass c6 (c5 c1) ...) (defclass c7 (c4 c3) ...)
These definitions give us an inheritance hierarchy that looks like this; bold lines link a subclass to the first parent class, while lighter lines link to the second parent class:
Now consider the following method definitions, which specialize on this class hierarchy:
(defmethod m1 ((x c1)) ...) ; method 1 (defmethod m1 ((x c2)) ...) ; method 2
It's clear that calling M1's generic function with
an object of type C1 or C3 will invoke
method 1, and that calling the generic function with an object of
type C2 or C4 will invoke method 2. But
what happens if we call M1's generic function with an
object of type C5, C6, or C7?
These classes all inherit -- directly or indirectly -- from both
C1 and C2.
If we invoke the generic function M1 with an object
of type C5, C6, or C7, CLOS
must decide whether to invoke method 1 or method 2. (It can't do
both.) This decision is based upon some measure of whether
C1 or C2 is a more specific parent class.
The measure is based upon the position of the parent class in the
class precedence list of the subclass. Here are the class
precedence lists (CPLs) for C5, C6, and
C7:
Class CPL ----- --- C5 (C5 C3 C1 C2) C6 (C6 C5 C3 C1 C2) C7 (C7 C4 C2 C3 C1)
Classes near the beginning of the CPL are more specific, so
C5 and C6 are more specific to
C1 and C7 is more specific to
C2. Therefore, calling the M1 generic
function with an object of type C5 or C6
will invoke method 1. Calling M1 with an object of type
C7 will invoke method 2.
The next question you should ask is "how does CLOS
determine the CPL?" There is, of course, an algorithm for computing
the CPL -- you can find this described in a Lisp reference manual.
Or you can define some classes and ask Lisp to tell you the CPL;
most implementations include a function named
CLASS-PRECEDENCE-LIST that expects a class
object as its only argument (use FIND-CLASS to
get the class object from its name) and returns a CPL.
? (class-precedence-list (find-class 'c6)) (C6 C5 C3 C1 C2)
TBCL: Insert good example of multiple inheritance here.
Design conservatively with multiple inheritance, and you shouldn't have to depend upon knowledge of the algorithm by which CLOS computes the CPL.
If you define methods as we've seen throughout this chapter, the generic function that gets created will offer a capability called standard method combination. The methods that we've so far used have all been primary methods. Under standard method combination, we can also define before, after, and around methods which get combined with the primary method.
To define a before, after, or around
method we add a corresponding keyword (a method qualifier)
to our DEFMETHOD form, like this:
(defmethod madness :before (...) ...) (defmethod madness :after (...) ...) (defmethod madness :around (...) ...)
Let's take a look at standard method combination in action. We'll begin
with the :BEFORE and :AFTER methods.
; Define a primary method ? (defmethod combo1 ((x number)) (print 'primary) 1) #<STANDARD-METHOD COMBO1 (NUMBER)> ; Define before methods ? (defmethod combo1 :before ((x integer)) (print 'before-integer) 2) #<STANDARD-METHOD COMBO1 :BEFORE (INTEGER)> ? (defmethod combo1 :before ((x rational)) (print 'before-rational) 3) #<STANDARD-METHOD COMBO1 :BEFORE (RATIONAL)> ; Define after methods ? (defmethod combo1 :after ((x integer)) (print 'after-integer) 4) #<STANDARD-METHOD COMBO1 :AFTER (INTEGER)> ? (defmethod combo1 :after ((x rational)) (print 'after-rational) 5) #<STANDARD-METHOD COMBO1 :AFTER (RATIONAL)> ; Try it ? (combo1 17) BEFORE-INTEGER BEFORE-RATIONAL PRIMARY AFTER-RATIONAL AFTER-INTEGER 1 ? (combo 4/5) BEFORE-RATIONAL PRIMARY AFTER-RATIONAL 1
When we call COMBO1, CLOS determines which methods
are applicable. As we learned earlier, only one primary method is
applicable. But, as we saw in the call to (COMBO1 17),
we can have multiple applicable :BEFORE and
:AFTER methods. Because integer is a subtype of
rational, an integer argument to COMBO1, the
:BEFORE and :AFTER methods that specialize
on INTEGER and RATIONAL arguments are
applicable.
So CLOS has now determined a set of applicable methods: a primary
method and some before and after methods. The standard method
combination determines the order in which these methods get invoked.
First, all of the applicable :BEFORE methods
are invoked, with the more specific methods invoked first.
Then the applicable primary method is invoked. Next, all of the
applicable :AFTER methods are invoked, with the more
specific methods invoked last. Finally, the value of the
primary method is returned as the value of the generic function.
:BEFORE and :AFTER methods are often
used to add extra behaviors to a method. They typically introduce
some kind of side effect -- by doing I/O, by changing global state,
or by altering slots of one or more of the objects passed as
parameters. There are three actions not available to
:BEFORE and :AFTER methods:
But the standard method combination offers a third kind of
qualified method, the :AROUND method, that can perform
all of these actions. An :AROUND method is defined using
a method qualifier, just as you might expect:
(defmethod madness :around (...) ...)
When a generic function has one or more :AROUND
methods defined, the most specific applicable :AROUND
method is invoked first, even if there are applicable
:BEFORE methods. At this point, the
:AROUND method has complete control -- if it simply
returns, then none of the other applicable methods will be
invoked. Normally an :AROUND method calls
CALL-NEXT-METHOD which allows control to proceed
through other applicable methods.
CALL-NEXT-METHOD calls the next most specific
:AROUND method; if there are no less specific
applicable :AROUND methods, then
CALL-NEXT-METHOD invokes all of the applicable before,
primary, and after methods exactly as detailed above. The value
returned by the most specific :AROUND method is
returned by the generic function; typically you'll use the value --
or some modification thereof -- returned by
CALL-NEXT-METHOD.
If you call CALL-NEXT-METHOD without arguments, it
uses the arguments of the current method. But you can call
CALL-NEXT-METHOD with arguments, and change the
parameters seen by the called method(s).
; Define a primary method
? (defmethod combo2 ((x number)) (print 'primary) 1)
#<STANDARD-METHOD COMBO2 (NUMBER)>
; Define before methods
? (defmethod combo2 :before ((x integer)) (print 'before-integer) 2)
#<STANDARD-METHOD COMBO2 :BEFORE (INTEGER)>
? (defmethod combo2 :before ((x rational)) (print 'before-rational) 3)
#<STANDARD-METHOD COMBO2 :BEFORE (RATIONAL)>
; Define after methods
? (defmethod combo2 :after ((x integer)) (print 'after-integer) 4)
#<STANDARD-METHOD COMBO2 :AFTER (INTEGER)>
? (defmethod combo2 :after ((x rational)) (print 'after-rational) 5)
#<STANDARD-METHOD COMBO2 :AFTER (RATIONAL)>
; Define around methods
? (defmethod combo2 :around ((x float))
(print 'around-float-before-call-next-method)
(let ((result (call-next-method (float (truncate x)))))
(print 'around-float-after-call-next-method)
result))
#<STANDARD-METHOD COMBO2 :AROUND (FLOAT)>
? (defmethod combo2 :around ((x complex)) (print 'sorry) nil)
#<STANDARD-METHOD COMBO2 :AROUND (COMPLEX)>
? (defmethod combo2 :around ((x number))
(print 'around-number-before-call-next-method)
(print (call-next-method))
(print 'around-number-after-call-next-method)
99)
; Try it
? (combo2 17)
AROUND-NUMBER-BEFORE-CALL-NEXT-METHOD
BEFORE-INTEGER
BEFORE-RATIONAL
PRIMARY
AFTER-RATIONAL
AFTER-INTEGER
1
AROUND-NUMBER-AFTER-CALL-NEXT-METHOD
99
? (combo2 4/5)
AROUND-NUMBER-BEFORE-CALL-NEXT-METHOD
BEFORE-RATIONAL
PRIMARY
AFTER-RATIONAL
1
AROUND-NUMBER-AFTER-CALL-NEXT-METHOD
99
? (combo2 82.3)
AROUND-FLOAT-BEFORE-CALL-NEXT-METHOD
AROUND-NUMBER-BEFORE-CALL-NEXT-METHOD
PRIMARY
1
AROUND-NUMBER-AFTER-CALL-NEXT-METHOD
AROUND-FLOAT-AFTER-CALL-NEXT-METHOD
99
? (combo2 #c(1.0 -1.0))
SORRY
NIL
One thing you can't do is to provide arguments to
CALL-NEXT-METHOD that would change the applicable methods.
In other words, you need to preserve the type of methods even as you
change their values. For example, the following redefinition of
one of the COMBO2 example methods won't work:
? (defmethod combo2 :around ((x float))
(call-next-method (floor x))) ; FLOOR returns an integer
#<STANDARD-METHOD COMBO2 :AROUND (FLOAT)>
? (combo2 45.9)
Error: applicable methods changed
The metaobject protocol (MOP) is a collection of functions that
expose much of the underlying workings of CLOS. We've already seen
one function that belongs to the MOP:
CLASS-PRECEDENCE-LIST. Some MOP functions, like
CLASS-PRECEDENCE-LIST, give you visibility into the inner
workings of CLOS. Here are some examples:
Function Argument Returns -------- -------- ------- CLASS-DIRECT-SUBCLASSES a class a list of the immediate subclasses CLASS-DIRECT-SUPERCLASSES a class a list of the immediate superclasses CLASS-DIRECT-SLOTS a class a list of non-inherited slots CLASS-DIRECT-METHODS a class a list of non-inherited methods
Other MOP functions let you change the underlying behavior of CLOS. You can use this capability to extend CLOS -- perhaps to implement a persistent object store -- or to alter the behavior to more closely correspond to a different kind of object system. Such changes are far beyond the scope of this book. Also, you should be aware that the MOP is not (yet, as of early 1999) a standard part of CLOS, and there is no definition of the MOP recognized by any national or international standards body.
So far, the defining document for the MOP is The Art of the Metaobject Protocol. Most Lisp vendors provide at least a partial MOP implementation; you should probably start with the vendor's documentation if you're interested in the MOP.