Search code examples
rolesrakumultiple-dispatch

Object, roles and multiple dispatch


I'm trying to use multiple dispatch to overload and use methods within composed classes. Here's the implementation:

role A {
    has $!b;

    submethod BUILD( :$!b ) {}

    multi method bar () {
    return $!b;
    }
}

class B does A {

    submethod BUILD( :$!b ) {}

    multi method bar() {
    return " * " ~ callsame ~ " * ";
    }
}

my $a = A.new( b => 33);
say $a.bar();
my $b = B.new( b => 33 );
say $b.bar();

This fails, however, with:

Calling callsame(Str) will never work with declared signature ()

(I really have no idea why callsame uses Str as a signature). Changing the method bar to use callwith:

multi method bar() {
    return " * " ~ callwith() ~ " * ";
}

Simply does not work:

Use of Nil in string context
  in method bar at multi.p6 line 18
 *  *

Is there any special way to work with call* within roles/classes?


Solution

  • The first issue is a matter of syntax. A listop function call parses an argument list after it, starting with a term, so this:

    return " * " ~ callsame ~ " * ";
    

    Groups like this:

    return " * " ~ callsame(~ " * ");
    

    And so you're calling the ~ prefix operator on " * ", which is where the Str argument it complains about comes from.

    Ultimately, however, the issue here is a misunderstanding of the semantics of role composition and/or deferral. Consider a non-multi case:

    role R { method m() { say 1; callsame() } }
    class B { method m() { say 2; callsame() } }
    class C is B does R { method m() { say 3; callsame(); } }
    C.m
    

    This outputs:

    3
    2
    

    Notice how 1 is never reached. This is because role composition is flattening: it's as if the code from the role were put into the class. When the class already has a method of that name, then it is taken in favor of the one in the role.

    If we put multi on each of them:

    role R { multi method m() { say 1; callsame() } }
    class B { multi method m() { say 2; callsame() } }
    class C is B does R { multi method m() { say 3; callsame(); } }
    C.m
    

    The behavior is preserved:

    3
    2
    

    Because the role composer accounts for the long name of the multi method - that is, accounting for the signature. Since they are the very same, then the one in the class wins. Were it to retain both, we'd end up with the initial call resulting in an ambiguous dispatch error!

    Deferral with nextsame, callsame, nextwith, and callwith all iterate through the possible things we could have dispatched to.

    In the case of a non-multi method, that is achieved by walking the MRO; since the method from the role was not composed, then it doesn't appear in any class in the MRO (nothing that only classes appear in the MRO, since roles are flattened away at composition time).

    In the case of a multi method, we instead walk the set of candidates that would have accepted the arguments of the initial dispatch. Again, since the identically long-named method in the class was chosen in favor of the role one at composition time, the one from the role simply isn't in consideration for the dispatch in the first place: it isn't in the candidate list of the proto, and so won't be deferred to.