Skip to content

Instantly share code, notes, and snippets.

@taboege
Created August 13, 2020 10:12
Show Gist options
  • Save taboege/238fb7f23c7707a5a8279e099cb141a1 to your computer and use it in GitHub Desktop.
Save taboege/238fb7f23c7707a5a8279e099cb141a1 to your computer and use it in GitHub Desktop.
Raku 20th article about RFC 190

RFC 190, by Damian Conway: Objects : NEXT pseudoclass for method redispatch

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.

On method dispatch

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.

The NEXT pseudoclass

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.

The Raku implementation

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 the call* 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.


There is more than one way to dump info

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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment