【问题标题】:How does a Perl 6 object find a multi method that might be in a parent class or role?Perl 6 对象如何找到可能在父类或角色中的多方法?
【发布时间】:2017-07-12 02:13:52
【问题描述】:

考虑这个例子,其中一个子类有一个没有签名的 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-it 调用了1。一些理性的人可能会认为它首先在Bar 中寻找匹配的签名,并且 slurpy 永远不会让任何东西通过它。然而,调用在继承链中找到了正确的 multi。

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

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

【问题讨论】:

  • 什么是 WAT?这很难用谷歌搜索。
  • 只有在您知道 WTF 相关的情况下,您才能使用 Google WTF 与 WAT。我更喜欢大多数人都能理解的简单英语。我仍然不太清楚你在说什么,因为多重继承不是这里问题的一部分。

标签: raku method-resolution-order multimethod


【解决方案1】:

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

当写类似的东西时:

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

编译器会生成一个:

proto sub foo(|) {*}

也就是说,除非你自己写了一个proto 子。 proto 是实际安装到 lexpad 中的; multi sub 永远不会直接安装到 lexpad 中,而是安装到 proto 的候选列表中。

所以,当调用multi子时,过程是:

  1. 使用词法查找查找要调用的子程序,它解析为proto
  2. 调用proto,它会挑选出最好的multi 候选人并调用它

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

一个非常相似的过程发生在多种方法上,除了:

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

表示对多方法的调用是:

  1. 使用通常的方法分派算法(仅使用 C3 方法解析顺序搜索类)查找方法,该算法解析为 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 类型、解包或is default 的隐含where 子句),则将报告模棱两可的调度.如果所有组都被搜索到没有结果,那么调度失败。

还有一些关于 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

请注意,此文本排序仅适用于平局;就类型而言,源代码中候选的顺序并不重要。 (命名参数也只能起到决胜局的作用,有时会让人感到意外。)

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

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

【讨论】:

    【解决方案2】:

    Rakudo Perl 6 方法查找过程默认由Metamodel::MROBasedMethodDispatch role 完成。对应源码见Rakudo's /src/Perl6/Metamodel/MROBasedMethodDispatch.nqp

    (默认情况下,它使用role Metamodel::C3MRO,实现C3 method resolution order。有关源代码,请参见Rakudo's /src/Perl6/Metamodel/C3MRO.nqp。)

    .^find_method 返回一个基于短名称(不带参数)的匹配方法。只要短名称对应于多个方法,此返回的方法就是proto

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

    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 位置参数传递。候选列表中最接近这些参数的签名(又名"narrowest matching")是(Bar $: *@a, *%_) 之一。因此,具有该签名的例程被调用。

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

    【讨论】:

    • @raiph 感谢您提供的信息,我编辑了我的答案。如果您能提供一些将 C3 与其他 MRO 混合的示例,那就更好了。
    • 嗯。 wikipeda page for C3 linearization 指出,C3“也可作为 Perl 5 核心中从 5.10.0 版本开始的替代、非默认 MRO”。我不是 100% 确定这意味着我认为这意味着什么。但是,如果 am 正确,那么如果 Inline::Perl5 在使用原始的现代前 Pumpking 版本作为 Perl 5 解释器时处理 P5 对象,那么这将是一个将 C3 mro 对象与非C3 的。我对此感到好奇,所以希望今晚能进行更多调查。我不认为 Lua 或 Ruby 使用 C3。但两者都有Inline::*s...
    • 这并不能真正帮助回答这个问题。在您的示例中,首先显示 Foo 的方法,但首先使用 Bar 中具有相同签名的方法。当您在没有多重继承的情况下提到 C3 时,我不确定为什么这很重要。元模型链接不讨论顺序。
    • @briandfoy 问题是How is the method lookup structured?,答案是针对那部分的。我认为candidates返回的顺序不是查找顺序,核心开发者可以更好地解释。
    • @raiph 我记得我已经阅读了此代码,但不记得它在核心中的位置。如果您能给我们提供源代码的链接就更好了。
    猜你喜欢
    • 2020-01-06
    • 2018-10-28
    • 1970-01-01
    • 2010-10-28
    • 1970-01-01
    • 2014-06-12
    • 1970-01-01
    • 1970-01-01
    • 2016-04-13
    相关资源
    最近更新 更多