问题描述
下面的 &greet 函数是纯函数,可以适当地用 is pure trait 标记。
sub greet(Str:D $name) { say "Hello,$name" }
my $user = get-from-db('USER');
greet($user);
然而,这不是:
sub greet {
my $name = get-from-db('USER');
say "Hello,$name"
}
greet($user);
这个呢?
sub greet(Str:D $name = get-from-db('USER')) { say "Hello,$name" }
greet();
从函数的“内部”来看,它看起来很纯粹——当参数绑定到相同的值时,它总是产生相同的输出,没有副作用。但是从函数的外部来看,它似乎是不纯的——当使用相同的参数调用两次时,它会产生不同的返回值。 Raku/Rakudo 的前景是什么?
解决方法
在实现参数的默认值时,语言可能至少采用两种策略:
- 将参数默认值视为编译器在遇到没有足够参数的调用时应在调用站点发出的内容,以便生成传递给被调用方的额外参数。这意味着可以支持参数的默认值,而无需在调用约定中对其进行任何明确支持。然而,这也要求您在编译时始终知道调用的去向(或者至少知道它足够准确以插入默认值,并且不能期望在子类的方法覆盖中使用不同的默认值并具有成功了)。
- 具有足够强大的调用约定,以便被调用者可以发现没有为参数传递值,然后计算默认值。
由于其动态特性,其中只有第二个对 Raku 真正有意义,这就是它所做的。
在执行策略 1 的语言中,将这样的函数标记为纯函数可以说是有意义的,因为计算每个调用点的默认寿命的代码,因此任何基于纯度的分析和转换可能已经必须处理评估默认值的代码,并且可以看到它不是纯值的来源。
在策略 2 和 Raku 下,我们应该将默认值理解为在其签名中具有默认值的块或例程的实现细节。因此,如果计算默认值的代码是不纯的,那么整个例程是不纯的,因此 is pure
trait 是不合适的。
更一般地说,如果对于给定的参数捕获,我们总是可以期望相同的返回值,则 is pure
特征是适用的。在给出的示例中,参数 capture \()
与此矛盾。
这里的另一种分解是使用 multi
子项而不是参数默认值,并且只用 is pure
标记一个候选。
当您说 sub is pure
时,you 保证任何给定的输入将始终产生相同的输出。在您最后一个 sub greet
示例中,在我看来,您无法保证对于默认值情况,因为数据库的内容可能会更改,或者 get-from-db
可能会产生副作用。
当然,如果你确定数据库没有改变,并且没有任何副作用,你仍然可以将 is pure
应用到子,但是为什么你要使用数据库呢? ?
你为什么要将子标记为 is pure
呢?好吧,它允许编译器在编译时对子例程的调用进行常量折叠。举个例子:
sub foo($a) is pure {
2 * $a
}
say foo(21); # 42
如果您查看为此生成的代码:
$ raku --target=optimize -e 'sub foo($a) is pure { 2 * $a }; say foo(21)'
然后你会在接近尾声时看到这个:
│ │ - QAST::IVal(42)
42
是 foo(21)
的常量折叠调用。这样整个调用就被优化掉了,因为 sub 被标记为 is pure
并且您提供的参数是一个常量。