Raku Dispatch 解密

But Here's My Dispatch, So callwith Maybe

Raku 的一个很好的特性是 multi-dispatch, 即多重分派。它允许你在函数, 方法或 Grammar token 中使用相同的名字并让它们所处理的数据的类型来决定执行哪一个。下面是一个 factorial postfix 操作符, 用两个 multies 来实现:

multi postfix:<!> (0) { 1 }
multi postfix:<!> (UInt \n) { n × samewith n − 1 }

say 5!

# OUTPUT: 120

虽然 multi-dispatch 的主题很明显并且它还有一些说明文档, 我今天想讲的是 7 个特殊的子例程, 让你能够行走在 dispatch 迷宫中。它们是 nextwith, nextsame, samewith, callwith, callsame, nextcalleelastcall.

设立实验室

Multies 从最窄到最广的候选者进行排序,并且当一个 multi 被调用时,绑定器尝试找到一个匹配并调用第一个匹配的候选者。 有时,您可能希望调用或简单地移动到链中的下一个匹配候选者,可选地使用不同的参数。 为了观察这些操作的效果,我们将使用以下设置:

class Wide             { }
class Middle is Wide   { }
class Narrow is Middle { }

multi foo (Narrow $v) { say 'Narrow ', $v; 'from Narrow' }
multi foo (Middle $v) { say 'Middle ', $v; 'from Middle' }
multi foo (Wide   $v) { say 'Wide   ', $v; 'from Wide'   }

foo Narrow; # OUTPUT: Narrow (Narrow)
foo Middle; # OUTPUT: Middle (Middle)
foo Wide;   # OUTPUT: Wide   (Wide)

我们有三个类,每个类都继承自前一个类,所以我们的 Narrow 类 可以适应 MiddleWide multi 候选者; Middle 也可以适应 Wide,但不能适应 Narrow; 而 Narrow 既不适用于 Middle,也不适用于 Narrow。请记住,Raku 中的所有类也都是 Any 类型,因此也适用于任何接受 Any 的候选者。

对于我们的 Callables,我们在 foo 子例程上使用三个 multi 候选者:每个类一个。在类的主体中,我们打印什么我们所调用的 multi 的类型,以及作为参数传递的值。对于它们的返回值,我们只使用一个字符串来告诉我们返回值来自哪个 multi;我们稍后会使用这些。

最后,我们使用三个类型的对象与我们的自定义类进行三次调用。从输出来看,我们可以看到三个候选者中的每一个候选者都按预期被调用了。

这一切都是平淡而无聊的。但是,我们可以调味!在这些例程的内部,我们可以随时调用 nextsamesamewithcallwithcallame 来调用具有相同或不同参数的另一个候选者。但首先,我们来弄清楚它们都做什么?

主题

我们要测试的前 5 个例程的命名遵循如下约定:

  • call____调用链中的下一个匹配的候选者并回到此处
  • next____ - 跳转到链中的下一个匹配的候选者并且不回到此处
  • ____same - 使用 同一个参数就像用于当前候选者一样
  • ____with - 使用提供的这些新参数进行操作
  • samewith - 从头开始一个同样的调用, 遵循一个新的分派链, 使用这些新的参数并且回到此处

samesame 不是什么稀奇的东西, 这种情况最好由常规循环代替。主要的外卖是“call”,就是说你调用候选者并回到原地, 使用它的返回值或做更多的事情; “next”意味着前往下一个候选者,并使用其返回值作为当前候选者的返回值; 然而末尾的 samewith 仅仅控制你是否想要使用同样的参数就像你在当前候选者中使用的那样, 或者提供一个新的集合。

让我们来玩玩这些东西吧!

It’s all called the same…

我们尝试的第一个例程是 callsame。它使用和当前候选者同样的参数调用下一个匹配的候选者并返回该候选者的返回值。

让我们来修改下我们的 Middle 候选者以调用 callsame 然后打印出它的返回值:

calss Wide             {}
calss Middle is Wide   {}
class Narrow is Middle {}

