别名为常量时无法解析签名

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

作为 , . Now here's a different use case: let's try to create a multi 的后续,通过 API 版本区分,如下所示:

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

我们对第二个版本使用常量,因为两者不能一起出现在一个符号中 space。但这会产生这个错误:

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

所以 say two.new.^api; returns 正确的 api 版本,调用者是 get-api(WithApi.new),所以 $foo 有正确的类型和正确的 API 版本,却没有调用 multi?我在这里遗漏了什么吗?

解决方案非常简单:也为“1”版本设置别名:

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

这还允许您删除签名中的 where 子句。

请注意,您无法通过他们的名字来区分他们:

say one.^name;  # WithApi
say two.^name;  # WithApi

如果您希望能够做到这一点,则必须设置与 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");

那你就可以通过名字来区分了:

say one.^name;  # one
say two.^name;  # two

伊丽莎白·马蒂森。签名匹配符号,而不是符号名称。但是,当您使用别名(使用常量)一个新名称时,您仍然保留该名称。让我们用它来进行统一的多重调用,其中唯一改变的是 api 版本:

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

再次遵循 Elizabeth 在 中的回答,常量用于新版本以避免命名空间冲突,但 multis 将仅基于 api 版本以相对类型安全的方式选择,无需在签名中使用别名符号。即使您发明了一个新常量来使用任何元数据对 WithApi 进行别名,mul​​ti 仍将根据 api 版本(这正是我一直在寻找的)进行选择。

TL;DR 是一个 运行-time where 子句,它在关注的参数上调用一对方法。其他人的答案做同样的工作,但使用编译时结构提供更好的检查和更好的性能。这个答案融合了我与 Liz 和 Brad 的看法。

JJ 回答的主要优点和缺点

在 JJ 的回答中,所有逻辑都包含在 where 子句中。相对于其他人的答案中的解决方案,这是它的唯一优势;它根本不添加 LoC。

JJ 的解决方案有两个明显的弱点:

  • 参数上 where 子句的检查和分派开销发生在 运行-time1。这是昂贵的,即使谓词不是。在 JJ 的解决方案中,谓词 昂贵的谓词,使事情变得更糟。总而言之,在使用 multiple dispatch 的最坏情况下,开销是 sum of all where 子句用于 all multis.

  • 在代码 where .^api() == 1 && .^name eq "WithApi" 中,每个 multi 变体的 43 个字符中有 42 个是重复的。相比之下,非 where 子句类型约束要短得多,不会掩盖差异。当然,JJ 可以声明 subsets 具有类似的效果,但这会消除他们解决方案的唯一优势,而不会修复其最重要的弱点。

附加编译时元数据;在多个分派中使用它

在具体解决 JJ 的问题之前,这里有一些通用技术的变体:

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

同样,但参数化:

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

我使用 role 作为元数据,但这是偶然的。关键是要有可以在编译时附加的元数据,它有一个类型名称,因此可以在编译时建立分派解决方案候选者。

JJ 运行-time answer

的编译时元数据版本

解决方案是声明元数据并根据需要将其附加到 JJ 的 类。

Brad 解决方案的变体:

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

另一种方法是编写单个可参数化元数据项:

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

版本匹配范围

JJ 在下面的评论中写道:

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)

此答案中涵盖的 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" }

这三个调用都显示 That's api 1 thru 3。如果第二个 multi 未注释,则优先调用 v2

请注意,尽管角色签名包含 where 子句,但仍会在编译时检查 get-api 例程分派和候选者解析。这是因为 运行 角色的 where 子句的 运行 时间是在编译 get-api 例程期间;当 get-api 例程被 调用时, 角色的 where 子句不再相关。

脚注

1Multiple Constraints 中,拉里写道:

For 6.0.0 ... any structure type information inferable from the where clause will be ignored [at compile-time]

但对于未来他推测:

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.

在给定的命名空间中只能有一个东西。

我假设你将第二个声明放入常量并用 my 声明它的全部原因是它给你一个重新声明错误。

问题是,它应该仍然给您一个重新声明错误。 您的代码甚至不应该编译。

您应该改为使用 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> {}

那么很明显为什么您尝试做的事情不起作用。 第二个声明从一开始就不会安装到命名空间中。 因此,当您在第二个 multi sub 中使用它时,它声明其参数与第一个 class.

的类型相同

(即使您在代码中使用 my,也无法将其安装到命名空间中。)


您假设命名空间是平面命名空间。
不是。

您可以有一个 class 有一个名字,但只能在另一个名字下访问。

our constant Bar = anon class Foo {}

sub example ( Bar $foo ) {
    say $foo.^name; # Foo
}
example( Bar );

为方便起见,Raku 将 class 安装到命名空间中。
否则会有很多代码看起来像:

our constant Baz = class Baz {}

您在尝试使用命名空间的同时又试图颠覆命名空间。 我不知道你为什么期望它起作用。


让您的确切代码像您编写的那样工作的快速方法是声明第二个 class 是第一个的子class。

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 {}
#                                                                   ^________^

然后当第二个multi检查它的参数是第一种类型时,当你给它第二个时它仍然匹配。

这不太好。


并没有真正的内置方法来完全按照您的意愿进行操作。

您可以尝试创建一个新的元类型,它可以创建一个新类型,其行为与 classes 一样。

我个人只是将它们分别命名为独立的名称。

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> {}

如果您从模块加载它们:

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
}