为什么将Perl 6命名参数限制为确定的值会使它成为必需值?

13

考虑这些只接受一个命名参数的子程序。命名参数应该是可选的,我没有看到任何例外。

如果没有类型约束,那么没有问题; 命名参数不是必需的。如果有可以接受类型对象的类型约束(没有注释,:U:_),那也不会存在问题。

Parameter '$quux' of routine 'quux' must be an object instance of type 'Int', 
not a type object of type 'Int'.  Did you forget a '.new'?
  in sub quux at /Users/brian/Desktop/type.p6 line 16
  in block <unit> at /Users/brian/Desktop/type.p6 line 37

如果使用注释:D来设置需要一个定义的值的类型约束,那么该命名参数就不再是可选的。也就是说,在其他定义中,我不必提供值。但是使用:D时,我必须提供一个值。我宁愿不要省略:D,因为我想要的值必须被定义。

Signatures文档中可以得到:

通常,类型约束只检查传递的值是否为正确的类型。

但是,在这里我没有传递任何值。我认为这些约束只适用于赋值操作。由于我没有显式地提供要分配的值,所以我认为不会有任何赋值和问题。然而,在Rakudo 2017.10中并非如此。这导致我采取各种不良方式进行解决。这与我的问题“对于Perl 6块而言,那是一个参数还是零个?”有关,我试图区分零个和一个参数的情况。

我可以通过分配默认值来解决这个问题,但在某些情况下,没有任何有意义的默认值。例如,Bool很容易,但是什么确切的Int适合呢?无论它是什么,都将成为一些魔法值,这会使代码变得复杂和混乱。我用“Perl 6是否有无限整数”来解决这个问题,但我之所以能够这样做是因为Inf在这种情况下可以作为有效的值。

sub foo ( :$foo ) {
    put $foo.defined ?? 'foo defined' !! 'foo not defined';
    }

sub bar ( Int :$bar ) {
    put $bar.defined ?? 'bar defined' !! 'bar not defined';
    }

sub baz ( Int:U :$baz ) {
    put $baz.defined ?? 'baz defined' !! 'baz not defined';
    }

sub quux ( Int:D :$quux ) {
    put $quux.defined ?? 'quux defined' !! 'quux not defined';
    }

sub quack ( Int:_ :$quack ) {
    put $quack.defined ?? 'quack defined' !! 'quack not defined';
    }

foo();
foo( foo => 2 );

bar();
bar( bar => 2 );

baz();
baz( baz => Int );

quack();
quack( quack => 2 );

quux( quux => 2 );
quux();
4个回答

8
所有参数都有一些值,即使它们是可选的。我在379678b794a7中澄清了您所引用的文档。
可选参数具有默认默认值,这些值是显式或隐式类型约束的类型对象(例程的隐式约束为Any,块的隐式约束为Mu)。
sub (Int $a?, Num :$b) { say "\$a is ", $a; say "\$b is ", $b }()
# OUTPUT:
# $a is (Int)
# $b is (Num)

以上,默认默认值符合参数的类型约束。如果你使用:U:_类型的表情符号,情况也是如此。

然而,当你使用:D类型的表情符号时,默认默认值就不再匹配类型约束了。如果不加检查,你将失去指定:D约束的好处。因为默认默认类型的对象在这个例程的主体中会导致爆炸,从而导致期望参数为明确值的问题。

sub (Int:D $a?, Num:D :$b) { say $a/$b }()

回答标题中的问题,即是否指定 :D 应该自动使参数变为必需。我对此持反对意见,因为它引入了一个特殊情况,用户必须学习才能节省一次键入单个字符(! 以标记参数为必需)。这样做还会产生一个不太有帮助的错误提示,谈论参数数量或必需参数,而不是实际的问题:默认参数未通过参数类型检查失败。

4

就我所知,可选的位置参数也存在类似的问题:

sub a(Int:D $number?) { ... }
a; # Parameter '$number' of routine 'a' must be an object instance of type 'Int', not a type object of type 'Int'.  Did you forget a '.new'

如果您将类型对象指定为默认值,则会出现相同的问题:
sub a(Int:D $number = Int) { ... };
a; #  # Parameter '$number' of routine 'a' must be an object instance of type 'Int', not a type object of type 'Int'.  Did you forget a '.new'