multi foo(Narrow $v) { say 'Narrow', $v, 'from Narrow' }
multi foo(Middle $v) {
    say 'Middle', $v;

    my $result = callsame;
    say "We're back! The Return value is $result";

    'from Middle'
}

multi foo(Wide $v) { say 'Wide ', $v; 'from Wide' }

foo Middle;

# OUTPUT:
# Middle (Middle)
# Wide   (Middle)
# We're back! The return value is from Wide

现在可以看到我们的单个 foo 调用导致了两次调用。第一次到了 Middle, 因为它是我们给 foo 调用的类型对象。第二次到了 Wide, 因为它是下一个能接收 Middle 类型的候选者; 在输出中我们看到 Wide 被调用时仍旧使用了我们原来的 Middle 类型对象。最后, 我们回到了我们的 Middle 候选者中, 并把 Wide 候选者的返回值设置给 $result 变量。

目前为止非常清晰, 让我们试试修改下参数!

Have you tried to call them with…

正如我们所知, __with 变体允许我们使用不同的参数。我会使用和之前的例子相同的代码, 区别就是现在我会执行 callwith, 并使用 Narrow 类型对象作为新的参数:

class Wide             {}
class Middle is Wide   {}
class Narrow is Middle {}
multi foo(Narrow $v) { say 'Narrow', $v; 'from Narrow' }
multi foo(Middle $v) {
    say 'Middle', $v;

    my $result = callwith Narrow;
    say "We're back! The return value is $result";

    'from Middle'
}
multi foo(Wide $v) { say 'Wide ', $v; 'from Wide' }

foo Middle;

# OUTPUT:
# Middle (Middle)
# Wide   (Narrow)
# We're back! The return value is from Wide

输出的第一部分很清楚: 我们仍旧使用 Middle 调用 foo, 并且先击中 Middle 候选者。然而, 下一行输出有点古怪。我们在 callwith 中使用了 Narrow 参数, 所以究竟 Wide 候选者是怎么得到调用的而不是调用 Narrow 候选者呢?

原因是 call____next____ 例程使用与原始调用相同的调度链。由于 Narrow 候选者比 Middle 候选者更窄, 所以被拒绝, 在当前的链条中不会被考虑。下一个 callwith 将会调用的候选者将是下一个匹配 Middle 的候选者 – 而不是笔误:Middle 是我们用于发起调度的参数, 因此下一个候选者将是仍然可以接受该原始调用的参数的候选者。一旦发现下一个候选者,传递给 callwith 的新参数就会绑定到它身上, 这是你的工作, 以确保它们可以。

让我们在实战中看一个更复杂的例子。

Kicking It Up a Notch

我们会使用更多的 multies 和类型来扩展我们原来的例子:

class Wide             {}
class Middle is Wide   {}
class Narrow is Middle {}

subset Prime where .?is-prime;
subset NotPrime where not .?is-prime;

multi foo(Narrow   $v) { say 'Narrow   ', $v; 'from Narrow'  }
multi foo(Middle   $v) { say 'Middle   ', $v; 'from Middle'  }
multi foo(Wide     $v) { say 'Wide     ', $v; 'from Wide'    }
multi foo(Prime    $v) { say 'Prime    ', $v; 'from Prime'   }
multi foo(NotPrime $v) { say 'Non-Prime', $v; 'from Prime'   }

foo Narrow; # OUTPUT: Narrow    (Narrow)
foo Middle; # OUTPUT: Middle    (Middle)
foo Wide;   # OUTPUT: Wide      (Wide)
foo 42;     # OUTPUT: Non-Prime 42
foo 31337;  # OUTPUT: Prime     31337

我们原来的三个类都是 Any 类型并且我们还创建了两个 Any 的子集(subset): PrimeNotPrime。其中 Prime 在类型上匹配素数而 NotPrime 在类型上匹配不是素数的数字或者匹配不含 .is-prime 方法的数字。因为我们的三个自定义类没有 .is-prime 方法, 所以它们都在类型上匹配 NotPrime

