Signature can't be resolved when it's aliased to a constant

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

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.


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
}

Tags:

Raku

Dispatch