`is pure` 特性和默认参数

cod*_*ons 6 functional-programming rakudo purely-functional raku

下面的 &greet 函数是纯的,可以适当地标上is pure trait。

sub greet(Str:D $name) { say "Hello, $name" }
my $user = get-from-db('USER');
greet($user);
Run Code Online (Sandbox Code Playgroud)

然而,这不是:

sub greet { 
    my $name = get-from-db('USER');
    say "Hello, $name" 
}

greet($user);
Run Code Online (Sandbox Code Playgroud)

不过这个呢?

sub greet(Str:D $name = get-from-db('USER')) { say "Hello, $name" }

greet();
Run Code Online (Sandbox Code Playgroud)

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

Jon*_*ton 7

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

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

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

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

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

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

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


Eli*_*sen 6

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

当然,如果你确定数据库没有变化,并且没有任何副作用,你仍然可以申请is puresub,但为什么要使用数据库呢?

你为什么要把一个子标记为is pure反正?好吧,它允许编译器在编译时对子例程的调用进行常量折叠。例如:

sub foo($a) is pure {
    2 * $a
}
say foo(21);   # 42
Run Code Online (Sandbox Code Playgroud)

如果您查看为此生成的代码:

$ raku --target=optimize -e 'sub foo($a) is pure { 2 * $a }; say foo(21)'
Run Code Online (Sandbox Code Playgroud)

那么你会在接近尾声时看到这个:

 ?   ?               - QAST::IVal(42) 
Run Code Online (Sandbox Code Playgroud)

42是恒定折叠呼吁foo(21)。所以这样整个调用都被优化掉了,因为 sub 被标记is pure并且你提供的参数是一个常量。

  • 我认为我的问题不够清楚。我知道相关的问题是我是否可以保证“任何给定的输入总是产生相同的输出”。但我要问的是什么才算是“输入”。如果输入是调用者传入的_arguments_,那么是的,最后一个 &greet 会产生不同的输出。但是,如果输入是函数接收的_parameters_,那么 &greet 函数始终为任何给定输入生成相同的输出。通常,参数绑定到参数,因此区别并不重要 - 但默认参数是一个例外 (2认同)