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

好久不见. 提交于 2020-05-14 19:07:32

问题


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?


回答1:


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.




回答2:


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



回答3:


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
}



回答4:


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).



来源:https://stackoverflow.com/questions/61057164/signature-cant-be-resolved-when-its-aliased-to-a-constant

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!