很抱歉,这只是将参数应用于:D约束的后果:您必须指定一个已定义的默认值,才能在不使用任何参数的情况下调用它。当然,另一种方法可以是使用多个子例程和必需的命名参数:
multi sub a() { say "no foo named" }
multi sub a(Int:D :$foo!) { say "foo is an Int:D" }

希望这能帮到你。

用表达每个参数组合的多个子程序是困难的。 - brian d foy
我认为文档中应该使用“隐式设置参数”或类似的术语来强调参数将存在,只是由谁设置值的问题,而不是使用“可选参数”的说法。 - brian d foy
“可选参数”中的“可选”并不意味着该参数是可选的。相反,它意味着传递与该参数对应的参数是可选的。 - raiph
这就是为什么你不应该说“可选参数”。记住,那些对此一无所知的人会阅读这篇文档并认为文档所说的就是事实。 - brian d foy
也许是“可选参数”?有人可能会反对,因为他们认为“参数”是多余的或令人困惑的,但根本问题在于,在绑定参数到参数(如果它们没有包含在特定调用中,则不绑定)的过程完成之前,参数和参数是不同的东西。因此,对“可选参数”反应的任何惊讶都接近完美,就像我们想要关于这个问题的可教授时刻的“WAT?”触发器一样。 - raiph

3

我通过检查是否为 Any 类型或者智能匹配来解决了这个问题:

sub f ( :$f where { $^a.^name eq 'Any' or $^a ~~ Int:D } ) {
    put $f.defined ?? "f defined ($f)" !! 'f not defined';
    }

f( f => 5 );
f();

回答我的原始问题:所有参数都是必需的。只是它们如何获得值的问题。它可以通过参数、显式默认值或隐式默认值(基于其类型约束)来获得。

我认为我明白你为什么写下“所有参数都是必需的”,因为在这个特定的SOQA中,你的思考过程很明显,但它直接与P6语言及其文档中长期存在且合理的用法相矛盾(例如.optional),其中位置参数和命名参数都可以是可选或必需的。 - raiph
1
这些定义适用于用户如何指定“参数”,但正如我所展示的,它与关于“参数”的其他期望产生了冲突。 - brian d foy
1
我认为这个补丁让情况变得更糟了。我认为这是心理模型中一个更大的问题。 - brian d foy
顺便说一下,你在那里进行了两个智能匹配(一个是块中的显式匹配,另一个是在其结果中的 where)。你可以使用 thunk 来使它变快约 15%:(:$f where .WHAT =:= Any || Int:D)由于 where 子句会阻止调度缓存,并且你无论如何都要在主体中检查 .definedness,所以只需使用 Int 类型约束和主体中的 .DEFINITE 检查即可获得约 7.8 倍的更快代码。 - user2410502
我认为这个补丁使情况变得更糟了。请打开一个文档问题,以便可以改进:https://github.com/perl6/doc/issues/new - user2410502
通过类型名称进行检查并不是处理这个问题的好方法。这不是 Perl 5,类可以有相同的名称。你应该使用 $^a === Any - Brad Gilbert

2

修复 Brian 代码中的问题

sub f ( :$f where { $^a.^name eq 'Any' or $^a ~~ Int:D } ) { ... }

这段代码丑陋、复杂,而且存在毫无必要的强制类型转换
以下是修复了这些问题的代码:
sub f ( :$f where Any | Int:D ) { ... }

但是仍然存在问题。使用Any类型意味着f: f(foo)调用,其中foo恰好是Any,将通过。
相反,可以使用一种调用代码不可能使用的类型(假设以下声明在包中并从包外使用):
my role NoArg {}
our sub f ( :$f where Int:D | NoArg = NoArg ) { ... }

这留下了一个最后的问题:brian方法中固有的LTA错误信息。
虽然以下内容目前会减慢代码速度,但它进一步提高了代码的可读性,并且如果传递了无效参数,则自动生成的错误消息是完全有效的(期望OptionalInt但得到...):
my role NoArg {}
my subset OptionalInt where Int:D | NoArg;
our sub f ( OptionalInt :$f = NoArg ) { ... }

这段文本的英译中文如下:

它不是关于命名参数的

Brian 的问题标题为:

为什么将一个 Raku 的 命名参数 约束为确定值会使它成为必需值?

