As a follow up to this question about using different APIs in a single program, Liz Mattijsen suggested to use constants. Now here's a different use case: let's try to create a multi that differentiates by API version, like this:
class WithApi:ver<0.0.1>:auth<github:JJ>:api<1> {}
my constant two = my class WithApi:ver<0.0.1>:auth<github:JJ>:api<2> {}
multi sub get-api( WithApi $foo where .^api() == 1 ) {
return "That's version 1";
}
multi sub get-api( WithApi $foo where .^api() == 2 ) {
return "That's version deuce";
}
say get-api(WithApi.new);
say two.new.^api;
say get-api(two.new);
We use a constant for the second version, since both can't be together in a single symbol space. But this yields this error:
That's version 1
2
Cannot resolve caller get-api(WithApi.new); none of these signatures match:
(WithApi $foo where { ... })
(WithApi $foo where { ... })
in block <unit> at ./version-signature.p6 line 18
So say two.new.^api; returns the correct api version, the caller is get-api(WithApi.new), so $foo has the correct type and the correct API version, yet the multi is not called? Is there something I'm missing here?
TL;DR JJ's answer is a run-time where clause that calls a pair of methods on the argument of concern. Everyone else's answers do the same job, but using compile-time constructs that provide better checking and much better performance. This answer blends my take with Liz's and Brad's.
Key strengths and weaknesses of JJ's answer
In JJ's answer, all the logic is self-contained within a where clause. This is its sole strength relative to the solution in everyone else's answers; it adds no LoC at all.
JJ's solution comes with two significant weaknesses:
Checking and dispatch overhead for a where clause on a parameter is incurred at run-time1. This is costly, even if the predicate isn't. In JJ's solution the predicates are costly ones, making matters even worse. And to cap it all off, the overhead in the worse case when using multiple dispatch is the sum of all the where clauses used in all the multis.
In the code where .^api() == 1 && .^name eq "WithApi", 42 of the 43 characters are duplicated for each multi variant. In contrast a non-where clause type constraint is much shorter and would not bury the difference. Of course, JJ could declare subsets to have a similar effect, but then that would eliminate the sole strength of their solution without fixing its most significant weakness.
Attaching compile-time metadata; using it in multiple dispatch
Before getting to JJ's problem in particular, here are a couple variations on the general technique:
role Fruit {} # Declare metadata `Fruit`
my $vegetable-A = 'cabbage';
my $vegetable-B = 'tomato' does Fruit; # Attach metadata to a value
multi pick (Fruit $produce) { $produce } # Dispatch based on metadata
say pick $vegetable-B; # tomato
Same again, but parameterized:
enum Field < Math English > ;
role Teacher[Field] {} # Declare parameterizable metadata `Teacher`
my $Ms-England = 'Ms England';
my $Mr-Matthews = 'Mr Matthews';
$Ms-England does Teacher[Math];
$Mr-Matthews does Teacher[English];
multi field (Teacher[Math]) { Math }
multi field (Teacher[English]) { English }
say field $Mr-Matthews; # English
I used a role to serve as the metadata, but that's incidental. The point was to have metadata that can be attached at compile-time, and which has a type name so dispatch resolution candidates can be established at compile-time.
A compile-time metadata version of JJ's run-time answer
The solution is to declare metadata and attach it to JJ's classes as appropriate.
A variation on Brad's solution:
class WithApi1 {}
class WithApi2 {}
constant one = anon class WithApi:ver<0.0.1>:auth<github:JJ>:api<1> is WithApi1 {}
constant two = anon class WithApi:ver<0.0.1>:auth<github:JJ>:api<2> is WithApi2 {}
constant three = anon class WithApi:ver<0.0.2>:api<1> is WithApi1 {}
multi sub get-api( WithApi1 $foo ) { "That's api 1" }
multi sub get-api( WithApi2 $foo ) { "That's api deuce" }
say get-api(one.new); # That's api 1
say get-api(two.new); # That's api deuce
say get-api(three.new); # That's api 1
An alternative is to write a single parameterizable metadata item:
role Api[Version $] {}
constant one = anon class WithApi:ver<0.0.1>:auth<github:JJ>:api<1> does Api[v1] {}
constant two = anon class WithApi:ver<0.0.1>:auth<github:JJ>:api<2> does Api[v2] {}
constant three = anon class WithApi:ver<0.0.2>:api<v1> does Api[v1] {}
multi sub get-api( Api[v1] $foo ) { "That's api 1" }
multi sub get-api( Api[v2] $foo ) { "That's api deuce" }
say get-api(one.new); # That's api 1
say get-api(two.new); # That's api deuce
say get-api(three.new); # That's api 1
Matching ranges of versions
In a comment below JJ wrote:
If you use where clauses you can have multis that dispatch on versions up to a number (so no need to create one for every version)
The role solution covered in this answer can also dispatch on version ranges by adding another role:
role Api[Range $ where { .min & .max ~~ Version }] {}
...
multi sub get-api( Api[v1..v3] $foo ) { "That's api 1 thru 3" }
#multi sub get-api( Api[v2] $foo ) { "That's api deuce" }
This displays That's api 1 thru 3 for all three calls. If the second multi is uncommented it takes precedence for v2 calls.
Note that the get-api routine dispatch is still checked and candidate resolved at compile-time despite the fact the role signature includes a where clause. This is because the run-time for running the role's where clause is during compilation of the get-api routine; when the get-api routine is called the role's where clause is no longer relevant.
Footnotes
1 In Multiple Constraints, Larry wrote:
For 6.0.0 ... any structure type information inferable from the where clause will be ignored [at compile-time]
But for the future he conjectured:
my enum Day ['Sun','Mon','Tue','Wed','Thu','Fri','Sat'];
Int $n where 1 <= * <= 5 # Int plus dynamic where
Day $n where 1 <= * <= 5 # 1..5
The first where is considered dynamic not because of the nature of the comparisons but because Int is not finitely enumerable. [The second constraint] ... can calculate the set membership at compile time because it is based on the Day enum, and hence [the constraint, including the where clause] is considered static despite the use of a where.
The solution is really simple: also alias the "1" version:
my constant one = my class WithApi:ver<0.0.1>:auth<github:JJ>:api<1> {}
my constant two = my class WithApi:ver<0.0.1>:auth<github:JJ>:api<2> {}
multi sub get-api(one $foo) {
return "That's version 1";
}
multi sub get-api(two $foo) {
return "That's version deuce";
}
say one.new.^api; # 1
say get-api(one.new); # That's version 1
say two.new.^api; # 2
say get-api(two.new); # That's version deuce
And that also allows you to get rid of the where clause in the signatures.
Mind you, you won't be able to distinguish them by their given name:
say one.^name; # WithApi
say two.^name; # WithApi
If you want to be able to do that, you will have to set the name of the meta-object associated with the class:
my constant one = my class WithApi:ver<0.0.1>:auth<github:JJ>:api<1> {}
BEGIN one.^set_name("one");
my constant two = my class WithApi:ver<0.0.1>:auth<github:JJ>:api<2> {}
BEGIN two.^set_name("two");
Then you will be able to distinguish by name:
say one.^name; # one
say two.^name; # two
Only one thing can be in a given namespace.
I assume the whole reason you are putting the second declaration into a constant and declaring it with my is that it was giving you a redeclaration error.
The thing is, that it should still be giving you a redeclaration error.
Your code shouldn't even compile.
You should have to declare the second one with anon instead.
class WithApi:ver<0.0.1>:auth<github:JJ>:api<1> {}
constant two = anon class WithApi:ver<0.0.1>:auth<github:JJ>:api<2> {}
It would then be obvious why what you are trying to do doesn't work.
The second declaration is never installed into the namespace in the first place.
So when you use it in the second multi sub it is declaring that its argument is the same type as the first class.
(Even when you are using my in your code it isn't managing to install it into the namespace.)
You are assuming that the namespace is a flat namespace.
It isn't.
You can have a class that has one name, but is only ever accessible under another.
our constant Bar = anon class Foo {}
sub example ( Bar $foo ) {
say $foo.^name; # Foo
}
example( Bar );
Raku installs the class into the namespace for you as a convenience.
Otherwise there would be a lot of code that looked like:
our constant Baz = class Baz {}
You are trying to use the namespace while at the same time trying to subvert the namespace.
I don't know why you expect that to work.
A quick way to get your exact code to work as you wrote it, is to declare that the second class is a subclass of the first.
class WithApi:ver<0.0.1>:auth<github:JJ>:api<1> {}
constant two = anon class WithApi:ver<0.0.1>:auth<github:JJ>:api<2> is WithApi {}
# ^________^
Then when the second multi checks that its argument is of the first type, it still matches when you give it the second.
This isn't great.
There isn't really a built-in way to do exactly what you want.
You could try to create a new meta type that can create a new type that will act like both classes.
I personally would just alias them both to independent names.
constant one = anon class WithApi:ver<0.0.1>:auth<github:JJ>:api<1> {}
constant two = anon class WithApi:ver<0.0.1>:auth<github:JJ>:api<2> {}
If you are loading them from modules:
constant one = BEGIN {
# this is contained within this block
use WithApi:ver<0.0.1>:auth<github:JJ>:api<1>;
WithApi # return the class from the block
}
constant two = BEGIN {
use WithApi:ver<0.0.1>:auth<github:JJ>:api<2>;
WithApi
}
Elizabeth Mattijsen answer above game me a hint. Signatures match symbol, not symbol name. However, when you alias (using a constant) to a new name, you still keep the name. Let's use this to have an uniform multi call where the only thing that changes is the api version:
class WithApi:ver<0.0.1>:auth<github:JJ>:api<1> {}
my constant two = my class WithApi:ver<0.0.1>:auth<github:JJ>:api<2> {}
my constant two = my class WithApi:ver<0.0.1>:auth<github:JJ>:api<2> {}
my constant three = my class WithApi:ver<0.0.2>:api<1> {}
multi sub get-api( $foo where .^api() == 1 && .^name eq "WithApi" ) {
return "That's version 1";
}
multi sub get-api( $foo where .^api() == 2 && .^name eq "WithApi") {
return "That's version deuce";
}
say get-api(WithApi.new); # That's version 1
say get-api(two.new); # That's version deuce
say get-api(three.new); # # That's version 1
Again following Elizabeth's answer in the previous question, constants are used for the new versions to avoid namespace clashes, but the multis will be selected solely based in api version in a relatively type-safe way, without needing to use the aliased symbols in the signature. Even if you invent a new constant to alias WithApi with any metadata, the multi will still be selected based on api version (which is what I was looking for).
Related
Can someone help me understand the following behavior?
class Box {
has $.data;
multi method new($d) {
say 'here';
self.bless(data => $d);
}
}
# construct object with the custom new()
my $box = Box.new('hi');
say $box.data;
# construct object using default new()
my $box2 = Box.new(data => 'be');
say $box2.data;
This outputs:
here
hi
be
OK, perfect, exactly what is expected. However, change the code so the new method $d has a default value like this:
class Box {
has $.data;
multi method new($d = '') { # we give $d a default value now
say 'here';
self.bless(data => $d);
}
}
my $box = Box.new('hi');
say $box.data;
my $box2 = Box.new(data => 'be');
say $box2.data;
You now get this output:
here
hi
here # new method is getting called both times and $.data is not set
This is not what I expected. I figured I would get the same output as before. Can someone please explain why I don't get he same output?
UPDATE: I notice that if I change the new() signature to:
multi method new($d = '', *%_ ())
I can get things to work as expected. But I still don't understand exactly why it didn't work without *%_ () in the first place.
The two overloads of new under consideration here are yours and the one defined on the top-level type Mu. Namely,
multi method new($d)
multi method new(*%attrinit)
Or, written more explicitly,
multi method new(Box: $d?)
multi method new(Mu: *%attrinit)
But, we need to be even more explicit. Because, though it looks like only the latter should match Box.new(data => 'be'), the two are in fact both valid candidates. That's because, according to the documentation for Method,
Methods automatically capture extra named arguments into the special variable %_, where other types of Routine will throw at runtime. So
method x() {}
is actually equivalent to
method x(*%_) {}
and that applies to multi method as well. The rationale behind this is to allow methods to forward named arguments that they don't understand to their callers.
So, really, our two overloads are
multi method new(Box: $d?, *%_)
multi method new(Mu: *%attrinit)
So when we write Box.new(data => 'be'), we have two candidates which are valid, and the first one has a more specific invocant (Box rather than Mu), so it gets called.
In the case without the default argument, the candidates look like
multi method new(Box: $d, *%_)
multi method new(Mu: *%attrinit)
so the first multi is only a candidate for invocation if there's one positional argument.
We can use the trick from this Stack Overflow answer to suppress this behavior.
multi method new($d = '', *% ()) {
say "here $d";
self.bless(data => $d);
}
The *% () (the space is important here) is actually a rather neat little trick. The linked answer explains it better than I can, but basically the *% part says "I accept any named arguments", and then the () is a sub-signature to match against, namely the empty signature. You can't stop a method from accepting named arguments, so this more or less reads as "my method accepts any named arguments, as long as the list of named arguments is equal to the empty list".
Interesting question and good Answers already.
However, I think that both depend a little too much on obscure aspects of raku, namely 'bless' and '*% ()' - not to say that these tricks don't have a place, but that the common case given (a positional with default) should not need you to reach for the power tools.
Here's my solution:
class Box {
has $.data = ''; # we give data a default value here
multi method new($data) {
samewith(:$data) # redespatch positional data as named data
}
}
If I have a Role R defined as:
role R { method answer { 42 } }
What is the difference (if any) between these two lines:
my $a = 'question' does R;
my $b = 'question' but R;
They appear very similar:
say $a.answer; # OUTPUT: «42»
say $b.answer; # OUTPUT: «42»
say $a.WHAT; # OUTPUT: «(Str+{R})»
say $b.WHAT; # OUTPUT: «(Str+{R})»
Is this a case of there being More Than One Way To Do It™, and these both mean the same thing? Or is there a subtle difference that I'm missing?
note:
I understand that does is both an operator and a trait and thus can be used when for compile-time mixins (e.g., class C does R {}) whereas but is only for runtime mixins. I also understand that but can be used with an object (e.g., my $c = 'question' but False) whereas does can only be used with a Role. I'm not asking about either of those differences; my only question is about whether there's a difference when both are used at runtime with a Role. I have read the documentation section on mixing in Role, but didn't see an answer.
Put simply:
does modifies an object in place (and should be used with caution with value types, see note below)
but returns a new object.
When created off of a literal, it's probably not as evident, but when used with another object, it's pretty clear I think:
role R { method answer { 42 } }
my $question = 'question';
my $but = $question but R;
my $does = $question does R;
say $question.WHAT; # (Str+{R})
say $but.WHAT; # (Str+{R})
say $does.WHAT; # (Str+{R})
say $question.WHERE; # 129371492039210
say $but.WHERE; # 913912490323923
say $does.WHERE; # 129371492039210 <-- same as $question's
Notice I cheated a bit and swapped the order of does and but. If I had preserved the order you had, the does would modify $question in place, applying the role, meaning that but would clone $question (with its role) and apply the role (again!):
my $does = $question does R;
my $but = $question but R;
say $does.WHAT; # (Str+{R})
say $but.WHAT; # (Str+{R}+{R})
This is because does as an operator is conceptually akin to ++ or +=, that is, designed to be used in a standalone context, for instance
my $foo = …;
given $bar {
when 'a' { $foo does A }
when 'b' { $foo does B }
when 'c' { $foo does B }
}
Using but is conceptually closer to using $foo + 1 — mostly meaningless unless assigned to or passed to something else.
A warning for does and value types
If you use does on a value type (strings, numbers mainly), there is an extremely high likelihood that you will cause unintended side effects. This is because value types (which, e.g., strings are) are supposed to be immutable and substitutable for one other. Note the following:
role Fooish { }
my $foo = 'foo';
$foo does Fooish;
say 'foo'.WHAT; # (Str+{Fooish})
This is a substitution that's happening at compile time (so it won't affect, e.g, 'foobar'.substr(0,3), that happens at runtime), but can cause some truly weird effects if you toss them in a loop:
role Fooish { }
my #a;
#a.push('foo' does Fooish) for ^10;
say #a[0].WHAT; # (Str+{Fooish}+{Fooish}+{Fooish}+{Fooish}+{Fooish}
+{Fooish}+{Fooish}+{Fooish}+{Fooish}+{Fooish})
Applying multiple rolls takes longer and longer the more you do it, so if you change that to ^100000, be ready to wait a while. OTOH, doing but gives you nice constant time and doesn't pollute the literal. This behavior seems, AFAICT, to be perfectly valid, but definitely something that can catch you unexpectedly.
I'd like to create some parametrized types for Raku; basically, I'd like to create some different classes whose main difference would be the range of values of one of its attributes; for instance, classes represent types of building, I'd like to have different classes for buildings with 3 or any other number of floors.
So this is the best I could think of:
subset Two-Tops of UInt where * <=2;
subset Three-Tops of UInt where * <=3;
role Zipi[ ::Capper ] {
has Capper $.floor;
}
class Capped-at-three does Zipi[Three-Tops] {}
my $capped = Capped-at-three.new( floor => 2 );
say $capped.raku;
This is clearly unpractical as soon as you need to take care of many different numbers of floors (not here in Granada, where they have at most 10, I think, but well... ). The problem here is basically you need to have the information for subsets at compile time, so unless you use macros (still experimental), there's no way you can use any kind of variable. So can you think of a practical way of defining this kind of curried roles for any value of the parameter?
Actually, unlike I said in my previous you can use conditions in where clauses without problem, you just need to encase them in braces:
role Zipi[$condition] {
has $.floor is rw where {$_ ~~ $condition}
method foo($x) { $!floor = $x }
}
class A does Zipi[2 < * < 5] {
method bar($x) { $.floor = $x }
}
#my $a = A.new( floor => 10); # error
my $a = A.new( floor => 4); # OK
#$a.foo(10); # error
$a.foo(3); # OK
#$a.bar(0); # error
$a.bar(4); # OK
#$a.floor = 9; # error
$a.floor = 3; # OK
That should cover all of the assignment types
I have very limited MOP chops, and the following seems ugly, but it works, and might be a step in the right direction.
What I've done:
Dynamically constructed an array of 10,000 subsets via the MOP.
Time shifted their construction to compile time via BEGIN.
Used an appropriate element from the array to parameterize the role.
my #max-floors-checkers;
BEGIN {
#max-floors-checkers = do for ^10_000 -> \floors {
Metamodel::SubsetHOW.new_type:
refinee => UInt,
refinement => { $^floors <= floors }
}
}
role BuildingCategory[ ::MaxFloorsCheck ] { has MaxFloorsCheck $.floors }
class Capped-at-three does BuildingCategory[ #max-floors-checkers[3] ] {}
my $capped3 = Capped-at-three.new( floors => 2 );
say $capped3.raku; # Capped-at-three.new(floors => 2
my $capped4 = Capped-at-three.new( floors => 4 ); # Type check failed
I tried using anonymous where clauses, but similarly to no avail, but I tracked down the issue: the where clause is apparently being ignored by the BUILD method . I'm not sure if it's because it has direct access (via $!floor) which bypasses the where clause, or if something else weird is going on (probably the latter, I general got Nil if I tried to use the paramaterized value in a where clause).
Nonetheless, this should work nicely, including giving a helpful error message:
role Zipi[$condition] {
has $.floor;
submethod BUILD(:$floor, |c) {
die "Invalid floor number."
unless $floor ~~ $condition;
$!floor = $floor;
}
}
You can see how it'd be easy to modify if you can assume floors are always 0 .. x, or x .. y and could provide an even more helpful error message.
A nanswer covering the case a reader knows Java but not Raku.
Collection<String> coll = new LinkedList<String>();
parametrized types for Raku
The linked Java example is:
The instantiation of a generic type with actual type arguments is called a parameterized type. Example (of a parameterized type):
Collection<String> coll = new LinkedList<String>();
A reasonable Raku analog is:
my Positional[Str] \coll = Array[Str].new;
The Positional type is a parameterizable role. A role specifies an interface and/or partial implementation of a type. I believe Raku's Positional is sufficiently analogous to Java's Collection that it serves for the purposes of this nanswer.
The Array type is a parameterizable class. It specifies a data structure that adheres to the Positional role. It isn't a linked list but it will suffice for the purposes of this nanswer.
I want to test the type of the first object in a signature. The following shows some ways I have found that work. But why does a smart match on a Type (2nd of the 3 tests below) not work?
Is there a better way than stringifying and testing for the string equivalent of the Type?
(Below is the use case I am working on)
raku -e "sub a( |c ) { say so |c[0].WHAT.raku ~~ /'Rat'/, so |c[0].WHAT ~~ Rat, so |c[0].^name ~~ /'Rat'/ };a(3/2);a(2)"
TrueFalseTrue
FalseFalseFalse
# OUTPUT:
#TrueFalseTrue
#FalseFalseFalse
I am writing a proto sub handle, and most of the subs have similar signatures, eg. multi sub handle( Pod $node, MyObj $p, Int $level --> Str)
So most of the multi subs do different things depending on what is in $node. However, how to handle cases when the handle is called with Nil or a plain string. I am thinking about something like
proto handle(|c) {
if |c[0].^name ~~ /'Str'/ { # code for string }
else { {*} }
}
A better way to introspect ...
In general, a better way to do anything in any programming language is to not introspect if you can avoid it.
In general, in Raku, you can avoid manual introspection. See the section Introspection toward the end of this answer for further discussion.
... a capture
The best tool for getting the functionality that introspection of a capture provides is to use a signature. That's their main purpose in life.
I want to test the type of the first object in a signature
Use signatures:
proto handle(|) {*}
multi handle( Pod $node ) { ... }
multi handle( Str $string ) { ... }
multi handle( Nil ) { ... }
The following shows some ways I have found that work.
While they do what you want, they are essentially ignoring all of Raku's signature features. They reduce the signature to just a binding to the capture as a single structure; and then use manual introspection of that capture in the routine's body.
There's almost always a simpler and better way to do such things using signatures.
why does [|c[0].WHAT ~~ Rat, with c[0] == 3/2] not work?
I'll simplify first, then end up with what your code is doing:
say 3/2 ~~ Rat; # True
say (3/2) ~~ Rat; # True
say (3/2).WHAT ~~ Rat; # True
say |((3/2).WHAT ~~ Rat); # True
say (|(3/2).WHAT) ~~ Rat; # False
say |(3/2).WHAT ~~ Rat; # False
The last case is because | has a higher precedence than ~~.
Is there a better way than stringifying and testing for the string equivalent of the Type?
OMG yes.
Use the types, Luke.
(And in your use case, do so using signatures.)
Introspection
Compared to code that manually introspects incoming data in the body of a routine, appropriate use of signatures will typically:
Read better;
Generate better low-level code;
Be partially or fully evaluated during the compile phase.
If a language and its compiler have addressed a use case by providing a particular feature, such as signatures, then using that feature instead of introspection will generally lead to the above three benefits.
Languages/compilers can be broken into four categories, namely those that:
Do not do or allow any introspection;
Allow the compiler to introspect, but not devs;
Allow both the compiler and devs to introspect, but aim to make it a last resort, at least for devs;
Enable and encourage devs to introspect.
Raku(do) are in the third category. In the context of this SO, signatures are the primary feature that all but eliminates any need for a dev to manually introspect.
You can simply smartmatch to a type:
raku -e "sub a( *#c ) { say #c[0] ~~ Rat };a(3/2);a(2)"
True
False
Also I am using here a slurpy and not a capture, which is another alternative. Any way, with a single argument you're probably better off using type captures
raku -e "sub a( ::T $ ) { say ::T ~~ Rat };a(3/2);a(2)"
True
False
You can pull stuff out of a Capture in the signature.
# ( |C ( ::Type $a, +#b ) )
proto handle( | ( ::Type, +# ) ) {
if Type ~~ Str {
…
} else {
{*}
}
}
Basically a ::Foo before a parameter (or instead of it) is similar to .WHAT on that parameter.
It also becomes usable as a type descriptor.
sub foo ( ::Type $a ) {
my Type $b = $a;
}
It is an incredibly bad idea to compare types based on their name.
my $a = anon class Foo { has $.a }
my $b = anon class Foo { has $.b }
say $a.WHAT =:= $b.WHAT; # False
say $a.^name eq $b.^name; # True
As far as Raku is concerned it is entirely a coincidence that two types happen to have the same name.
If you do use the names, your code will be confused about the reality of the situation.
Consider this example where a subclass has a multi method with no signature and one with a slurpy parameter:
class Foo {
multi method do-it { put "Default" }
multi method do-it ( Int $n ) { put "Int method" }
multi method do-it ( Str $s ) { put "Str method" }
multi method do-it ( Rat $r ) { put "Rat method" }
}
class Bar is Foo {
multi method do-it { put "Bar method" }
multi method do-it (*#a) { put "Bar slurpy method" }
}
Foo.new.do-it: 1;
Foo.new.do-it: 'Perl 6';
Foo.new.do-it: <1/137>;
Foo.new.do-it;
put '-' x 10;
Bar.new.do-it: 1;
Bar.new.do-it: 'Perl 6';
Bar.new.do-it: <1/137>;
Bar.new.do-it: 5+3i;
Bar.new.do-it;
How is the method lookup structured? I'm looking more for a way to explain it and specifically not complaining about it.
Int method
Str method
Rat method
Default
----------
Int method
Str method
Rat method
Bar slurpy method
Bar method
There's a call to Bar's do-it with 1 for instance. Some reasonable people might think that it looks for a matching signature in Bar first and that slurpy would never let anything get past it. Yet, the call finds the right multi in the inheritance chain.
Does Bar already know all the signatures? Does it search or is all of that stuff already resolved when it is composed?
And, is there a way to find out at run time which class provided the method? Maybe with some call into HOW? This would be a handy debugging tool when I have a multi I've incorrectly specified and is being handled elsewhere.
The key thing to keep in mind with multiple dispatch is that it happens after sub or method resolution has taken place. So all multiple dispatch is actually a two step process. The two steps are also independent of each other.
When writing something like:
multi sub foo($x) { }
multi sub foo($x, $y) { }
The compiler will generate a:
proto sub foo(|) {*}
That is, unless you wrote a proto sub by yourself. The proto is what actually gets installed into the lexpad; a multi sub is never installed directly into the lexpad, but instead installed into the candidates list of the proto.
So, when calling a multi sub, the process is:
Find the sub to call using a lexical lookup, which resolves to the proto
Call the proto, which picks the best multi candidate and calls it
When there are multi candidates in nested scopes, the proto from an outer scope will be cloned and installed into the inner scope, and the candidate added to the clone.
A very similar process happens with multi methods, except:
Multi methods are just stored up in a todo list until the closing } of the class, role, or grammar
A proto may be provided by a role or a class, so composing a role with multi candidates just adds them to the todo list also
Finally, if there is multi methods with no proto, but a parent class has such a proto, that will be cloned; otherwise an empty proto will be made
Meaning that a call to a multi-method is:
Find the method using the usual method dispatch algorithm (which just searches classes using the C3 Method Resolution Order), which resolves to the proto
Call the proto, which picks the best multi candidate and calls it
The exact same sorting and selection algorithm are used for both multi subs and multi methods. The invocant, so far as the multiple dispatch algorithm cares, is just the first parameter. Furthermore, the Perl 6 multiple dispatch algorithm does not weight earlier arguments more heavily than later ones, so just as:
class A { }
class B is A { }
multi sub f(A, B) { }
multi sub f(B, A) { }
Would be considered tied, and give an ambiguous dispatch error if called with f(B, B), so would defining:
class B { ... }
class A {
multi method m(B) { }
}
class B is A {
multi method m(A) { }
}
And then calling B.m(B), since again the multi-dipsatcher just sees the type tuples (A, B) and (B, A).
Multiple dispatch itself is concerned with the concept of narrowness. A candidate C1 is narrower than C2 if at least one argument of C1 is a narrower type than the argument in the same position in C2, and all other arguments are tied (that is, not narrower, not wider). If the inverse is true then it is wider. Otherwise, it is tied. Some examples:
(Int) is narrower than (Any)
(Int) is tied with (Num)
(Int) is tied with (Int)
(Int, Int) is narrower than (Any, Any)
(Any, Int) is narrower than (Any, Any)
(Int, Any) is narrower than (Any, Any)
(Int, Int) is narrower than (Int, Any)
(Int, Int) is narrower than (Any, Int)
(Int, Any) is tied with (Any, Int)
(Int, Int) is tied with (Int, Int)
The multi-dipsatcher builds a directed graph of the candidates, where there is an edge from C1 to C2 whenever C1 is narrower than C2. It then finds all of the candidates with no incoming edges, and removes them. These are the first group of candidates. The removal will produce a new set of candidates with no incoming edges, which are then removed and become the second group of candidates. This continues until all candidates are taken from the graph, or if we reach a state where we can take nothing from the graph (a very rare situation, but this will be reported to the programmer as a circularity). This process happens once, not per dispatch, and it produces a set of groups of candidates. (Yes, it is just a topological sort, but the grouping detail is significant for what comes next.)
When a call happens, the groups are searched in order for a matching candidate. If two candidates in the same group match, and there are no tie-breakers (named parameters, where clauses or implied where clauses from subset types, unpacks, or is default) then an ambiguous dispatch will be reported. If all the groups are searched without a result being found, then the dispatch fails.
There are also some narrowness considerations with regard to arity (required parameter beats optional parameter or slurpy) and is rw (it's narrower than an otherwise equal candidate without is rw).
Once one or more candidates in a group have been found to match, then tie-breakers are considered. These include the presence of named parameters, where clauses, and unpacks, and work on a first-match-wins basis.
multi f($i where $i < 3) { } # C1
multi f($i where $i > 1) { } # C2
f(2) # C1 and C2 tied; C1 wins by textual ordering due to where
Note that this textual ordering is only applicable to the tie-breaking; so far as types go, the order of candidates in the source code is not important. (That named parameters also act only as tie-breakers is sometimes a source of surprise.)
Finally, I'll note that while the results of a multiple dispatch will always match the 2-step process I've described, in reality a good amount of runtime optimization takes place. While all lookups are initially resolved exactly as described, the outcome is placed into a dispatch cache, which provides much faster lookups than searching the groups delivered by the topological sort could. This is installed in such a way that the call of the proto can be entirely bypassed, saving a callframe. You can see artifacts of this behavior if you --profile; the auto-generated proto for any type-based dispatch (without tie-breakers) will receive a tiny number of calls compared to the multi candidates. This doesn't apply if you write custom logic in your proto, of course.
Beyond that, if you're running on MoarVM, the dynamic optimizer can go a bunch further. It can use collected and inferred type information both to resolve the method/sub dispatch and the multi dispatch, turning a 2-step process into a 0-step process. Small candidates can be inlined into the caller also (again, the profiler can tell you that the inlining has happened), which arguably turns a multi-dispatch into a -1 step process. :-)
The Rakudo Perl 6 method look up process is done by the Metamodel::MROBasedMethodDispatch role by default. See Rakudo's /src/Perl6/Metamodel/MROBasedMethodDispatch.nqp for the corresponding source code.
(Which, in turn, by default, uses role Metamodel::C3MRO, which implements C3 method resolution order. See Rakudo's /src/Perl6/Metamodel/C3MRO.nqp for the source code.)
.^find_method returns a matching method based on a short name (without parameters). Whenever the short name corresponded to multiple methods this returned method is a proto.
Calling .candidates on a proto returns a list of Method objects that match the proto. (Calling .candidates on a non-proto method just returns that same method as the only element in a one element list.)
for Bar.^find_method('do-it').candidates -> $method {
$method.signature.say;
}
which gives:
(Foo $: *%_)
(Foo $: Int $n, *%_)
(Foo $: Str $s, *%_)
(Foo $: Rat $r, *%_)
(Bar $: *%_)
(Bar $: *#a, *%_)
The Bar.new.do-it: 5+3i; call passes a Bar as self plus the 5+3i positional argument. The signature from the candidate list that's closest to those arguments (aka "narrowest matching") is the (Bar $: *#a, *%_) one. So the routine with that signature gets called.
The Bar.new.do-it; call passes a Bar as self and nothing else. The (Bar $: *%_) signature is an even closer (narrower) match than (Bar $: *#a, *%_). Again, the routine with the closest (narrowest) signature gets called.