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 multi
s.
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 subset
s 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 multi
s 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
.
WithApi
in themulti
declarations are tied to the defined:api<1>
class that's at the package level. Thetwo
constant defines it's own locally scoped:api<2>
version ofWithApi
that whilst it has the same name doesn't match the token used in the multi dispatch. – Nicemulti sub get-api( WithApi $foo where .^api() == 2 )
needs to bemulti sub get-api( two $foo where .^api() == 2 )
. – Frigid