define-method-combination

Prerequisite: Knowledge of macros. Either fluency with reading and writing them, or enthusiasm for them and willingness to use macrexpand-1 to see what the macros in the text are doing.

1)Macros or CLOS?

If you are learning CL, you need to decide which to learn first, macros or CLOS. Graham has macros in Chapter 10, CLOS in chapter 11. Keene uses macros in her book on CLOS. What neither spot is that if you put off learning CLOS until you are OK with macros, then you can plunge straight into defining your own method combinations.

Some extended classes can inherit methods from their foundation classes. Others must have methods defined for them. The goal of method combination is to find ways in which the methods for the extended classes can be defined incrementally. The desired results comes from reusing the method from the foundation class in a suitable combination with a small amount of additional code in the method for the extended class that adds the extra functionality needed in the extended class. Simply running a method for the extended class in place of the method for the foundation class is common, but is a small defeat in the battle for code reuse.

If you start with defining your own method combinations, you can focus on the goal: exploiting the fact that most of the code for your new method is already present in the existing methods higher up the class hierarchy. Ofcourse you end up "re-inventing the wheel", coming up with ways of reusing code that could be done just as smoothly with the built-in standard method combination. Standard method combination is jolly useful. The first advantage to starting off by defining ones own method combinations is that it demystifies method combination, by presenting it as a familiar programming task. One has a list of applicable methods and needs to write a macro to pick out the one that you want. The second advantage is that one aquires the right attitude to standard method combination: it is a tool not an idol. The third advantage is that if you spot an opportunity for using a different kind of method combination, you can take it. No need to lament that you could make your code smaller and clearer, if only you knew how to write your own method combinations. The fourth advantage is that you can use weird method combinations to write mind boggling code that will make other programmers heads explode. Perhaps that should be three advantages and one disadvantage.

2)First we need some classes

Here is a class definition which we will use later.

(defclass flat-bed-truck
    () ;List of superclasses is empty since this is our first class
    ( ;extra open paren packages the slot names into a list
      (chasis-weight :accessor chasis-weight
	             :initarg :chasis-weight)
      (cab-colour :accessor cab-colour
		  :initarg :cab-colour)
      (logo-colours :accessor logo-colours
		    :initarg :logo-colours)
    ) ;end of list of slot names
    ;which leaves a place for class options
    )

This specifies three slots, chasis-weight, cab-colour and logo-colours. It creates three methods and three keywords to use as arguments to make-instance. We are very unimaginative about names, reusing the slot names. Here is a method

(defmethod paint-list ((truck flat-bed-truck))
    (cons (cab-colour truck)
	  (logo-colours truck)))

Here is how you use it.

