几天前,我在为一个问题的解决方案做原型时,写了一段代码,给我带来了巨大的喜悦。那是一种让你每次看到它运行时都会傻笑的代码,只因为这个解决方案看起来是多么的聪明和优雅,也因为证明了,与你的期望相反,它确实是可行的,而感到兴奋。
想分享这种兴奋,我把它分享到了网上。这个片段是这样的。
sub foo ( $a, $b ) { $a ~ $b }
my $wh;
$wh = &foo.wrap: {
LEAVE &foo.unwrap: $wh;
callwith( $^b, $^a );
}
say foo( 1, 2 ) for 1 .. 3;
这是非常密集的。
而且有点神秘
而且还很有趣
所以,让我们打破它。要做到这一点,我们将开始从结束,并努力回到开始的方式。
结果
当上面的代码得到执行时,它打印出以下几行。
21 12 12
这就是最后一行的结果。
say foo( 1, 2 ) for 1 .. 3;
其中打印了三次调用 foo( 1, 2 )
的结果。这一行对于 Perl 程序员来说是很熟悉的,因为 Raku 支持"语句修饰符",它从 Perl 中继承了这种修饰符:语句末尾的 for
修改了前面的语句,并对 1 .. 3
范围内的每个元素(实际上是一个 Range 对象)执行一次。
但是如果我们调用 foo( 1, 2 )
三次,那么为什么第一次调用它的时候输出的是 21,而其他时候输出的是 12 呢?
包装代码
那是因为之前的块:
my $wh;
$wh = &foo.wrap: {
LEAVE &foo.unwrap: $wh;
callwith( $^b, $^a );
}
这段代码是神奇的核心所在。
第一个重要的位子是对 wrap 的调用 这是在 foo
子程序对象上调用的(这就是为什么我们用 &
sigil 来引用它,否则我们可能会调用函数,而不是得到对它的引用)。
wrap
方法允许我们将一个代码块附加到 foo
上,这样调用该函数将代替执行该代码块。
重新分派
在我们用来封装 foo
的代码块中,我们使用 callwith 来调用封装的子程序。我们可以使用许多不同的选择,这取决于我们是否想要得到一个返回值,是否想要修改底层函数被调用的参数。
在这种情况下,callwith
允许我们修改参数,但它从不返回。而我们修改参数的方法像 callwith( $^b, $^a )
调用它。
但是这些变量是怎么来的呢?
自声明的定位符
它们是自我声明的位置参数。
当一个代码块没有定义一个显式的参数列表时,在它的范围内使用 ^
twigil 的任何变量( =
任何在符号后面有 ^
的变量)都会声明一个隐式的位置参数。块接收到的每个位置参数都会按照字母顺序被分配给这些变量。
那么在上面的例子中,调用 callwith( $^b, $^a )
将重新分派到我们正在封装的函数,并将第一个和第二个参数进行切换。由于原来的子程序将两个参数连在一起(使用 ~
操作符),这就解释了第一行输出:我们调用 foo( 1, 2 )
,这将重新分派到 foo( 2, 1 )
,结果是 21 行。
进步!
现在是真正神奇的一点。
自解封装器
在真正重新调度到原代码之前,我们还有一行。
LEAVE &foo.unwrap: $wh;
这一行使用 LEAVE 相位器来注册一个语句,在执行离开当前块时被调用。因此,在切换参数并重新派发到原子程序后,执行离开块,这条语句最终被执行。
当它被执行时,它会调用 foo
上的 unwrap 方法来删除包装代码(它通过传递存储在 $wh
变量中的包装句柄来实现)。
包裹
就是这样! 这段代码定义了一个 foo
子程序,它接受两个参数并将它们连接起来。然后将其封装到一个代码块中,在调用原始子程序之前交换参数,然后将其删除,就像它从未存在过一样。
这就解释了输出:我们第一次调用 foo( 1, 2 )
时,参数被交换了,我们打印出 21,但是在随后的调用中,已经没有包装代码了,所以我们得到了12这样的行。
真是口水直流啊! 但所有这些都只需要七行代码。Raku 真的可以包装一拳。
这很好,但是真正的用处是什么呢?
顺便说一下,这是我为了解决一个实际问题而做的原型:我有一个回调会被反复调用,我想在第一次执行回调的时候挂入,这样我就可以运行一个初步检查。但是一旦检查通过,每次都做就没有意义了。 Unwrap 来救场了。
顺便说一下,这利用了另一个有用的 Raku 特性:在 Raku 中,所有的东西都是一个对象。这就是为什么我们可以,例如,无缝调用子程序上的方法。 ↩
原文链接: https://pinguinorodriguez.cl/blog/self-unwrapping-routine/