In his series of object orientation RFC's Perl/Raku luminary Damian Conway includes a proposal for method redispatch, RFC 190, which is the subject of today's article.
Perl has a pseudoclass named SUPER
which an object can use to invoke a
method of its parent class that it has overridden. It looks approximately
like this:
sub dump_info {
my $self = shift; # obtain invocant
$self->SUPER::dump_info; # first dump parent
say $self->{derived_info}; # then ourselves
}
In this example, taken loosely from the RFC, we define a method dump_info
in a derived class which dumps the info of its parent class and then anything
that itself added to the object, exemplified by the derived_info
attribute.
Conway notes that this breaks down under multiple inheritance because
SUPER
will only dispatch to the first parent class of $self
and once
you go SUPER
you can't go back.
Supposing that all dump_info
methods in parent classes are similarly
implemented, only the family bonds indicated in the below diagram with
double lines would be traversed, resulting in lots of info potentially
undumped:
Grand11 Grand12 Grand21 Grand22
║ │ │ │
╟────────┘ ├────────┘
║ │
Parent1 Parent2
║ │
╟─────────────────────┘
║
Derived
One might think that to get this right, each class needs to dispatch to all of its parent classes somehow, which would be akin to a post-order traversal of the inheritance tree. This is correct insofar as it models the relevance of methods in the inheritance tree, supposing that left parents are more important than right ones.
Conway's proposal is subtly different. Namely, he proposes to add a new
pseudoclass named NEXT
which is to be used just like SUPER
above and
which should, instead of continuing in the parent of the current package,
resume the original method dispatch process with the next appropriate
candidate as if the current one had not existed. Then, method redispatch
is performed with respect to the original object's class and sees the
entire inheritance tree instead of cutting off all other branches below
SUPER
. Effectively, this offloads the responsibility of redispatching
from each specific class onto the runtime method dispatch mechanism.
Grand11══Grand12══╗ Grand21══Grand22
║ │ ║ ║ │
╟────────┘ ║ ╟────────┘
║ ║ ║
Parent1 ╚═══Parent2
║ │
╟─────────────────────┘
║
Derived
Concretely, when calling dump_info
on an object blessed into the Derived
class, there is an array of possible candidates. They are the methods of the
same name of Derived
, Parent1
, Grand11
, Grand12
, Parent2
, Grand21
and Grand22
, in order of relevance. Given this array, each dump_info
implementation just has to redispatch to the single next method in line.
It is on the runtime to keep enough data around to continue the dispatch
chain.
Notably, this mechanism can also provide an implementation of RFC 8,
which is about the special method AUTOLOAD
. AUTOLOAD
is called as a
fallback when some method name could not be resolved. Redispatching via
NEXT
can be used to decline to autoload a method in the current class
and leave that task to another AUTOLOAD
in the inheritance tree.
While the status of RFC 190 is "frozen", hence accepted, this feature looks
different in Raku today. There is no NEXT
and even SUPER
is gone.
Instead we have three types of redispatch keywords:
callsame
,callwith
: calls the next candidate for the method, either using the same arguments or with the other, given ones.nextsame
,nextwith
: the same as as thecall*
keywords, except they do not return control to the current method.samewith
: calls the same candidate again with different arguments.
callsame
and callwith
implement the process that NEXT
would have,
but in the wider context of all dispatch-related goodies that Raku got.
They work in all places that have a linearized hierarchy of "callable
candidates". This includes redispatch of methods along the inheritance
tree, it naturally includes candidates for multi methods and subs,
it includes FALLBACK
(erstwhile AUTOLOAD
) and wrapped routines.
Another speciality is in case you ever find you have to redispatch from the
current method but do so in another context, for example inside a Promise,
then the nextcallee
keyword can be used to obtain a Callable which can
continue the dispatch process from anywhere.
We change NEXT::
→ callsame
and after some localizations, our
dump_info
method looks like this:
method dump-info {
callsame; # first dump next
put $!derived-info; # then ourselves
}
In summary, being able to redispatch to a parent class's method is useful. In a situation with multiple inheritance and multi subs, the fixation on the "parent class" is less helpful and is replaced by "less specific method". Conway's proposal to redispatch to the next less specific method made it into Raku and its usefulness is amplified way beyond the RFC by other Raku features and the careful design connecting them.
Curiously, a NEXT module was first shipped as a core module with Perl v5.7.3, released in 2002, written by... Damian Conway.
For the particular pattern used as an example in the RFC, where each class
in the inheritance tree independently throws in its own bit, there is
another way in Raku to accomplish the same as redispatch. This is the
method call operator .*
(or its greedy variant .+
).
Consider
# Parent and grandparent classes look the same...
class Derived is Parent1 is Parent2 {
sub dump-info { put "Derived" }
}
Each dump-info
method is only concerned with its own info and does not
redispatch. The .*
methodop walks the candidate chain and calls all
of them, returning a list of return values (although in this case we care
only about the side effect of printing to screen):
Derived.new.*dump-info
# Derived
# Parent1
# Grand11
# Grand12
# Parent2
# Grand21
# Grand22
The methods are called from most to least relevant, in pre-order of the
inheritance tree. This way we can keep our dump-info
methods oblivious
to redispatch, whereas explicitly redispatching using callsame
and co.
would allow us to choose between pre- and post-order.