This is the first in a series about programming in Common Lisp. My emphasis will be on the use of CLOS, the Common Lisp Object System. Together we will look at the patterns and design choices that appear when one organizes a program around the features provided by CLOS. We'll examine some real Lisp systems -- web servers, GUI toolkits, expert systems and others -- and look at how they use CLOS. We'll see how the Meta Object Protocol, or MOP, can be used to extend CLOS in ways ranging from the mundane to the esoteric.
The phrase "object-oriented" can evoke strong feelings of hostility in Lisp aficionados. On the one hand they feel that Lisp has always been object-oriented in some sense and wonder what the fuss is all about; on the other, they are alienated from object-oriented programming as practiced in the C++ and Java worlds, with its heavy emphasis on compile-time bondage and discipline, and feel that the phrase has been hijacked in such a way as to exclude Lisp. I don't disagree with these sentiments. Sometimes I will use the former as an excuse to talk about whatever Lisp subject I want. However, I also intend to react against the latter. There is common ground between object-oriented programming as practiced in Lisp and in other languages, and it is useful for a Lisp programmer to recognize patterns in other languages and see how they apply or not in Lisp, and believe it or not there are lessons to be learned from the object-oriented techniques and idioms of other languages.
We'll start by looking at one small choice a programmer makes in a Lisp program: how to name slot accessors. This choice can't change the correctness of a program and doesn't directly affect its design, but it does affect the readability of the program. Moreover, it reflects how the programmer thinks about the interaction of objects and generic functions. Good accessor names start a programmer down the path of thinking in terms of generic function-based protocols that use objects. This is the essence of object oriented programming in Common Lisp, in contrast to the communicating objects model commonly seen when programming in C++ and Java.
Consider this class definition:
(defclass pie () ((filling :accessor pie-filling :initarg :filling :initform nil) (crust :accessor pie-crust :initarg :crust :initform nil)))
This defines a class pie with two slots,
filling and crust, that can hold data of any
type. Accessors are defined for each slot. More specifically, the
accessors are methods on generic functions. For example, we've
defined two methods for the slot filling: pie-filling, a
reader method which gets the value of the filling slot, and
(setf filling), a writer method which sets it. These
methods are equivalent to these definitions:
(defmethod pie-filling ((obj pie)) (slot-value obj 'filling)) (defmethod (setf pie-filling) (newval (obj pie)) (setf (slot-value obj 'filling) newval))
Why did we include "pie", the class name, in the accessor names?
Obviously the correctness of the generated methods isn't an issue; the
accessor names are decoupled from the slot names. We prepended
pie- to the accessor names out of force of habit.
Various Lisp textbooks recommend this style and construct examples
with it. The defstruct facility creates accessors with this style of
name by default; in some Lisp books there is an emphasis on
structuring code so that CLOS classes can eventually be replaced by
structures for (usually nebulous) performance reasons. The programmer
may feel a need to "uniquify" the accessor name -- in this case
distinguishing it from all other filling accessors -- even though the
accessors are generic functions that will only be invoked via method
dispatch on pie objects. This may be associated with a fear of
colliding from similarly named functions from another problem domain,
dental fillings, for example. The programmer might also feel that
embedding the class name in the accessors provides additional
documentation in functions that use the accessors, a kind of Hungarian
notation for Lisp.
These reasons don't stand up to scrutiny. 10 years ago CLOS implementations were immature and their performance was not well understood. It might have been desirable to have an escape hatch to using structures instead of CLOS classes then, but now in the 21st century we are more comfortable with CLOS performance. The enormous expressive advantages of classes over structures make it unlikely that one will want to move from classes to structures except in a few limited circumstances.
Name collisions are best avoided by using the package system. In our
example above our pie definition might be in the "BAKER" package; we'd
expect a dental filling function to be defined in the "DENTIST"
package. If we ever wanted to use both functions in a single program
-- say one that explores the effects of eating pie on getting cavities
-- we could carefully control how identical symbol names map to
symbols viause-package, export,
import, and the rest of the package system machinery.
We'll say more about that in a future article.
The last reason is the most interesting one to wrestle with because it is an aesthetic choice that influences the way the programmer thinks about a program. If you include the class name in the accessor, you're implicitly assuming that the argument to the accessor is of that class, or at least a subclass of it. Consider a method on our pie class:
(defmethod make-shopping-list ((dish pie)) (append (ingredients (pie-crust dish)) (ingredients (pie-filling dish))))
It's clear that repeating the name of the class in all the accessors
is not adding any documentation value; we know the type of dish, because
it's right there in the method lambda list! Even in a large method it
would not be hard to find the types of the arguments to the crust and
filling accessors. Persuaded by these arguments, we rewrite the class
definition of pie to use the same names for slots and
their accessors:
(defclass pie () ((filling :accessor filling :initarg :filling :initform nil) (crust :accessor crust :initarg :crust :initform nil)))
and rewrite the make-shopping-list method as:
(defmethod make-shopping-list ((dish pie)) (append (ingredients (crust dish)) (ingredients (filling dish))))
Looking at this definition, we have to ask ourselves, "is pie unique in being composed of crust and filling?" I'm not actually a baker, but I tend to think not. If we have tarts, quiche and galettes in our world then we could save some work and generalize the shopping list method:
(defmethod make-shopping-list (dish) (append (ingredients (crust dish)) (ingredients (filling dish))))
Now we have a method that works for all baked goods that have a crust
and a filling. This genericity is the acme of object-oriented
programming in Common Lisp. The essence of a Lisp program lies in the
semantics of its generic functions, and not so much in the class
hierarchy or in class containment relationships. In our
make-shopping-list example, any object for which the crust and
filling methods are implemented can participate in our
make-shopping-list protocol, regardless of its class and
superclasses. While this would have been true if we'd kept the old
pie-crust and pie-filling names, we would have been much less
likely to think in these terms, and it would be plain confusing to
describe a general shopping list protocol in terms of pie properties.
At this point we might turn the above make-shopping-list
method into a plain old function. We won't though, in order to allow
users to write methods that override it or combine with it. It might
make sense to use append method combination in the
make-shopping-list generic function, in order to allow
all applicable methods to contribute ingredients:
(defgeneric make-shopping-list (dish) (:method-combination append)) (defmethod make-shopping-list append (dish) (append (ingredients (crust dish)) (ingredients (filling dish))))
Your credulity might be stretched by our assumption that a general
shopping list creator only needs to examine crust and filling needs
and, while I'm a big fan of baked goods, I agree. In defining our
make-shopping-list method to apply to any object we are being a bit too
promiscuous; we only want to apply to objects that have a crust and a
filling or, in other words, objects that support the
crusty-container protocol. We can demand that such
objects inherit from a crusty-container protocol class.
This class has no slots or
methods; it merely indicates (and demands) that its subclasses do.
When defining a protocol class its common to also define a standard
subclass of it that other classes can inherit for implementation,
especially if the protocol is implementable in terms of class slots.
Using these principles, let's revisit the pie problem:
(defclass crusty-container () ()) (defclass standard-crusty-container (crusty-container) ((filling :accessor filling :initarg :filling :initform nil) (crust :accessor crust :initarg :crust :initform nil))) (defclass pie (standard-crusty-container) ()) (defmethod make-shopping-list append ((dish crusty-container)) (append (ingredients (crust dish)) (ingredients (filling dish))))
We've explored the consequences of choosing good accessor names and
we'll finish up with some advice for choosing them. Generally the
slot and accessor names should be the same unless there's a good
reason; one such reason might be that the accessor will be exported
from a package, but the slot name will not. The name should describe
the contained object and not the container. The containing class'
name should not be prepended to an accessor name. Sometimes this is
not possible because the most obvious slot name does contain the class name
This may indicate that the contained object is tightly
coupled with the containing object and is not a good candidate to open
up to a generic protocol. For example, in a class nose,
nose-ring is a reasonable slot name; we sure hope it
isn't any other kind of ring. Or it may be that it's just the way it
is and it's best not to get too stressed about it. We'll be returning
to the theme of flexibility again and again as we look at object
oriented programming in Common Lisp.
End.
Copyright © 2002 Tom Moore. All rights reserved. Permission to copy this document unmodified and in its entirety is granted.