Brian 所描述的情况并不涉及命名参数。它是由于 :D 约束造成的。它适用于所有参数、变量和标量容器:

sub quux ( Int:D :$quux ) {} # Named parameters require a definite value:
quux;                        # Error.

sub quux ( Int:D $quux? ) {} # Positional parameters do too:
quux;                        # Error

my Int:D $foo;               # And variables: Error.

my Int:D @array;             # And containers:
@array[42] := Nil;           # Error

Raku(do)正在提供帮助

:D类型的“笑脸”表示一个明确的值。这是它的目的。

如果用户代码未为任何具有(仅):D约束的参数/变量/容器提供明确的值,则Raku(do)逻辑上必须执行一些明智的操作以维护类型安全性。这就是它正在做的事情。


考虑Raku中逐渐升级的类型约束:
  1. 如果没有指定类型约束,就没有必要坚持任何特定类型的值。Raku(do) 保持 内存安全性,但仅此而已。

  2. 如果参数/变量/容器具有 Int(或 Int:_)类型约束,则 Raku(do) 确保赋给它或绑定到它的任何值都是 Int,但它不会坚持这是一个明确的 Int

  3. 如果参数/变量/容器具有 :D 约束(例如 Int:D),则 Raku(do) 坚持分配或绑定到它的任何值都是确定的值,或者如果不是,则通过抛出可恢复异常(如果安全)或终止程序(如果不安全)来维护类型安全性。它没有做的一件事是编造一些神奇的默认值,因为正如 Brian 在他的问题中所指出的:

    [Raku(do)] 可以通过分配默认值来解决这个问题,但是......什么确定的 Int 会适合呢?无论是什么,都将是一些神奇的值,这将使代码复杂和分散注意力。


在下面的参数情况下,Raku(do)拒绝将quux调用绑定到quux子例程,并通过抛出可恢复异常来保持类型安全。如果开发人员希望这样做,他们可以选择.resume,但后果自负:
sub quux ( Int:D :$quux? ) { say 42 } # Does NOT display 42
quux;
CATCH { .resume }
say 99;                               # 99

在变量情况下,Rakudo 可能再次能够维护内存安全性:
my Int:D $quux = 42;
$quux = Nil;
CATCH { .say; .resume }  # Type check failed ...
say $quux;               # 42

或者也可能不会;在极端情况下,Raku(do) 在编译时失败并显示无法恢复的错误。
my Int:D $quux; # "Error while compiling ... requires an initializer"
CATCH { .resume }
say 99;         # Does NOT display 99

在这种情况下,Raku(do)必须在编译期间退出以避免魔术值问题。

“所有参数都是必需的”这个说法是不正确的

在他的回答中,Brian说“所有参数都是必需的”。
正如我上面解释的那样,这不仅仅涉及参数,还涉及变量和标量容器。
说所有参数、所有变量和所有容器都是必需的有意义吗?那是什么意思?
Brian的心理模型完全与Raku的语法相矛盾:
sub foo ($bar is required, $baz?) { ... }

“$bar”是必需的吗?是的。 “$baz”是必需的吗?不是。
想象一个包含两个字段的网络表单:一个带有“*必填”注释的用户名字段,以及一个没有注释或者也许带有“*可选”注释的密码字段。这个表单包含多少个必填字段?大多数人会说它只包含一个。这是Raku在“必填”和“可选”这两个词方面所遵循的心智模型。
如果编写表单背后的代码的人指定密码字段必须包含确定的值,那么它就不能留空。也许代码注入一个默认的“my_password”来满足确定的值约束。同样,这是Raku关于:D约束的心智模型。
是的,可能会有人对“可选参数”和“必需参数”感到困惑。Brian显然就是这样。因此,也许我们在文档中至少使用一两次“参数”这个词?我不确定它是否有帮助。
另外,如果开发人员编写了一个“可选参数”,但同时还指定了一个:D限制,那么SO中描述的问题仍然存在。将调用参数更改为“参数”可能没有帮助。
我认为Rakoon的共识是,责任在于那些认为自己有更好措辞的人来展示和说明。我对Brian的“所有参数都是必需的”的回应是“每个人类问题都有一个众所周知的解决方案——整洁、貌似正确,但实际上是错误的”。但也许你,亲爱的读者,有更好的主意?

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接