🚀 go-pugleaf

RetroBBS NetNews Server

Inspired by RockSolid Light RIP Retro Guy

Article View: comp.lang.clos
Article #690

Re: Elements of CLOS Style

#690
From: Gregor Kiczales
Date: Sat, 29 Jun 1991 17:24
137 lines
7031 bytes

   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
Technical Details
Message-ID: <GREGOR.91Jun29102433@tracer-bullet.parc.xerox.com>
Path: arc1-ca.pugleaf.net!utzoo!utgpu!news-server.csri.toronto.edu!rpi!usc!apple!decwrl!parc!gregor
References: <19910626133701.1.JMORRILL@adams.bbn.com>

Show Full Headers
Complete Article Headers
Path: arc1-ca.pugleaf.net!utzoo!utgpu!news-server.csri.toronto.edu!rpi!usc!apple!decwrl!parc!gregor
From: gregor@parc.xerox.com (Gregor Kiczales)
Newsgroups: comp.lang.clos
Subject: Re: Elements of CLOS Style
Message-ID: <GREGOR.91Jun29102433@tracer-bullet.parc.xerox.com>
Date: 29 Jun 91 17:24:33 GMT
References: <19910626133701.1.JMORRILL@adams.bbn.com>
Sender: news@parc.xerox.com
Organization: Xerox Palo Alto Research Center
Lines: 137
In-Reply-To: jmorrill@bbn.com's message of 26 Jun 91 13:37:00 GMT