如果我们使用这种新格局重建之前的例子, 我们会得到和之前同样的结果:

class Wide             { }
class Middle is Wide   { }
class Narrow is Middle { }

subset    Prime where     .?is-prime;
subset NotPrime where not .?is-prime;

multi foo (Narrow   $v) { say 'Narrow    ', $v; 'from Narrow'   }
multi foo (Middle   $v) {
    say 'Middle    ', $v;

    my $result = callwith Narrow;
    say "We're back! The return value is $result";

    'from Middle'
}
multi foo (Wide     $v) { say 'Wide      ', $v; 'from Wide'     }
multi foo (Prime    $v) { say 'Prime     ', $v; 'from Prime'    }
multi foo (NotPrime $v) { say 'Non-Prime ', $v; 'from NotPrime' }

foo Middle;

# OUTPUT:
# Middle    (Middle)
# Wide      (Narrow)
# We're back! The return value is from Wide

原来的调用来到 Middle 候选者,它用 Narrow 类型对象 callwithWide 候选者中。

现在,我们来混合一下, 用 42 而不是 Narrow 来调用。我们确实有一个 NotPrime 候选者。 42 和原来的 Middle 都可以适应这个候选者。而且比原来的 Middle 候选者更宽, 并且它还处在调度链的上游。这可能会出错!

class Wide             { }
class Middle is Wide   { }
class Narrow is Middle { }

subset    Prime where     .?is-prime;
subset NotPrime where not .?is-prime;

multi foo (Narrow   $v) { say 'Narrow    ', $v; 'from Narrow'   }
multi foo (Middle   $v) {
    say 'Middle    ', $v;

    my $result = callwith 42;
    say "We're back! The return value is $result";

    'from Middle'
}
multi foo (Wide     $v) { say 'Wide      ', $v; 'from Wide'     }
multi foo (Prime    $v) { say 'Prime     ', $v; 'from Prime'    }
multi foo (NotPrime $v) { say 'Non-Prime ', $v; 'from NotPrime' }

foo Middle;

# OUTPUT:
# Middle    (Middle)
# Type check failed in binding to $v; expected Wide but got Int (42)
#   in sub foo at z2.p6 line 15
#   in sub foo at z2.p6 line 11
#   in block <unit> at z2.p6 line 19

哦,对,那个!我们给 callwith 的新参数不会影响调度, 所以尽管有一个候选者可以在调用链中进一步地处理我们的新参数, 但不是下一个候选者可以处理原始的 args, 这是 callwith 所调用的。结果是抛出异常, 由于我们的新参数绑定给下一个调用者时失败了。

谁是下一个?

这个能让我们在调度链上抓住下一个匹配的候选者的趁手的小子例程是 nextcallee。它不仅返回该候选者的 Callable,而且它将其从链上移出,以便下一个 next____call____ 会进入下一个候选者,下一个 nextcallee 将会移出并返回 next-next 候选者。所以, 让我们回到我们之前的例子, 作点弊!

class Wide             { }
class Middle is Wide   { }
class Narrow is Middle { }

subset    Prime where     .?is-prime;
subset NotPrime where not .?is-prime;

multi foo (Narrow   $v) { say 'Narrow    ', $v; 'from Narrow'   }
multi foo (Middle   $v) {
    say 'Middle    ', $v;

    nextcallee;
    my $result = callwith 42;
    say "We're back! The return value is $result";

    'from Middle'
}
multi foo (Wide     $v) { say 'Wide      ', $v; 'from Wide'     }
multi foo (Prime    $v) { say 'Prime     ', $v; 'from Prime'    }
multi foo (NotPrime $v) { say 'Non-Prime ', $v; 'from NotPrime' }

foo Middle;

# OUTPUT:
# Middle    (Middle)
# Non-Prime 42
# We're back! The return value is from NotPrime

啊哈!有用!代码几乎完全一样。唯一的变化是我们在调用 callwith 之前弹出了 nextcallee 调用。它移除了无法处理新的 42 参数的 Wide 候选者,所以,从输出可以看出,我们的调用进入了 NotPrime 候选者。

