Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

Tags:

dispatch

raku

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?

like image 686
jjmerelo Avatar asked Apr 06 '20 09:04

jjmerelo


Video Answer


2 Answers

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.

like image 196
raiph Avatar answered Sep 21 '22 06:09

raiph


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
like image 28
Elizabeth Mattijsen Avatar answered Sep 18 '22 06:09

Elizabeth Mattijsen