Perl 6 对象如何找到可能在父 class 或角色中的 multi 方法?

How does a Perl 6 object find a multi method that might be in a parent class or role?

考虑这个例子,其中一个 subclass 有一个没有签名的 multi 方法和一个带有 slurpy 参数的方法:

class Foo {
    multi method do-it { put "Default" }
    multi method do-it ( Int $n ) { put "Int method" }
    multi method do-it ( Str $s ) { put "Str method" }
    multi method do-it ( Rat $r ) { put "Rat method" }
    }

class Bar is Foo {
    multi method do-it { put "Bar method" }
    multi method do-it (*@a) { put "Bar slurpy method" }
    }

Foo.new.do-it: 1;
Foo.new.do-it: 'Perl 6';
Foo.new.do-it: <1/137>;
Foo.new.do-it;

put '-' x 10;

Bar.new.do-it: 1;
Bar.new.do-it: 'Perl 6';
Bar.new.do-it: <1/137>;
Bar.new.do-it: 5+3i;
Bar.new.do-it;

方法查找的结构是怎样的?我正在寻找更多的方式来解释它,特别是不抱怨它。

Int method
Str method
Rat method
Default
----------
Int method
Str method
Rat method
Bar slurpy method
Bar method

例如,Bardo-it1 调用。一些有理智的人可能会认为它首先在 Bar 中寻找匹配的签名,而 slurpy 永远不会让任何东西通过它。然而,该调用在继承链中找到了正确的 multi。

Bar是否已经知道所有签名?它是在搜索还是在编写时所有这些东西都已经解决了?

而且,有没有办法找出在 运行 时间 class 提供的方法?也许有一些关于 HOW 的电话?这将是一个方便的调试工具,当我有一个 multi 我错误地指定并且正在其他地方处理时。

Rakudo Perl 6 方法查找过程由 Metamodel::MROBasedMethodDispatch role by default. See Rakudo's /src/Perl6/Metamodel/MROBasedMethodDispatch.nqp 为相应的源代码完成。

(反过来,默认情况下,源代码使用 role Metamodel::C3MRO, which implements C3 method resolution order. See Rakudo's /src/Perl6/Metamodel/C3MRO.nqp。)

.^find_method returns 一种基于短名称(不带参数)的匹配方法。每当短名称对应于多个方法时,此返回的方法是 proto.

在原型上调用 .candidates returns 与原型匹配的 Method objects 列表。 (在非原型方法上调用 .candidates 只是 returns 与单元素列表中唯一元素相同的方法。)

for Bar.^find_method('do-it').candidates -> $method {
    $method.signature.say;
}

给出:

(Foo $: *%_)
(Foo $: Int $n, *%_)
(Foo $: Str $s, *%_)
(Foo $: Rat $r, *%_)
(Bar $: *%_)
(Bar $: *@a, *%_)

Bar.new.do-it: 5+3i; 调用将 Bar 作为 self 加上 5+3i 位置参数传递。候选列表中最接近这些参数(又名 )的签名是 (Bar $: *@a, *%_)。因此调用具有该签名的例程。

Bar.new.do-it; 调用将 Bar 作为 self 传递,仅此而已。 (Bar $: *%_) 签名比 (Bar $: *@a, *%_) 更接近(更窄)匹配。同样,调用具有最接近(最窄)签名的例程。

要牢记多重分派的关键是它发生在 子或方法解析发生之后。所以所有的多重分派实际上是一个两步过程。这两个步骤也是相互独立的。

写类似这样的东西时:

multi sub foo($x) { }
multi sub foo($x, $y) { }

编译器会生成一个:

proto sub foo(|) {*}

也就是说,除非你自己写了一个proto sub。 proto 是实际安装到 lexpad 中的内容; multi sub 不会直接安装到 lexpad 中,而是安装到 proto.

的候选列表中

所以,当调用一个multi sub时,过程是:

  1. 使用词法查找找到要调用的子项,解析为 proto
  2. 调用 proto,选择最佳 multi 候选并调用它

当嵌套作用域中有 multi 个候选者时,来自外部作用域的 proto 将被克隆并安装到内部作用域中,并将候选者添加到克隆中。

多方法会发生非常相似的过程,除了:

  • 多个方法只是存储在待办事项列表中,直到 class、角色或语法
  • 结束 }
  • proto 可能由角色或 class 提供,因此与 multi 候选人组成角色只会将他们也添加到待办事项列表中
  • 最后,如果有 multi 方法没有 proto,但是父 class 有这样的 proto,那将被克隆;否则将生成一个空 proto

