Wait the light to fall

Raku 中的签名

焉知非鱼

Signature in Raku

签名也是对象 #

class Signature {}

签名是代码对象参数列表的静态描述。即, 签名描述了你需要什么参数和多少参数传递给代码或函数以调用它们。

传递参数给签名把包含在 Capture 中的参数绑定到了签名上。

> sub a($a, $b) {};
> &a.signature.perl.say
:($a, $b)
> my $b = -> $a, $b {};
> $b.signature.perl.say
:($a, $b)

签名是一个对象, 就像 Raku 中的任何其它东西一样。 任何 Callable 类型中都有签名, 并且它能使用 .signature 方法获取到。

class Signature { ... }

签名字面量 #

签名出现在子例程方法名后面的圆括号中, 对于 blocks 签名出现在 -><-> 箭头后面, 或者作为变量声明符(例如 my )的输入, 或者以冒号开头作为单独的项。

sub f($x) { }
#    ^^^^ sub f 的签名
method x() { }
#       ^^ 方法 x 的签名
my $s = sub (*@a) { }
#           ^^^^^ 匿名函数的签名

for <a b c> -> $x { }
#              ^^    Block 的签名

my ($a, @b) = 5, (6,7,8);
#  ^^^^^^^^ 变量声明符的签名

my $sig = :($a, $b);
#          ^^^^^^^^ 独立的签名对象

签名字面量可以用于定义回调或闭包的签名。

sub f(&c:(Int))    {}
sub will-work(Int) {}
sub won't-work(Str){}
f(&will-work);

