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