The Art of the Metaobject Protocol, and ENSURE-CLASS-USING-CLASS: Part 2
In Part 1 of this accidental series, I provided a first draft of an implementation of ensure-class-using-metaclass. It relied on capturing a primary method invocation for ensure-class-using-class in a closure and passing that to ensure-class-using-metaclass. Unsurprisingly, the first version had some issues.
The first issue is that the lambda lists of the generic function and methods I defined provided direct-default-initargs, direct-slots, and direct-superclasses as arguments in all cases (with plausible NIL values). This matches the specification of ensure-class-using-class on page 183 of AMOP. However, it collided with SBCL’s implementation of conditions:
Invalid initialization argument:
:DIRECT-DEFAULT-INITARGS
in call for class #<STANDARD-CLASS SB-PCL::CONDITION-CLASS>.
[Condition of type SB-PCL::INITARG-ERROR]
See also:
Common Lisp Hyperspec, 7.1.2 [:section]
Well, whoops. Apparently we need to support arbitrary initialization arguments passed to ensure-class-using-class/ensure-class-using-metaclass, including fewer than anticipated. The fix is simple:
(defgeneric c2mop:ensure-class-using-metaclass
(metaclass class name
&key %ecuc-method &allow-other-keys)
...)
(defmethod c2mop:ensure-class-using-class :around
(class name &rest args &key (metaclass 'standard-class) &allow-other-keys)
...)
(defmethod c2mop:ensure-class-using-metaclass
(metaclass class name
&rest args &key %ecuc-method &allow-other-keys)
...)
The second problem I encountered was more difficult: My methods weren’t applicable!
(defmethod c2mop:ensure-class-using-metaclass
((metaclass my-metaclass) class name &rest args &key)
...)
After some initial confusion, I realized why: I was passing in (find-class 'my-metaclass) to the metaclass parameter. But that’s an instance of standard-class , not of my-metaclass. Whoops!
The first solution I thought of was to extend the language for method specialization in Lisp. If we could specialize on the subclass relations extracted directly from the classes instead of on instances – well, that would solve my problem. Turns out there’s some prior art in more general specialization too: predicate dispatch CLOS, filtered functions CLOS, and Custom Specializers in Object-Oriented Lisp, for example.
However, the point at which classes get finalized is typically when you make instances from them. Which means I’d need to finalize at new, odd times for the above approach to work.
The second solution I thought of was moving the work I was doing into initialize-instance and reinitialize-instance, specialized on my-metaclass. This might have worked, but it felt wrong – it’s not really the initialization of my-metaclass I wanted to change, but the definition, the act of defining the classes. So I discarded this option as being essentially a pun.
If only I could make an instance of my-metaclass that skipped all the metaclass work, and was basically an empty instance I could pass around for specialization purposes!
Well, that’s simple. c2mop:class-prototype does precisely that (although, warning, it’s permitted to invoke initialize-instance). So instead of passing in (find-class 'my-metaclass) to the metaclass parameter, I now pass in a prototype instead. Problem solved!
And with those two fixes, the ensure-class-using-metaclass generic has been working like a charm. If it stops working like a charm, I’ll have to write a part 3 to this series.