Debugging Method: Check the (Applicable) Methods

Yesterday, while working on a private project – let’s call it BMCG – I did essentially the same blunder twice.

The work on BMCG was MOP-focused. The first blunder occurred when I was implementing a method on change-class. Here’s a minimal example:

CL-USER> (defclass specializer-object () ())
#<STANDARD-CLASS COMMON-LISP-USER::SPECIALIZER-OBJECT>
CL-USER> (progn (defvar *bucket* nil)
                (defmethod change-class :around ((obj specializer-object) new-class-name &rest initargs &key &allow-other-keys)
                  (push (find-class new-class-name) *bucket*)
                  (call-next-method)))
#<STANDARD-METHOD COMMON-LISP:CHANGE-CLASS :AROUND (SPECIALIZER-OBJECT T) {10023C5123}>
CL-USER> (defclass test-class-a (specializer-object)
           ((some-slot :initarg :some-slot)))
#<STANDARD-CLASS COMMON-LISP-USER::TEST-CLASS-A>
CL-USER> (defclass test-class-b (specializer-object)
           ((some-other-slot :initarg :some-other-slot)))
#<STANDARD-CLASS COMMON-LISP-USER::TEST-CLASS-B>
CL-USER> (defvar test-obj (make-instance 'test-class-a))
TEST-OBJ
CL-USER> (trace change-class)
(CHANGE-CLASS)
CL-USER> (change-class test-obj 'test-class-b :some-other-slot 5)
  0: (CHANGE-CLASS #<TEST-CLASS-A {10027AD0C3}> TEST-CLASS-B :SOME-OTHER-SLOT 5)
    1: (CHANGE-CLASS #<TEST-CLASS-A {10027AD0C3}> #<STANDARD-CLASS COMMON-LISP-USER::TEST-CLASS-B> :SOME-OTHER-SLOT 5)
; Evaluation aborted on #<SB-PCL:ILLEGAL-CLASS-NAME-ERROR {100299ACF3}>.

I spent half an hour carefully trawling through my (full) code version of the above problem before I, frustrated, checked the hyperspec and quickly saw the following:

Method Signatures:
  change-class (instance standard-object) (new-class standard-class) &rest initargs
  change-class (instance t) (new-class symbol) &rest initargs

Duh, no wonder I ended up with an illegal-class-name-error. I’d specialized new-class-name on T, by overlooking the existence of the “implementing method” specializing on standard-class. And thus, the second time around the :around method, I tried to use the class itself as a class-name.

Whoops!

The second blunder occurred when looking at ensure-class-using-class. I had written a method specializing on a metaclass, but it seemed to only fire when redefining the class, not when defining it initially. A stripped-down example:

BMCG> (defmethod c2mop:ensure-class-using-class ((class some-metaclass)
                                                 name &rest args
                                                 &key direct-slots
                                                 &allow-other-keys)
        ...)
;; CANONICALIZE-DIRECT-SLOT's a helper function
;; used by the method on ENSURE-CLASS-USING-CLASS
BMCG> (trace canonicalize-direct-slot)
(CANONICALIZE-DIRECT-SLOT)
BMCG> (defclass cy ()
        ((x :initarg :x :accessor x :magic-property #'magic-function))
        (:metaclass some-metaclass))
#<SOME-METACLASS BMCG::CY>
BMCG> (defclass cy ()
        ((x :initarg :x :accessor x :magic-property #'magic-function))
        (:metaclass some-metaclass))
  0: (BMCG::CANONICALIZE-DIRECT-SLOT (:NAME BMCG::X :READERS (BMCG::X) :WRITERS ((SETF BMCG::X)) :INITARGS (:X) SB-PCL::SOURCE #S(SB-C:DEFINITION-SOURCE-LOCATION :NAMESTRING NIL :INDICES 0) :MAGIC-PROPERTY (FUNCTION MAGIC-FUNCTION)))
  0: CANONICALIZE-DIRECT-SLOT returned
       (:NAME X :READERS (X) :WRITERS ((SETF X)) :INITARGS (:X) SB-PCL::SOURCE
        #S(SB-C:DEFINITION-SOURCE-LOCATION :NAMESTRING NIL :INDICES 0)
        :MAGIC-PROPERTY #<FUNCTION MAGIC-FUNCTION>)
#<SOME-METACLASS BMCG::CY>

Lo and behold, the pre-defined methods are:

 Methods:
         ensure-class-using-class (class class) name &key
 :metaclass
 :direct-superclasses
 &allow-other-keys        
         ensure-class-using-class (class forward-referenced-class) name &key
 :metaclass
 :direct-superclasses
 &allow-other-keys        
         ensure-class-using-class (class null) name &key
 :metaclass
 :direct-superclasses
 &allow-other-keys

Turns out, the class argument is nil if the class hasn’t been defined already. Thus the first time the defclass form got executed, class was nil and the method specializing on some-metaclass wasn’t applicable.

In both cases, the symptomatic weirdness – my expectations not being met – was caused by how I’d overlooked the set of pre-defined methods.

Whoops!

Some Other Observations

ensure-class-using-class can’t specialize on the metaclass argument, since that’s a keyword parameter. Seems like a serious design flaw, since it means typical shells of e.g. applicable :around methods can’t be built, since there’s nothing to specialize them on when class is nil. If metaclass had been a required argument instead, that would have been cleaner in making it possible to build method cakes without having to hack around not having much to specialize on. Imagine wanting to specialize on a subclass of a metaclass?

Turns out what I was doing in ensure-class-using-class is better done in compute-effective-slot-definition though – I was canonicalizing the slot definitions, after all, but that work can be done when computing the effective definitions instead. But since ensure-class-using-class is the “functional counterpart” to defclass, the above is still a problem – I do have some bookkeeping to do when defining classes with a given metaclass. At worst, admittedly, I can hack that in as part of a specialized defclass macro. But it might be uglier than if specializing on the metaclass were possible.

Click Here to Leave a Comment Below