Search code examples
inheritancemethodscommon-lispreusabilityclos

CLOS: how to call a less specific method?


There is a generic method, say incx. There are two versions of incx. One specialized on type a, and one specialized on type b. Type b is a subclass of a. You are given an object of type b, the derived type - but you want to call the method that is specialized on type a. You could do this easily if there wasn't already a method of the same name specialized on type b, but alas, there is such a method.

So how do you call the method specialized on type a in such a situation?

(defclass a () ((x :accessor x :initform 0)))
(defclass b (a) ((y :accessor y :initform 0)))

(defgeneric inc (i))

(defmethod inc ((i a)) (incf (x i)))
(defmethod inc ((i b)) (incf (y i)))

(defvar r (make-instance 'b))

As promised by CLOS, this calls the most specialized method:

* (inc r) 
* (describe r)
    ..
  Slots with :INSTANCE allocation:
    X  = 0
    Y  = 1

But this in this particular case, (not in general) what I want is to access the less specialized version. Say something like:

(inc (r a)) ; crashes and burns of course, no function r or variable a
(inc a::r)  ; of course there is no such scoping operator in CL

I see the call-next-method function can be used from within a specialized method to get the next less specialized method, but that isn't what is wanted here.

In the code this was cut out of, I do need something similar to call-next-method, but for calling a complementary method. Rather than calling a method of the same name in the next less specialized class, we need to call its complementary method, which has a different name. The complementary method is also specialized, but calling this specialized version doesn't work - for much the same reasons that call-next-method was probably included for. It isn't always the case that the required method specialized on the super class has the same name.

(call-next-method my-complement)  ; doesn't work, thinks my-complement is an arg

Here is another example.

There is a base class describing electron properties and a derived class describing the properties of a "strange-electron". Methods specialized on the strange electron desire to call methods specialized on the electron. Why? because these methods do the normal electron part of the work for the program. The non-electron part of the strange electron is almost trivial, or rather it would be if it didn't duplicate the electron code:

(defgeneric apply-velocity (particle velocity))
(defgeneric flip-spin (particle))

;;;; SIMPLE ELECTRONS

(defclass electron ()
  ((mass
      :initform 9.11e-31
      :accessor mass)
   (spin
      :initform -1
      :accessor spin)))

(defmacro sq (x) `(* ,x ,x))

(defmethod apply-velocity ((particle electron) v)
  ;; stands in for a long formula/program we don't want to type again:
  (setf (mass particle) 
        (* (mass particle) (sqrt (- 1 (sq (/ v 3e8)))))))

(defmethod flip-spin ((particle electron))
  (setf (spin particle) (- (spin particle))))

;;;; STRANGE ELECTRONS

(defclass strange-electron (electron)
  ((hidden-state
      :initform 1
      :accessor hidden-state)))

(defmethod flip-spin ((particle strange-electron))
  (cond
    ((= (hidden-state particle) 1)
     (call-next-method)

     ;; CALL ELECTRON'S APPLY-VELOCITY HERE to update
     ;; the electron. But how???
     )
    (t nil)))

;; changing the velocity of strange electrons has linear affect!
;; it also flips the spin without reguard to the hidden state!
(defmethod apply-velocity ((particle strange-electron) v)
  (setf (mass particle) (* (/ 8 10) (mass particle)))

  ;; CALL ELECTRON'S SPIN FLIP HERE - must be good performance,
  ;; as this occurs in critical loop code, i.e compiler needs to remove
  ;; fluff, not search inheritance lists at run time
  )

It all reduces to a simple question:

How to call the less specialized method if a more specialized one has been defined?


Solution

  • Dirk's answer has a couple of problems which can be fixed, as shown here.

    Firstly, it does not generalize without becoming a new static object system. When attempting generalization, one quickly runs into the fact that all methods belonging to the same generic definition have the same name. In order to fix this problem one is left to give the functions munged names reflecting their type signature (as per Stroustrup's famous macro processor).

    Secondly, when generalized it becomes a separate static object oriented system. As a static system it does not play well with CLOS. It becomes a case of mixed paradigms.

    However, Dirks approach of avoiding code duplication can be kept local without exporting the auxiliary routines to the interface. This can be accomplished by wrapping them in CLOS methods. These CLOS methods then become branches in the specialization tree, one's that can be specialized separately from other branches. The name change then represents a branch rather than a type signature (more manageable).

    So here is the encapsulated auxiliary function approach applied to the inc example. Note that inc-a becomes a less specialized function that can be called by others, including methods specialized on the inherited b class, as no methods in the b class specialize it further (unlike for inc).

    (defclass a () ((x :accessor x :initform 0)))
    (defclass b (a) ((y :accessor y :initform 0)))
    
    (defgeneric inc (i))
    (defgeneric inc-a (i)) ; same as inc, but won't be further specialized
    
    (defmacro inc-a-stuff (i) ; this is not exported! not an interface
      `(incf (x ,i))
      )
    
    (defmethod inc ((i a)) (inc-a-stuff i))
    (defmethod inc ((i b)) (incf (y i)))
    
    ;; provides a method to generalize back to class a
    ;; this method does not get further specialization by b, thus
    ;; remains a window into the "a part"
    (defmethod inc-a ((i a)) (inc-a-stuff i))
    
    (defvar r (make-instance 'b))
    
    (inc r) ; all good, increments y
    
    ;;(inc (r a)) ; ah how do you get this?
    ;;
    (inc-a r) ; 
    
    (describe r)
    
    #|
    Slots with :INSTANCE allocation:
      X  = 1
      Y  = 1
    |#
    

    This solution is hazard free for dynamic changes to the object schema. I.e. it works within CLOS.