nexcallee 是精湛的,所以循环是一个挑战,因为它会使用循环或thunk的调度程序来查找被调用者。所以它最常见和最简单的用法是获得…下一个被调用者。如果你需要传递下一个被调用者, 你得先这样做:

multi pick-winner (Int \s) {
    my &nextone = nextcallee;
    Promise.in(π²).then: { nextone s }
}
multi pick-winner { say "Woot! $^w won" }

with pick-winner ^5 .pick -> \result {
    say "And the winner is...";
    await result;
}

# OUTPUT:
# And the winner is...
# Woot! 3 won

Int 候选者接收 nextcallee 然后启动一个会被并行执行的 Promise, 在超时后返回。我们不能在这儿使用 nextsame, 因为它会尝试 nextsame Promise 的代码块而非我们原来的子例程, 因此, nextcallee 节省了我们一整天时间。

我想我们已经到达了令人眼花缭乱的例子的巅峰,我可以听到观众的呼喊。 “这种东西有什么用呢,反正?只是制造了更多的 subs, 而不是混乱的 multis!“ 所以,让我们来看看更多的真实世界的例子,以及接下来遇见的 nextsamenextwith

Next one in line, please

我们来写一个类!一个能做事的类!

role Things {
    multi method do-it($place) {
        say "I am {<eating sleeping coding weeping>.pick} at $place"
    }
}

class Doer does Things {}

Doer.do-it: 'home' # 输出: I am coding at home

我们碰不到role,因为它是别人为我们而做的,保持它们本来的面目吧。但是,我们希望我们的 class 能做更多的事情!对于某些$place,我们希望它告诉我们一些更具体的东西。另外,如果这个地方是 my new place,我们想知道哪些地方是新的。以下是代码:

role Things {
    multi method do-it($place) {
        say "I am {<eating sleeping coding weeping>.pick} at $place"
    }
}

class Doer does Things {
    multi method do-it($place where .contains: 'home') {
        nextsame if $place.contains: 'large';
        nextwith "home with $<color> roof"
          if $place ~~ /$<color>=[red|green|blue]/;

        samewith method 'my new place';
    }

    multi method do-it('my new place') {
        nextwith 'red home'
    }
}

Doer.do-it: 'the bus';       # OUTPUT: I am eating at the bus
Doer.do-it: 'home';          # OUTPUT: I am sleeping at red home
Doer.do-it: 'large home';    # OUTPUT: I am sleeping at large home
Doer.do-it: 'red home';      # OUTPUT: I am eating at home with red roof
Doer.do-it: 'some new home'; # OUTPUT: I am eating at red home
Doer.do-it: 'my new place';  # OUTPUT: I am coding at red home

多了一点额外的代码,并且没有对提供该方法的 role 进行单个更改,我们添加了一大堆新功能。我们来看看我们使用的三个新的调度更改例程。

nextsamenextwith 函数与它们的 callsamecallwith 对应函数非常相似,除了它们不回到被调用的位置,它们的返回值将被用作当前例程的返回值。所以使用 nextsame 就像使用 return callsame,但是使用较少的键入,并且编译器能够进行更多的优化。

我们添加到类中的第一个 multi 方法被调度到 $place .contains 单词 home 的位置。在方法的主体中,如果 $place 也包含单词 large,我们使用 nextsame,即使用与当前方法相同的参数调用下一个匹配的候选者。这是这里的关键。我们不能全部再次调用我们的方法,因为它将进入无限循环重新分派给自己。然而,由于 nextsame 在同一个调度链中使用下一个候选者,所以没有循环发生,我们正好得到 role Things 中的候选者。

往下读代码中,nextwith 也随之而来。当 $place 提到三种颜色之一时,我们使用它。类似于 nextsame,它去下一个候选者,除了我们给它一个新的参数使用之外。

最后,我们来到 samewith。与以前使用的例程不同,这个家伙从头开始重新启动调度,所以它基本上就像再次调用该方法一样,除了你不必知道或使用它的实际名称。我们用一组新的参数来调用 samewith,从输出中我们可以看到新的调度路径通过我们添加到我们的类中的第二个 multi 路径,而不是继续从 role 的 multi,就像我们的 next____ 版本所做的那样。

