Path: 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: 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 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