[comp.lang.clos] Elements of CLOS Style

jmorrill@bbn.com (Jeff Morrill) (06/26/91)

barmar@think.com says:
                          Unfortunately, there still isn't enough
  experience in the industry in proper design and documentation of OO
  components, so you end up with code like the above.

I think it is possible to come up with some "Elements 
of CLOS Style", in the spirit of Strunk and White, that
contribute to reusable code.  It is my experience that
CLOS has more than enough tools to write simple
and general code that obeys data encapsulation.
What we need now are heuristics that provide guidance
in the selection of the right tools at the right times.

Here are a few of my personal rules which I recommend:

1.  If it can be done without extending the MOP, don't 
extend the MOP.

2.  Separate classes that provide BEHAVIOR (methods) from 
classes that provide STRUCTURE (slots).  Otherwise one
tends to implement behavior that makes too many assumptions 
about the structure of the objects involved.

3.  Avoid the use of SLOT-VALUE and WITH-SLOTS.  It is an 
indication of a missing accessor method.  

4.  Avoid the use of TYPECASE.  If you find your code
riddled with

  (typecase object
     (vehicle (vehicle-speed object))
     (frog (frog-speed object))
     (...))

Then you probably would have been better off with a
generic function called SPEED.

5.  Avoid the use of TYPEP.  Replace (typep object 'frog) with
(frog-p object) where:

(defmethod frog-p ((object t)) nil)
(defmethod frog-p ((object frog)) t)

Using a method rather than a function ensures that other
applications can extend the definition.

6.  Avoid including the name of a class in the name
of a method.  For example, rather than naming it FROG-COLOR,
just name it COLOR, since someone else will surely want to 
reuse the generic function for things other than frogs.

7.  Use &ALLOW-OTHER-KEYS in the argument list of a generic 
function if you expect to encounter argument list conflicts 
some day.

8.  Avoid writing a method that cannot fit in one screenful.

jeff morrill
jmorrill@bbn.com

jonl%kuwait@lucid.com (Jon L White) (06/27/91)

I rather like the idea of some community-based style guidelines; at the
very least, it would provide a resevoir of FAQ's -- like, suggesting to
use PROGN method combination rather than explicit kludges.


But a couple of explicit comments.


re: 1.  If it can be done without extending the MOP, don't 
    extend the MOP.

Do you mean to say "make use of" rather than "extend"?



re: 5.  Avoid the use of TYPEP.  Replace (typep object 'frog) with
    (frog-p object) where:

This would be a very implementation-specific piece of advice, *if*
the issue you are worried about is running speed.  Recent discussions
on the common-lisp@mcc.com email list showed that most implementations
do some very aggressive optimizations for TYPEP (and even after they
are fixed up for the kinds of glitches that lgm pointed out, they
stil shouldn't be much worse than calling a random generic function
or so.)  If the issue is, however, only the stylistic one of not
using the generalized membership tester -- TYPEP -- well, then I
won't have much to say now about that.


-- JonL --

jmorrill@bbn.com (Jeff Morrill) (06/28/91)

jonl@lucid.com says:
  I rather like the idea of some community-based style guidelines; at the
  very least, it would provide a resevoir of FAQ's -- like, suggesting to
  use PROGN method combination rather than explicit kludges.

  But a couple of explicit comments.

  re: 1.  If it can be done without extending the MOP, don't 
      extend the MOP.
  Do you mean to say "make use of" rather than "extend"?

Better choice of words.  The Meta-Object Protocol is extremely
powerful, and one day (soon?) we will be able to use a portable,
stable version to implement some very interesting behavior.  One
should not use a bazooka to kill a mosquitoe, however, even if it
might work.

  re: 5.  Avoid the use of TYPEP.  

  This would be a very implementation-specific piece of advice, *if*
  the issue you are worried about is running speed.  

Quite true, I hadn't considered speed because these predicates
play such a small role in the overall efficiency of the
programs I write.  "Aggressive" optimization can make TYPEP quite
fast, but from what I've seen, the second argument must be a constant.
Hard-coding a constant makes things faster but less accessible to
programmers who wish to reuse the code.  I should have said:

5.  Avoid the use of TYPEP, except where speed is critical.
     (Speed is not always the most important thing.)

The same can be said for SLOT-VALUE.  Where the second argument
is a constant, one might get really fast slot access, but at
what cost to data encapsulation?

thanks for your insights,

jeff morrill
jmorrill@bbn.com

gregor@parc.xerox.com (Gregor Kiczales) (06/30/91)

   Date:	Wed, 26 Jun 1991 06:37:00 -0700
   From:	jmorrill@bbn.com (Jeff Morrill)

   I think it is possible to come up with some "Elements of CLOS Style",
   in the spirit of Strunk and White, that contribute to reusable code.

I agree.  After all, the basic CLOS design is now more than five years
old.  Moreover, much of the experience with other OO languages, can be
useful in developing a CLOS style.  In fact we already have a very good
start on this, in the form of Sonya Keene's book.

Unfortunately, good CLOS style is a subtle thing.  I found a few of your
proposed rules too terse to capture the full point they addressed.  In
this message, I want to say a little more about two of those rules.

   1.  If it can be done without using the MOP, don't extend the MOP.

(Note first that I have rewritten this rule, as suggested by JonL.)

I agree with the essence of this rule.  The MOP is a very powerful tool,
and it should, in general, be used sparingly.  One good way to think
about using the MOP is to realize that what the it allows you to do is
create an alternate programming language.  So, before using the MOP, ask
yourself the question "Do I need to define an alternate programming
language for this problem, or is standard CLOS good enough?"  I have
found that posing the question this way provides the proper bias towards
being conservative about the use of the MOP.

On the other hand, there are many ways to use the MOP which have a quite
subtle effect.  In these cases, the effect isn't so much to create an
alternative language as it is to give a better handle on the existing
language.  That is, even though it appears to be a big hammer, it can be
used quite delicately.  An example from a program I recently wrote
serves to show this.

The program in question does flow analysis on Scheme programs.  There
are a number of phases of the flow analysis, and each phase propagates
one or more categories of information about the program.

What I decided to do was define one class for each category of
information.  Instances of those classes were the actual information
propagated.  But I needed to do one more thing, which was to keep track
of the order in which the categories had to be handled.  I could have
kept this in a table separate from the class structure, but that would
have been cumbersome.  Instead I decided to define a special kind of
class, which in behavior was exactly the same as standard classes, but
which had a couple of extra slots which I could use to keep track of the
order information.

An excerpt from the code shows how this works:

(defclass category (standard-class)
     ((precedes       :initform ()		        ;A list of categories which
		      :accessor category-precedes)	;must precede this category.
      
      (simultaneous   :initform ()		        ;A list of categories which
		      :accessor category-simultaneous))	;must be done simulatenously
						        ;with this category.
  (:default-initargs :direct-superclasses (list (find-class 'information))))

(defmacro define-category (name initial-value builds &optional supers)
  `(defclass ,name ,(or supers '(information))
	()
     (:metaclass category)))

(defmacro define-category-precedes (name x)	;Add name to CATEGORY-PRECEDES of x.
  `(load-category-precedes ',name ',x))
(defmacro define-category-simultaneous (name x)	;Add name to CATEGORY-SIMULTANEOUS of x.
  `(load-category-simultaneous ',name ',x))

;;;
;;; Note that the nonsense with SET-CATEGORY-PRECEDES and SET-CATEGORY-SIMULTANEOUS 
;;; instead of #'(SETF CATEGORY-PRECEDES) etc. is so that this can run in PCL.
;;;
(defun load-category-precedes (name x)
  (flet ((set-category-precedes (nv cat) (setf (category-precedes cat) nv)))
    (load-category-ordering name x #'category-precedes #'set-category-precedes)))
(defun load-category-simultaneous (name x)
  (flet ((set-category-simultaneous (nv cat) (setf (category-simultaneous cat) nv)))
    (load-category-ordering name x #'category-simultaneous #'set-category-simultaneous)
    (load-category-ordering x name #'category-simultaneous #'set-category-simultaneous)))

(defun load-category-ordering (name x reader setter)
  (flet ((find-category (name)
	   (or (find-class name nil)
	       (error "The category named ~S doesn't exist." name))))
    (funcall setter 
	     (remove-duplicates (cons (find-category name)
				      (funcall reader (find-category x))))
	     (find-category x))))

Again, notice that this code doesn't alter the behavior or
implementation of CLOS.  It simply makes it possible for me to keep
information about ``what the CLOS program is representing'' directly
with the program, rather than in a separate table.

   3.  Avoid the use of SLOT-VALUE and WITH-SLOTS.  It is an 
   indication of a missing accessor method.

I think that there are valid CLOS programming styles in which explicit
use of SLOT-VALUE and WITH-SLOTS are made.  In my code, I often use
these for access to slots which is so primitive that I want to make sure
that no user (or subclasser) of my code can try to specialize it.

For example, suppose I have an object, with state which can initialized,
and then read, but which can't otherwise be changed after
initialization.  In this case, I will define a :READER for those slots,
and I will use (SETF SLOT-VALUE) inside the INITIALIZE-INSTANCE method
to write the slots.  I explicitly don't define an :WRITER method (or use
:ACCESSOR) because I don't want to give the suggestion that its legal
for users, or specializers of my program to write those slots.

There are other cases, such as the famous X Y RHO THETA implementation
of points, where it makes sense to use SLOT-VALUE directly.  I claim
that the following is elegant code:


(defclass position ()
     ((x :initform 0)
      (y :initform 0)))

(defmethod pos-x ((p position)) (with-slots (x) p x))
(defmethod pos-y ((p position)) (with-slots (y) p y))

(defmethod pos-rho   ((p position)) (with-slots (x y) p (sqrt ...)))
(defmethod pos-theta ((p position)) (with-slots (x y) p (atan y x)))


The instances have some raw state, which happens to be stored as X and
Y, and two interfaces, an x-y interface and a rho-theta interface.  It
isn't appropriate to implement one in terms of the other (in other words
define readers for x and y and then call those inside of rho and theta)
because both interfaces are ``at the same level.''

Gregor