(defvar bertha-brick-bus
    (make-instance 'flat-bed-truck
		   :chasis-weight 5000
		   :cab-colour 'green
		   :logo-colours '(red white blue)))
(paint-list bertha-brick-bus) => (GREEN RED WHITE BLUE)

So far defclass just looks like defstruct spoiled. There is history here. Think yourself back to 1985 and imagine that you are designing the syntax of defclass. You have been using macros since 1970. You want defclass to be flexible, but you don't much care if the flexibility is bought at the cost of verbosity because your macros will be typing the things in for you. You do care about simplicity and you have a particular notion of simplicity in mind: a `fill in the blanks' kind of simplicity that makes a construct an easy target for a macroexpansion. You want to be able to write code such as

(defun duplicate-names-for-simple-slot(slot-name)
    `(,slot-name :accessor ,slot-name
		 :initarg ,(intern (symbol-name slot-name)
				   'keyword)))

which does

(duplicate-names-for-simple-slot 'foo)
 => (FOO :ACCESSOR FOO :INITARG :FOO)

and lets you write a macro

(defmacro define-simple-class(name super-classes &rest slots)
    `(defclass ,name ,super-classes
       ,(mapcar #'duplicate-names-for-simple-slot
		slots)))

The implication is that you are going to miss the automatic accessor generation of defstruct for the eight minutes fourteen seconds that it takes to write a define-simple-class macro. Notice the very different perspective that macro writing gives you. The syntax of defclass is

(defclass name(superclass*)(slot-spec*)class-spec*)

The third item in the body of the defclass is list of slot-specifications. This is different from the third and subsequent items being the slot-specifications. One could say that there is an extra pair of parentheses. Alternatively one could say that the slot-specifications are put in a defclass using comma instead of comma-at.

Notice the terrible dilemma that I face as I try to write this web page. If I use define-simple-class I am not teaching CLOS, I am teaching Alan's macros on CLOS. If I do not use define-simple-class I am not teaching CLOS, I am teaching a strange bondage-and-discipline language that gives you RSI by making you type in your slot names in triplicate. There is a way out of this dilemma.

(class-of bertha-brick-bus)
=> #<STANDARD-CLASS FLAT-BED-TRUCK {4847336D}>

Which is mostly as expected, but why STANDARD-CLASS? Is there any other kind of class?

(mapcar (function class-of)
	  '(3 22/7 3.1416 (sqrt -1) #(1 2 3) atom (a b c)))
(#<BUILT-IN-CLASS FIXNUM (sealed) {28F134F5}>
 #<BUILT-IN-CLASS RATIO (sealed) {28F689E5}>
 #<BUILT-IN-CLASS SINGLE-FLOAT (sealed) {28F685B5}>
 #<BUILT-IN-CLASS CONS (sealed) {28F68ED5}>
 #<BUILT-IN-CLASS SIMPLE-VECTOR (sealed) {28F10665}>
 #<BUILT-IN-CLASS SYMBOL (sealed) {28F69BAD}>
 #<BUILT-IN-CLASS CONS (sealed) {28F68ED5}>)

In CL everything is an object. Not in an object obessive kind of way, with everything being a full-feature class with an inevitable overhead, but in a useful, pragmatic sort of way, in which there are classes corresponding to many of the types in the tradition Lisp type system. This lets you use methods to good effect without first having to define classes and create instances.

3)Next we need some methods

A method looks like this

(defmethod what-is ((x integer))
   (format t "~A is an instance of ~A.~%"
           x 'integer)
   x)

Obviously we want several methods, just like that one, to play with. So we buy a cookie cutter, sorry, I mean we write a macro

(defmacro example-method(class)
    `(defmethod what-is((x ,class))
       (format t "~A is an instance of ~A.~%"
	       x ',class)
       x))

Now we can stamp out some example methods

(example-method number)
(example-method rational)

and try them out

(mapcar #'what-is (list pi (rationalize pi)(round pi)))
3.141592653589793d0 is an instance of NUMBER.
245850922/78256779 is an instance of RATIONAL.
3 is an instance of INTEGER.
(3.141592653589793d0 245850922/78256779 3)

That is quite pretty, but it is just another way of writing

(defun tell-me-about(x)
    (typecase x
	      (integer (format t "~A is an integer.~%" x))
	      (rational (format t "~A is a rational.~%" x))
	      (number (format t "~A is a number.~%" x)))
    x)

4)Simplest possible method combination

Atleast we now have some methods and can try defining a method combination.

(define-method-combination most-specific()
    ((all *))
    `(call-method ,(car all)))

The first line gives the name of our method combination, most-specific, and also its parameter list. Method combinations can be parameterised. That is define-method-combination actually defines families of method combinations. You can see from the empty parameter list that we are declining to take advantage of this.

The second line is a list of variable bindings. We bind only a single variable, giving it that curious double parenthesised appearance. That should be familiar from (let ((x 1.4))...). `*' is the built-in wildcard symbol. `all' is an arbitary identifier. I chose `all' because using the wildcard gathers all the applicable methods into a list.

The final line is the meat of it, and contains a nasty twist. You'd think it would be `(call-method (car all)) and simply expand to (call-method (car all)), but call-method is a macro that expects a method object, not code to run to find one. Worse, the binding of all is there for expand-time, but is gone by run-time. So the comma, getting (car all) to run at expand time, is crucial.

When we ran defmethod, it spotted that there wasn't a generic function what-is already defined, so it made one, using standard method combination. Now we need to do defgeneric ourselves, to specify our own method combination.

(defgeneric what-is (thing)
    (:method-combination most-specific))

and we need to re-run the defmethods, in order to pick up the new method combination

(progn (example-method integer)
       (example-method number)
       (example-method rational))

If it has all worked, we get exactly what we got before. The simplest possible method combination is the running the most specific method, and it is what CLOS provides by default.

4.1)Checking it out

Rather than rush on to do something useful, lets just play with this a little. Our methods both print diagnostic messages and return values. We can discard the return value and still see what is going on. Let us make the method combination return the number of applicable methods that it saw.

(define-method-combination return-count()
    ((all *))
    `(progn (call-method ,(car all))
	    ,(length all)))

Once again, be alert to the nasty twist. The macro has to expand to something like (progn (call-method #<method object>) 2). Now we get

(mapcar #'what-is (list pi (rationalize pi)(round pi)))
3.141592653589793d0 is an instance of NUMBER.
245850922/78256779 is an instance of RATIONAL.
3 is an instance of INTEGER.
(1 2 3)

The return values are telling us that when we passed what-is a double float, it found only one applicable method, when we passed it a rational, it found two applicable methods, and when we passed it an integer it found three. Lets get all the methods to run. We could do with a helper function for the macro-expansion

(defun wrap-in-call-method(method-list)
    (loop for method in method-list 
	  collect `(call-method ,method)))

Then we can define run-all

(define-method-combination run-all()
    ((all *))
    `(progn ,@(wrap-in-call-method all)))

and after re-running defgeneric and the defmethods, to pick up the new method combination, we get

(mapcar #'what-is (list pi (rationalize pi)(round pi)))
3.141592653589793d0 is an instance of NUMBER.
245850922/78256779 is an instance of RATIONAL.
245850922/78256779 is an instance of NUMBER.
3 is an instance of INTEGER.
3 is an instance of RATIONAL.
3 is an instance of NUMBER.
(3.141592653589793d0 245850922/78256779 3)

5)Accessing arguments

If we give define-method-combination a (:arguments ..) list it will set up bindings to forms that read the arguments passed to the generic function. We can insert these forms into our method combination code. It goes like this

(define-method-combination report-first-argument()
  ((all *))
  (:arguments code-to-grab-first-argument)
  `(print ,code-to-grab-first-argument))

(defgeneric available-argument (first second)
  (:method-combination report-first-argument))

(defmethod available-argument(first second)
  "Not actually called"
  (declare (ignore first second)))

(available-argument 'foo 'bar) 
=>
FOO 
FOO

It is instructive to rename everything to make this appear simpler than it really is


(define-method-combination report()
  ((all *))
  (:arguments x)
  `(print ,x))

(defgeneric with-arg(x y)
  (:method-combination report))

(defmethod with-arg(x y)
  (declare (ignore x y)))

(with-arg 'foo 'bar)
=>
FOO 
FOO

See what I've done? I've renamed the variables to make (:arguments x) look like an ordinary lambda-list. This suggests that the combination code should be simply (print x). The comma in (print ,x) becomes mysterious.

Now we understand about the mystery comma we can play. Here we define classes for low order polynomials. The constant polynomial is easy enough. The linear polynomial class inherits its constant from the constant class, and the quadratic has its own slot for the coefficient of x squared, but gets both its other slots by inheritance from the linear class. We write a method combination specifically for evaluating polynomials. The methods for evaluating polynomials become trivial. They just pass their specific coefficents to the method combination, which looks at the value of x and does the rest.

(define-simple-class constant ()constant)
(define-simple-class linear (constant) linear)
(define-simple-class quadratic(linear) quadratic)


(defun make-horner-form(var list)
  (cond ((endp list)(error "Mustn't recurse down to empty list."))
	((endp (cdr list))
	 `(call-method ,(car list)))
	(t `(+ (* ,(make-horner-form var (cdr list))
		  ,var)
	       (call-method ,(car list))))))

(define-method-combination polynomial()
  ((all * :order :most-specific-last))
  (:arguments x)
  (make-horner-form x all))

(defgeneric calc(argument polynomial)
  (:method-combination polynomial))

(defmethod calc(x (p constant))
  (constant p))

(defmethod calc(x (p linear))
  (linear p))

(defmethod calc(x (p quadratic))
  (quadratic p))
(defparameter *p*
  (make-instance 'quadratic
                 :constant 3
                 :linear 7
                 :quadratic 1))

(calc 10 *p*) => 173
(calc 0.1 *p*) => 3.71

This example is quite bonkers because polynomials ought to be stored as lists or vectors of coefficients, not live in class hierarchies. Nevertheless, it illustrates an ideal. Suppose we want cubics.

(define-simple-class cubic (quadratic) cubic)
(defmethod calc(x (p cubic))
  (cubic p))
(setf (cubic *p*) 2)
(calc 10 *p*) => 2173
(calc 0.1 *p*) => 3.712

That is what we are aiming for with OOP. Get everything set up, then the code to compute a cubic is just (cubic p) and not

(+ (* (+ (* (+ (* (cubic p) x)
	(quadratic p)) x) (linear p)) x) (constant p))