`is pure` 特性和默认参数

`is pure` trait and default parameters

下面的 &greet 函数是纯函数,可以适当地用 is pure 特征标记。

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();

从函数“内部”看,它看起来很纯粹——当参数绑定到相同的值时,它总是产生相同的输出,没有副作用。但从函数外部来看,它似乎是不纯的——当使用相同的参数调用两次时,它会产生不同的 return 值。 Raku/Rakudo拿哪个准?

当你说子 is pure 时, 保证任何给定的输入将始终产生相同的输出。在你最后一个 sub greet 的例子中,在我看来你不能保证默认值的情况,因为数据库的内容可能会改变,或者 get-from-db 可能有副作用。

当然,如果您确定数据库没有改变,并且没有任何副作用,您仍然可以将 is pure 应用到 sub,但是为什么要使用那么数据库呢?

你为什么要把一个子标记为 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) 

42foo(21) 的常量弃牌跟注。所以这样整个调用就被优化掉了,因为 sub 被标记为 is pure 并且你提供的参数是一个常量。

一种语言在实现参数默认值时至少可以采用两种策略:

  1. 将参数默认值视为编译器在遇到没有足够参数的调用时应在调用站点发出的内容,以便生成额外的参数传递给被调用者。这意味着可以支持参数的默认值,而无需在调用约定中明确支持它。然而,这也要求您在编译时始终知道调用的去向(或者至少足够准确地知道它以插入默认值,并且不能期望在子类的方法覆盖中使用不同的默认值并且有它成功了)。
  2. 调用约定足够强大,被调用者可以发现参数没有传递值,然后计算默认值。

由于其动态特性,其中只有第二种对 Raku 真正有意义,这就是它的作用。

在执行策略 1 的语言中,可以说将这样的函数标记为纯函数是有意义的,因为计算默认值的代码存在于每个调用点,因此任何基于纯度进行分析和转换的东西将已经不得不处理评估默认值的代码,并且可以看到它不是纯值的来源。

在策略 2 和 Raku 下,我们应该将默认值理解为在其签名中具有默认值的块或例程的实现细节。因此,如果计算默认值的代码是不纯的,那么作为一个整体的例程就是不纯的,因此 is pure 特征不合适。

更一般地说,如果对于给定的参数捕获我们总是可以期望相同的 return 值,则 is pure 特征适用。在给出的示例中,参数 capture \() 与此矛盾。

这里的另一种因式分解是使用 multi subs 而不是参数默认值,并且只用 is pure.

标记一个候选者