意味着对多方法的调用是:

  1. 使用通常的方法调度算法查找方法(仅使用 C3 方法解析顺序搜索 classes),解析为 proto
  2. 调用 proto,选择最佳 multi 候选并调用它

多子和多方法使用完全相同的排序和选择算法。就多重分派算法而言,调用者只是第一个参数。此外,Perl 6 的多重分派算法并不比后面的参数更重视前面的参数,因此:

class A { }
class B is A { }
multi sub f(A, B) { }
multi sub f(B, A) { }

会被认为是绑定的,如果用 f(B, B) 调用,会给出一个不明确的调度错误,所以定义:

class B { ... }
class A {
    multi method m(B) { }
}
class B is A {
    multi method m(A) { }
}

然后调用 B.m(B),因为 multi-dipsatcher 再次只看到类型元组 (A, B)(B, A).

Multiple dispatch 本身就涉及到狭义的概念。如果 C1 的至少一个参数是比 C2 中相同位置的参数更窄的类型,并且所有其他参数都绑定(即不是更窄,也不是更宽),则候选 C1 比 C2 更窄。如果反之为真,则它更宽。否则,它是绑定的。一些例子:

(Int) is narrower than (Any)
(Int) is tied with (Num)
(Int) is tied with (Int)
(Int, Int) is narrower than (Any, Any)
(Any, Int) is narrower than (Any, Any)
(Int, Any) is narrower than (Any, Any)
(Int, Int) is narrower than (Int, Any)
(Int, Int) is narrower than (Any, Int)
(Int, Any) is tied with (Any, Int)
(Int, Int) is tied with (Int, Int)

multi-dipsatcher 构建了候选的有向图,其中只要 C1 比 C2 窄,就会有从 C1 到 C2 的边。然后它找到所有没有传入边缘的候选者,并删除它们。这些是第一批候选人。删除将产生一组没有传入边的新候选集,然后将其删除并成为第二组候选集。这一直持续到所有候选都从图中取出,或者如果我们达到无法从图中取出任何东西的状态(一种非常罕见的情况,但这将作为循环报告给程序员)。这个过程发生一次,而不是每次派遣,它会产生一组候选人。 (是的,这只是一种拓扑排序,但分组细节对接下来的事情很重要。)

来电时,系统会按顺序搜索组以找到匹配的候选人。如果同一组中的两个候选人匹配,并且没有决胜局(命名参数,where 子句或隐含的 subset 类型的 where 子句,解包,或 is default ) 然后将报告一个不明确的调度。如果搜索所有组都没有结果,则派发失败。

关于 arity(必需参数胜过可选参数或 slurpy)和 is rw(它比没有 is rw 的其他相等候选者更窄)也有一些狭窄的考虑。

一旦发现一组中的一名或多名候选人匹配,就会考虑决胜局。这些包括命名参数的存在、where 子句和解包,并在首场比赛获胜的基础上工作。

multi f($i where $i < 3) { } # C1
multi f($i where $i > 1) { } # C2
f(2) # C1 and C2 tied; C1 wins by textual ordering due to where

请注意,此文本排序仅适用于平局;就类型而言,源代码中候选项的顺序并不重要。 (命名参数也仅充当决胜局,有时令人惊讶。)

最后,我会注意到虽然多次分派的结果总是与我描述的两步过程相匹配,但实际上发生了大量的运行时优化。虽然所有查找最初都完全按照描述进行解析,但结果被放入调度缓存中,这提供了比搜索由拓扑排序提供的组更快的查找。这是以这样一种方式安装的,即可以完全绕过原型的调用,从而节省调用框架。如果 --profile,您可以看到这种行为的产物;与多个候选者相比,为任何基于类型的调度(没有决胜局)自动生成的 proto 将收到少量呼叫。如果您在 proto 中编写自定义逻辑,这当然不适用。

除此之外,如果您 运行 在 MoarVM 上,动态优化器可以更进一步。它可以使用收集和推断的类型信息来解决 method/sub 调度 多调度,将 2 步过程变成 0 步过程。小候选也可以内联到调用者中(同样,探查器可以告诉你内联已经发生),这可以说将多分派变成了 -1 步过程。 :-)