Understanding TclOO method dispatching
One of the features of the Ruff! documentation generator is automatic discovery of the entire callable interface of a class, including methods that have been inherited or mixed-in. In addition to enumeration of the available methods, this requires linking to the exact method implementation that is invoked for a method when multiple superclasses and mix-ins implement the same method. Most of this method search is straightforward but in the process of implementing it, I learnt a few things that were not immediately obvious (although documented) from the official reference pages.
TclOO allows defining methods on individual objects as well as part of a class definition. This note only deals with the latter to keep things simple for the present.
First, a quick review of the simple stuff. (The examples show working in tkcon/wish/tclsh shell)
(woof) 2 % namespace import oo::*
(woof) 3 % class create Base {
> method m {} {return "Base.m"}
> }
::Base
(woof) 4 % class create Derived { superclass Base }
::Derived
(woof) 5 % Derived create obj
::obj
(woof) 6 % obj m
Base.m
In the console session above, we first import the TclOO commands which reside in the oo:: namespace. We then create a simple class Base, and then derive a child class Derived from it. As the session shows, the method m defined in the Base class is also available in class Derived through class inheritance. We now override this method in the child class (one of the nice things about most Tcl object systems is you can modify class definitions on the fly)
(woof) 7 % define Derived { method m {} { return "Derived.m" } }
(woof) 8 % obj m
Derived.m
Invoking m now results in the method defined in Derived being invoked as expected.
Moving on to more simple stuff, methods may also be added to a class through "mix-ins".
(woof) 10 % class create Mixer { method mixie {} {return "Mixer.mixie"}}
::Mixer
(woof) 11 % define Derived {mixin Mixer}
(woof) 12 % obj mixie
Mixer.mixie
Above, we have defined a new class Mixer and mixed it into Derived. As expected, the methods defined by Mixer can be invoked on the Derived class instance obj.
So far, it's all straightforward. Things get a bit more interesting when the mixin includes a method that has the same name as a method defined in the class:
(woof) 13 % define Mixer { method m {} {return "Mixer.m"} }
(woof) 14 % obj m
Mixer.m
Invoking method m on obj results in a call to Mixer's method m. This was a mild surprise to me. My expectation was that methods defined in a class would always override methods that are "imported". Not so. Methods directly mixed into a class (as opposed to mixed into a superclass), override methods defined in the class itself. I'm not sure of the rationale behind this (although I'm sure there must be one) but it is in fact documented behaviour.
What was really surprising though was what happened when Mixer was mixed into the superclass Base.
(woof) 15 % define Base {mixin Mixer}
(woof) 16 % obj m
Derived.m
The result really flummoxed me. When Mixin was mixed into Derived, its methods overrode identically named methods in Derived. Now, when Mixin was mixed into Base as well, the method m in Derived overrode the Mixer version even though the latter was still mixed into the class as before. The key to understanding this (obvious after the fact, but something of a mystery before) is the following sentence from the reference manual page for the next command.
Any particular method implementation always comes as late in the resulting list of implementations as possible.
What does that mean? Basically, because of multiple inheritance as well as mix-ins, a class may occur multiple times in a method search path. When looking at which method in the search path to invoke, all occurences of the class in the method search path are ignored except the last occurence.
So in the example above where Mixin was not mixed into Base, the method search path for invoking method m on an object belong to class Derived looked like Mixer, Derived, Base since methods in directly mixed in classes override methods defined in the class itself. When searching for method m, Mixer is the first implementation encountered and invoked.
After mixing in Mixer into Base, the method search path looks like this: Mixer, Derived, Mixer, Base. As before, Mixer appears before Derived because it is mixed into the latter. Now, however, it also appears just ahead of Base because it is mixed into that class as well. The key point is that when searching for an implementation of method m, as per the quote from the reference manual above, the first occurence of Mixin is ignored because it appears again later in the search path. Thus, now Derived is treated as the first implemented class and its method m is invoked.
At first glance, it seemed odd to me that the last, and not the first, occurence of a class would be the one that mattered but some thought led to an understanding of why it works the way it does. Figuring it out is left as an exercise for the reader (Hint: consider, as an example, the oo::object class which is an ancestor of all derived classes.)
As a further exercise, try the following: given the following class definitions
class create A {
method x {} {return A.x}
method y {} {return A.y}
}
class create B {
method x {} {return B.x}
method y {} {return B.y}
}
class create AA {
superclass A
unexport x
}
class create C {
superclass A B AA
}
C create cobj
what will be returned by the following two commands:
cobj x cobj y
How many times in practice would such subtleties come into play? My guess is not many. But there might be instances where you need to be aware of the exact sequence of method search operations. One example would be a documentation generator that links methods to implementation classes
but others are possible as well, for example a generic serialization mixin. Certainly, it never hurts to be aware of these issues.
- ashok's blog
- Login to post comments
