CLOSminded: Slot Accessor Names Matter

by Tim Moore

Saturday, 6 April 2002

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.