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.