f(&won't-work);
CATCH { default { put .^name, ': ', .Str } };
# OUTPUT: «X::TypeCheck::Binding::Parameter: Constraint type check failed in binding to parameter '&c'␤»

f(-> Int { 'this works too' } );

支持签名和列表(List)间的智能匹配。

my $sig = :(Int $i, Str $s);
say (10, 'answer') ~~ $sig; # OUTPUT: «True␤» 

my $sub = sub ( Str $s, Int $i ) { return $s xx $i };
say $sub.signature ~~ :( Str, Int ); # OUTPUT: «True␤» 

given $sig {
    when :(Str, Int) { say 'mismatch' }
    when :($, $)     { say 'match' }
    default          { say 'no match' }
}
# OUTPUT: «match␤» 

它匹配第二个 when 从句, 因为 :($, $) 表示带有两个标量, 匿名参数的 Signature, 它是 $sig 的更一版版本。

当和散列智能匹配时, 签名被认为由散列的键组成。

my %h = left => 1, right => 2;
say %h ~~ :(:$left, :$right);
# OUTPUT
# True

参数分隔符 #

签名由逗号分割的 0 个或多个形式参数组成。

my $sig = :($a, @b, %c)
sub add ($a, $b) { $a + $b }

作为一个例外, 签名中的第一个参数的后面可以跟着一个冒号而非逗号来标记方法的调用者。调用者是用来调用方法的对象, 其通常绑定到 self。 通过在签名中指定调用者, 你可以更改它绑定到的变量名称。

method ($a: @b, %c){}  # 第一个参数是调用者

class Foo {
    method whoami ($me:) {
        "Well I'm class $me.^name(), of course!"
    }
}

say Foo.whoami; # Well I'm class Foo, of course!

类型约束 #

参数可以可选地拥有一个类型约束(默认为 Any)。这些能用于限制函数允许的输入。

my $sig = :(Int $a, Str $b)
sub divisors (Int $n) { $_ if $n %% $_ for 1..$n }
divisors 2.5; # !!! Calling 'divisors' will never work with argument types (Rat)
# ===SORRY!=== Error while compiling: 
# Calling divisors(Rat) will never work with declared signature (Int $n) 

匿名参数也行, 如果参数只需要它的类型约束的话。

my $sig = :($, @, %a)         # 两个匿名参数和一个 "正常的(有名字的)"参数
$sig = :(Int, Positional)     # 只有类型也行(两个参数)
sub baz (Str) {"Got passed a Str"}

类型约束也可以是类型捕获(type captures)。

除了这些名义上的类型之外, 额外的约束可以以代码块的形式加到参数上, 代码块必须返回一个真值以通过类型检测。

sub f(Real $x where { $x > 0 }, Real $y where { $y >= $x }) { }

where 子句中的代码有一些限制:任何会产生副作用的东西(例如, 打印输出, 从迭代器中拉取或递增状态变量)都不受支持, 如果使用了, 可能会产生令人惊讶的结果。此外, 在某些实现中, where 子句的代码可能会针对单个类型检查运行多次。

where 子句不需要是代码块, where 子句右侧的任何内容都将用于和参数智能匹配。所以你也可以写:

multi factorial(Int $ where 0) { 1 }
multi factorial(Int $x)        { $x * factorial($x - 1) }

第一个还能简化为:

multi factorial(0) { 1 }

你可以直接把字面量用作类型并把值约束到匿名参数上。

提示:注意不要在你有几个条件时不小心离开区块:

-> $y where   .so && .name    {}( sub one   {} ); # WRONG!! 
-> $y where { .so && .name }  {}( sub two   {} ); # OK! 
-> $y where   .so &  .name.so {}( sub three {} ); # Also good 

第一个版本是错误的, 会发出一个关于 sub 对象强制转换为字符串的警告。原因是表达式相当于 ($y ~~ ($y.so && $y.name)); 那就是“调用 .so, 如果它为 True, 调用 .name; 如果这也是 True, 则使用它的值进行smartmatching …”。这是 (.so && .name) 的结果将被智能匹配, 但我们要检查 .so.name 是否为真值。这就是为什么明确的 Block 或者 Junction 是正确的版本。

在签名中不是子签名(sub-signature)的一部分的所有先前的参数都可以在参数后面的 where 从句中访问。 因此, 最后一个参数的 where 从句可以访问不是子签名一部分的签名的所有参数。 对于子签名, 把 where 从句放在子签名中。

sub foo($a, $b where * == $a ** 2) { say "$b is a square of $a" }
foo 2, 4; # OUTPUT: «4 is a square of 2␤»» 
# foo 2, 3; 
# OUTPUT: «Constraint type check failed in binding to parameter '$b'…» 

sub one-of-them(:$a, :$b, :$c where { $a.defined ^^ $b.defined ^^ $c.defined }) {
    $a // $b // $c
};
say one-of-them(c=>42);
# OUTPUT«42␤»

约束可选参数 #

可选参数也可以拥有约束。任何参数 where 从句都将被执行, 即使它是可选的, 而且不是由调用者提供。在这种情况下, 您可能必须防止 where 从句中的未定义值。

sub f(Int $a, UInt $i? where { !$i.defined or $i > 5 } ) { ... }

约束吞噬参数 #

吞噬参数不能拥有类型约束。一个 where 从句连同一个 Junction可以达到同样的那个效果。

sub f(*@a where { $_.all ~~ Int }) { say @a };
f(42);
f(<a>);
# OUTPUT«[42] Constraint type check failed for parameter '@a'  in sub f at ...»

约束定义值和未定义值 #

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

sub limit-lines (Str $s, Int $limit) {
    my @lines = $s.lines;
    @lines[0 ..^ min @lines.elems, $limit].join("\n")
}

say (limit-lines "a \n b \n c \n d \n", 3).perl; # "a \n b \n c "
say limit-lines Str,      3;  # Uh-oh. Dies with "Cannot call 'lines';"

CATCH { default { put .^name, ': ', .Str } };
# OUTPUT: «X::Multi::NoMatch: Cannot resolve caller lines(Str: ); none of these signatures match:
#     (Cool:D $: |c is raw)
#     (Str:D $: :$count!, *%_)
#     (Str:D $: $limit, *%_)
#     (Str:D $: *%_)»

say limit-lines "a \n b", Int # Always returns the max number of lines

这样的情况, 我们其实只想处理定义了的字符串。要这样做, 我们使用 :D 类型约束。

sub limit-lines (Str:D $s, Int $limit) {
    ...
}

say limit-lines Str, 3;

CATCH { default { put .^name, .Str } };
# OUTPUT«X::AdHocParameter '$s' requires an instance of type Str, but a
# type object was passed.  Did you forget a .new?»
# Dies with "参数 '$s' 需要一个实例, 但是函数 limit-lines 中却传递了一个类型对象。

如果传递一个诸如 Str 这样的类型对象进去, 那么就会报错。这样的失败方式比以前更好了, 因为失败的原因更清晰了。

也有可能未定义的类型是子例程唯一有意义的接收值。这可以使用 :U 类型约束来约束它。例如, 我们可以把 &limit-lines 转换成 multi 函数以使用 :U 约束。

multi  limit-lines (Str $s, Int:D $limit) {
    my @lines = $s.lines;
    @lines[0 ..^ min @lines.elems, $limit].join("\n");
}

multi limit-lines (Str $s, Int:U $) {$s} # 如果传递给我一个未定义的类型对象, 就返回整个字符串

say limit-lines "a \n b \n c", Int;      # "a \n b \n c"

为了显式地标示常规的行为, 可以使用 :_, 但这不是必须的。 :(Num:_ $):(Num $) 相同。

约束 Callables 的签名 #

要基于 block和子例程的签名约束 block 和子例程引用, 参数名要写在签名的后面。

sub f(&c:(Int, Str))  { say c(10, 'ten') };
sub g(Int $i, Str $s) { $s ~ $i          };

f(&g);

# OUTPUT: ten10

约束返回类型 #

--> 标记后面跟着一个类型会强制在子例程执行成功时进行类型检测。返回类型箭头必须放在参数列表的后面。跟在签名声明后面的 returns 关键字有同样的功能。Nil 在类型检测中被认为是定义了的。这允许沿着调用链返回和传递 Failure

sub (--> Int)   { my Int $i; $i};
sub (--> Int:D)    { 1 };
sub () returns Int { 1 };

sub foo(--> Int) { 1 };
sub foo() returns Int { 1 };        # 同上
sub does-not-work(--> Int) { "" }; # throws X::TypeCheck::Return

如果类型约束是一个常量表达式, 那么它被用于子例程的返回值。那个子例程中的任何 return 语句必须是不含参数的。

sub foo(--> 123) { return }

NilFailure 总是被允许作为返回类型, 不管类型约束是什么。

sub foo(--> Int) { Nil };
say foo.perl; # Nil

不支持类型捕获和强制类型。

强制类型 #

要接受一个类型但是强制它自动地转为另一种类型, 使用接受的类型作为目标类型的参数。如果接受的类型是 Any, 那么它可以被省略。

sub f(Int(Str) $want-int, Str() $want-str) { say $want-int.WHAT, $want-str.WHAT }
f '10', 10;

# OUTPUT
# (Int)(Str)

吞噬参数(或长度可变参数) #

数组或散列参数可以通过前置一个星号(s)被标记为吞噬参数, 这意味着它可以被绑定给任意数量的参数(0 个或 多个)。

它们被叫做吞噬参数, 因为它们吞完函数中的任何剩余参数, 就像有些人吞吃面条那样。

$ = :($a, @b)  # 正好两个参数, 而第二个参数必须是 Positional 的
$ = :($a, *@b) # 至少一个参数, @b 吞噬完任何剩余的参数
$ = :(*%h)     # 没有位置参数, 除了任意数量的具名参数
sub one-arg (@)  { }
sub slurpy  (*@) { }
one-arg (5, 6, 7);   # ok, same as one-arg((5, 6, 7))
slurpy  (5, 6, 7);   # ok
slurpy   5, 6, 7 ;   # ok
# one-arg(5, 6, 7) ; # X::TypeCheck::Argument
# one-arg  5, 6, 7 ; # X::TypeCheck::Argument

sub named-names (*%named-args) { %named-args.keys };
say named-names :foo(42) :bar<baz>; # foo bar

注意位置参数不允许出现在吞噬参数的后面。

:(*@args, $last);
CATCH { when X::Parameter::WrongOrder { put .^name, ': ', .Str } }
# OUTPUT«X::Parameter::WrongOrder: 不能把必要参数放在可变长度参数的后面»

带有一个星号的吞噬参数会通过消融一层或多层裸的可迭代对象来展平参数。带有两个星号的吞噬参数不会展平参数:

sub a(*@a)  { @a.join("|").say };
sub b(**@b) { @b.join("|").say };

a(1,[1,2],([3,4],5));    #  1|1|2|3|4|5
b(1,[1,2],([3,4],5));    # 1|1 2|3 4 5

通常, 吞噬参数会创建一个数组, 为每个 argument 创建一个标量容器, 并且把每个参数的值赋值给那些标量。如果在该过程中原参数也有一个中间的标量分量, 那么它在调用函数中是访问不到的。

吞噬参数在和某些 traits and modifiers 组合使用时会有特殊行为, 像下面描述的那样。

Single Argument Rule Slurpy #

单一参数规则允许根据上下文处理子程序、for-loop 和列表构造函数的参数。许多位置类型的方法可以像处理列表或参数一样处理单个参数。在 Signature 中使用 +@ 作为标志, 提供了语法糖, 使这项工作更容易一些。任何非位置类型的单一参数都会被提升为一个具有单一项的列表。

sub f(+@a){ dd @a };
f(1);

# OUTPUT«[1]␤»
f(1, 2, 3);
# OUTPUT«[1, 2, 3]␤»
my @b = <a b c>;
f @b;
# OUTPUT«["a", "b", "c"]␤»

类型捕获 #

类型捕获允许把类型约束的说明推迟到函数被调用时。它们允许签名和函数体中的类型都可以引用。

sub f(::T $p1, T $p2, ::C) {
    # $p1 和 $p2 的类型都为 T, 但是我们还不知道具体类型是什么
    # C 将会保存一个源于类型对象或值的类型
    my C $closure = $p1 / $p2;
    return sub (T $p1) {
        $closure * $p1;
    }
}

# 第一个参数是 Int 类型, 所以第二个参数也是
# 我们从调用用于 &f 中的操作符导出第三个类型
my &s = f(10,2, Int.new / Int.new);
say s(2);  # 10 / 2 * 2  == 10

Positional vs. Named #

参数可以是跟位置有关的或者是具名的。所有的参数都是 positional 的, 除了吞噬型散列参数和有前置冒号标记的参数:

$ = :($a)   # 位置参数
$ = :(:$a)  # 名字为 a 的具名参数
$ = :(*@a)  # 吞噬型位置参数
$ = :(*%h)  # 吞噬型具名参数

在调用者这边, 位置参数的传递顺序和它们声明顺序相同。

sub pos($x, $y) { "x = $x y = $y" };
pos(4, 5); #  x = 4 y = 5

对于具名实参和具名形参, 只用名字用于将实参映射到形参上。

sub named(:$x, :$y) { "x=$x y=$y" }
named( y => 5, x => 4);

具名参数也可以和变量的名字不同:

sub named(:official($private)) { "公务" if $private }
named :official;

别名也是那样做的:

sub ( :color(:colour($c)) ) { } # 'color' 和 'colour' 都可以
sub ( :color(:$colour) )    { } # same API for the caller

带有具名参数的函数可以被动态地调用, 使用 |非关联化一个 Pair 来把它转换为一个具名参数。

multi f(:$named) { note &?ROUTINE.signature };
multi f(:$also-named) { note &?ROUTINE.signature };

for 'named', 'also-named' -> $n {
    f(|($n => rand))      # «(:$named)␤(:$also-named)␤»
}

my $pair = :named(1);
f |$pair; # «(:$named)␤»

同样的语法也可以用于将散列转换为具名参数:

my %pairs = also-named => 4;
sub f(|c) {};
f |%pairs;        # (:$also-named)

可选参数和强制参数 #

Positional 参数默认是强制的, 也可以用默认值或结尾的问号使参数成为可选的:

$ = :(Str $id)         # 必要参数 required parameter
$ = :($base = 10)      # 可选参数, 默认为 10
$ = :(Int $x?)         # 可选参数, 默认为 Int 类型的对象

具名参数默认是可选的, 可以通过在参数末尾加上一个感叹号使它变成强制参数:

$ = :(:%config)        # 可选参数
$ = :(:$debug = False) # 可选参数, 默认为 False
$ = :(:$name!)         # 名为 name 的强制具名参数

默认值可以依靠之前的参数, 并且每次调用都会被重新计算。

$ = :($goal, $accuracy = $goal / 100);
$ = :(:$excludes = ['.', '..']); # a new Array for every call

解构参数 #

参数后面可以跟着一个由括号括起来的 sub-signature, 子签名会解构给定的参数。列表解构成它的元素:

sub first (@array ($first, *@rest)) { $first }

或:

sub first ([$first, *@]) { $first }

而散列解构成它的键值对儿(pairs):

sub all-dimensions (% (:length(:$x), :width(:$y), :depth(:$z))) {
    $x andthen $y andthen $z andthen True
}

andthen 返回第一个未定义的值, 否则返回最后一个元素。短路操作符。andthen 左侧的结果被绑定给 $_ 用于右侧, 或者作为参数传递, 如果右侧是一个 blockpointy block 的话。

一般地, 对象根据它的属性解构。通用的惯用法是在 for 循环中解包一个 Pair 的键和值:

for <Peter Paul Merry>.pairs -> (:key($index), :value($guest)) {
    ...
}

但是, 将对象解包为属性只是默认行为。要使对象以不同方式解构, 请更改其 Capture 方法。

子签名 #

要匹配复合参数, 请在圆括号中的参数名后面使用子签名。

sub foo(|c(Int, Str)){
   put "called with {c.perl}"
}

foo(42, "answer");
# OUTPUT«called with \(42, "answer")»

长名字 #

为了在多重分派中排除特定参数, 使用一个双分号来分割它们。

multi sub f(Int $i, Str $s;; :$b) { dd $i, $s, $b };
f(10, 'answer');
# OUTPUT«10 "answer"Any $b = Any»

捕获参数 #

在参数前前置一个垂直的 | 会让参数变为 Capture, 并使用完所有剩下的位置参数和具名参数。

这常用在 proto 定义中( 像 proto foo (|) {*} ) 来标示例程的 multi 定义可以拥有任何类型约束

如果绑定到变量参数, 则可以使用 slip 运算符 | 作为整体转发。

sub a(Int $i, Str $s) { say $i.WHAT, $s.WHAT }
sub b(|c) { say c.WHAT; a(|c) }
b(42, "answer");
# OUTPUT«(Capture)␤(Int)(Str)␤»

参数特性和修饰符 #

默认地, 形式参数被绑定到它们的实参上并且被标记为只读。你可以使用 traits 特性更改参数的只读特性。

is copy特性让参数被复制, 并允许在子例程内部修改参数的值。

sub count-up ($x is copy) {
    $x = Inf if $x ~~ Whatever;
    .say for 1..$x;
}

is rw 特性让参数只绑定到变量上(或其它可写的容器)。 赋值给参数会改变调用一侧的变量的值。

sub swap($x is rw, $y is rw) {
    ($x, $y) = ($y, $x);
}

对于吞噬参数, is rw 由语言设计者保留做将来之用

方法 #

params 方法 #

method params(Signature:D:) returns Positional

返回 Parameter 对象列表以组成签名。

arity 方法 #

method arity(Signature:D:) returns Int:D

返回所必须的最小数量的满足签名的位置参数

count 方法 #

method count(Signature:D:) returns Real:D

返回能被绑定给签名的最大数量的位置参数。如果有吞噬位置参数则返回 Inf

returns 方法 #

签名返回的任意约束是:

:($a, $b --> Int).returns # Int

ACCEPTS 方法 #

multi method ACCEPTS(Signature:D: Capture $topic)
multi method ACCEPTS(Signature:D: @topic)
multi method ACCEPTS(Signature:D: %topic)
multi method ACCEPTS(Signature:D: Signature $topic)

前三个方法会看参能否绑定给 capture, 例如, 如果带有那个 Signature 的函数能使用 $topic 调用:

(1,2, :foo) ~~ :($a, $b, :foo($bar)) # true
<a b c d> ~~ :(Int $a)               # False

最后一个会为真如果 $topic 能接收的任何东西也能被 Signature 接收。

:($a, $b) ~~ :($foo, $bar, $baz?)   # True
:(Int $n) ~~ :(Str)                 # False