签名为常量时无法解析

jjm*_*elo 6 dispatch raku

作为关于在单个程序中使用不同 API 的这个问题的后续,Liz Mattijsen 建议使用常量。现在这里有一个不同的用例:让我们尝试创建一个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);
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

所以say two.new.^api;返回正确的 api 版本,调用者是get-api(WithApi.new),所以$foo有正确的类型和正确的 API 版本,但是 multi 没有被调用?有什么我在这里想念的吗?

Eli*_*sen 6

解决方案非常简单:还别名“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
Run Code Online (Sandbox Code Playgroud)

这也允许您摆脱where签名中的子句。

请注意,您将无法通过名字区分它们:

say one.^name;  # WithApi
say two.^name;  # WithApi
Run Code Online (Sandbox Code Playgroud)

如果您希望能够做到这一点,您必须设置与类关联的元对象的名称:

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");
Run Code Online (Sandbox Code Playgroud)

然后您将能够通过名称进行区分:

say one.^name;  # one
say two.^name;  # two
Run Code Online (Sandbox Code Playgroud)

  • 本质上,内部类型检查根本不使用名称。除了“where”子句和角色之外,它基本上是“sub istype(\a,\b) { return True if nqp::eqaddr(b,$_) for a.^mro; 假}`。 (2认同)

rai*_*iph 6

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

JJ 答案的主要优点和缺点

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

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

  • 检查和分派where参数子句的开销发生在运行时1。即使谓词不是,这也是代价高昂的。在JJ的解决方案谓词昂贵的,使事情变得更糟。更糟糕的是全部关闭,在使用时更坏情况的开销多个调度是总和所有where中使用的条款全部multi秒。

  • 在代码中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
Run Code Online (Sandbox Code Playgroud)

再次相同,但参数化:

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
Run Code Online (Sandbox Code Playgroud)

我使用 arole作为元数据,但这是偶然的。关键是要有可以在编译时附加的元数据,并且它有一个类型名称,以便可以在编译时建立分派解析候选者。

JJ 运行时答案的编译时元数据版本

解决方案是声明元数据并将其适当地附加到 JJ 的类。

布拉德解决方案的变体:

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
Run Code Online (Sandbox Code Playgroud)

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

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
Run Code Online (Sandbox Code Playgroud)

版本匹配范围

JJ在下面的评论中写道:

如果您使用where子句,您可以让multis 分派最多一个版本的版本(因此无需为每个版本创建一个)

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" }
Run Code Online (Sandbox Code Playgroud)

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

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

脚注

1多重约束中,拉里写道:

对于 6.0.0 ...从该where子句推断出的任何结构类型信息都将被忽略 [在编译时]

但对于未来,他猜想:

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
Run Code Online (Sandbox Code Playgroud)

第一个where被认为是动态的,不是因为比较的性质,而是因为Int不是有限可枚举的。[第二个约束] ...可以在编译时计算集合成员资格,因为它基于Day枚举,因此[约束,包括where子句]被认为是静态的,尽管使用了 a where