Last Call!

包中的最后一个方法是 lastcall。调用它截断当前的调度链,以便 next____call____ 例程不会有其它任何地方可去。以下是一个例子:

multi foo (Int $_) {
    say "Int: $_";
    lastcall   when *.is-prime;
    nextsame   when *  %% 2;
    samewith 6 when * !%% 2;
}
multi foo (Any $x) { say "Any $x" }

foo 6; say '----';
foo 2; say '----';
foo 1;

# OUTPUT:
# Int: 6
# Any 6
# ----
# Int: 2
# ----
# Int: 1
# Int: 6
# Any 6

我们所有的对 foo 的调用都会首先进入 Int 候选者。当数字为 .is-prime,我们调用 lastcall; 当它是一个偶数,我们调用 nextsame; 而当这是一个奇数时,我们使用 6 作为参数来调用samewith

我们调用 foo 的第一个数字是6,这不是素数,所以 lastcall 从不被调用。这是一个偶数,所以我们调用 nextsame,从输出我们看到,我们已经达到Any候选者。

接下来,当我们使用 2 调用 foo 时,这是一个素数和偶数,我们调用 lastcallnextcall。然而,因为 lastcall 被调用并截断了调度链,所以 nextcall 从不会看到 Any 候选者,所以我们只有在输出中调用 Int 候选者。

在最后一个例子中,我们再次使用一个素数,所以 lastcall 再一次被调用。然而,数字是一个奇数,所以我们使用 samewith 而不是 nextwith。由于 samewith 从头开始重新调度,它不在乎我们用 lastcall 截断了以前的链。因此,输出显示我们经过 Int 候选者两次,第二次调用使用 nextsame 到达 Any 候选者,因为 samewith 上使用的数字不是素数,而是偶数。

Wrapping It Up

为了整理这篇文章,我们将考察另一个领域,我们学到的例程可以派上用场:包装东西!以下是代码:

use soft;

sub meower (\ッ, |c) {
    nextwith "🐱 says {ッ}", |c when ッ.gist.contains: 'meow';
    nextsame
}

&say.wrap: &meower;
say 'chirp';
say 'moo';
say 'meows!';

# OUTPUT:
# chirp
# moo
# 🐱 says meows!

我们使用 soft pragma 来禁止内联,所以我们的包装是理智的。我们有一个 meower sub 来修改第一个参数,如果它 .contains 单词 meow,传递其余的参数,如果有的话,通过一个 Capture(就是 |c 这个东西)未修改。所有剩下的调用都是使用 nextsame 原样传递的,我们 .wrap meowersay 程序上,从输出可以看出,一切都按照我们的意愿工作。

这是代码的主要功能:meower 不知道什么 sub 被包装了! 然而,它仍然可以在不发生问题的情况下设法调用它。

在这里,我们把它放在 put 例程上,而且它没有任何修改就可以正常工作:

use soft;

sub meower (\ッ, |c) {
    nextwith "🐱 says {ッ}", |c when ッ.gist.contains: 'meow';
    nextsame
}

&put.wrap: &meower;
put 'chirp';
put 'moo';
put 'meows!';

# OUTPUT:
# chirp
# moo
# 🐱 says meows!

Conclusion

今天,我们学到了一些强大的例程,让您可以从其他候选者中重用现有的多个候选者。 callsamecallwith 允许您调用当前调度链中的下一个匹配候选者,使用相同的参数或新集合。nextsamenextwith 完成同样的事情,而不返回到调用现场。

samewith 子让您从头开始重新启动调度链,而无需知道当前例程的名称。而 lastcallnextcallee 可以通过截断当前的调度链,或者移出和操作下一个被调用者来操作当前的调度链。

好好使用它们吧!

-Ofun

原文:

Raku-But-Heres-My-Dispatch-So-Callwith-Maybe

comments powered by Disqus