函数

Raku 中的函数

例程(Routines)是 Raku 中代码重用的最小手段。它们有几种形式,最明显的是属于类和角色并与对象相关联的方法,还有函数, 也叫做子例程或短子程序,它们独立于对象而存在。

子例程默认是词法(my)作用域的,对它们的调用通常在编译时解析。

子例程可以具有签名,也称为参数列表,其指定签名期望的参数(如果有的话)。 它可以指定(或保持打开)参数的数量和类型,以及返回值。

子例程的内省通过例程提供。

定义/创建/使用 函数

子例程

创建子例程的基本方法是使用 sub 声明符,后跟可选标识符

sub my-func { say "Look ma, no args!" }
my-func;

sub 声明符返回可以存储在任何容器中的 Sub 类型的值:

my &c = sub { say "Look ma, no name!" }
c;     # OUTPUT: «Look ma, no name!␤» 
 
my Any:D $f = sub { say 'Still nameless...' }
$f();  # OUTPUT: «Still nameless...␤» 
 
my Code \a = sub { say ‚raw containers don't implement postcircumfix:<( )>‘ };
a.();  # OUTPUT: «raw containers don't implement postcircumfix:<( )>␤» 

sub 声明符将在编译时在当前作用域内声明一个新名称。因此,任何间接性都必须在编译时解析:

constant aname = 'foo';
sub ::(aname) { say 'oi‽' };
foo;

一旦将宏添加到 Raku 中,这将变得更有用。

为了使子程序接受参数,签名被放置在子例程名称和它的函数主体之间,在括号中:

sub exclaim ($phrase) {
    say $phrase ~ "!!!!"
}
exclaim "Howdy, World";

默认地, 子例程是词法作用域的。即 sub foo {...}my sub foo {...} 是相同的并且只被定义在当前作用域中。

sub escape($str) {
    # Puts a slash before non-alphanumeric characters
    S:g[<-alpha -digit>] = "\\$/" given $str
}

say escape 'foo#bar?'; # foo\#bar\?

{
    sub escape($str) {
        # Writes each non-alphanumeric character in its hexadecimal escape
        S:g[<-alpha -digit>] = "\\x[{ $/.ord.base(16) }]" given $str
    }

    say escape 'foo#bar?' # foo\x[23]bar\x[3F]
}

# Back to original escape function
say escape 'foo#bar?'; # foo\#bar\?

子例程不必命名; 这种情况下, 它们被叫做匿名的。

say sub ($a, $b) { $a ** 2 + $b ** 2 }(3, 4) # 25

但在这种情况下,通常希望使用更简洁的块语法。可以就地调用子例程和块,如上例所示。

Blocks 和 Lambdas

每当你看到像

{ $_ + 42 }, -> $a, $b { $a ** $b }

{ $^text.indent($:spaces) }

那么这是语法。 它在每个 ifforwhile 等关键字之后使用。

for 1, 2, 3, 4 -> $a, $b {
    say $a ~ $b;
}
# OUTPUT: «12␤34␤» 

它们也可以作为匿名代码块自己使用。

say { $^a ** 2 + $^b ** 2}(3, 4) # 25

有关块语法的详细信息,请参阅类型的文档。

签名

函数接受的参数在其签名中有描述。

sub format(Str $s) { ... }
-> $a, $b { ... }

有关签名的语法和使用的详细信息,请参阅 Signature 类的文档。

自动签名

如果没有提供签名,但在函数体中使用了两个自动变量 @_%_ 中的任何一个,则将生成带有 *@_*%_ 的签名。 两个自动变量可以同时使用。

sub s { say @_, %_ };
dd &s.signature # OUTPUT«:(*@_, *%_)␤»

参数

参数以逗号分隔列表的形式提供。 要消除嵌套调用的歧义, 可以使用圆括号或副词形式。

sub f(&c){ c() * 2 }; # call the function reference c with empty parameter list
sub g($p){ $p - 2 };
say(g(42)); # nest call to g in call to say
f: { say g(666) }; # call f with a block

当调用函数时,位置参数应该以与函数签名相同的顺序提供。 命名参数可以以任何顺序提供,但是最好将命名参数放在位置参数之后。 在函数调用的参数列表中,支持一些特殊的语法:

sub f(|c){};
f :named(35);     # 具名参数(in "adverb" form.)
f named => 35;    # 也是具名参数.
f :35named;       # 使用缩写的副词形式的具名参数
f 'named' => 35;  # 不是具名参数, 而是一个 Pair 位置参数
my \c = <a b c>.Capture;
f |c;             # Merge the contents of Capture $c as if they were supplied

传递给函数的参数在概念上首先被收集在 Capture 容器中。 关于这些容器的语法和使用的细节可以在 Capture 类的文档中找到。

当使用命名参数时,请注意,正常的 List “pair-chaining” 允许在命名参数之间跳过逗号。

sub f(|c){};
f :dest</tmp/foo> :src</tmp/bar> :lines(512);
f :32x :50y :110z;   # This flavor of "adverb" works, too
f :a:b:c;            # The spaces are also optional.

返回值

任何块或例程将把它的最后一个表达式作为返回值提供给调用者。如果 returnreturn-rw 被调用,它们的参数(如果有的话)将成为返回值。 默认返回值为 Nil

sub a { 42 };
sub b { say a };
b;
# OUTPUT«42␤»

多个返回值作为列表或通过创建捕获返回。 解构可以用于解开多个返回值。

sub a { 42, 'answer' };
put a.perl;
# OUTPUT«(42, "answer")␤»

my ($n, $s) = a;
put [$s, $n];
# OUTPUT«answer 42␤»

sub b { <a b c>.Capture };
put b.perl;
# OUTPUT«\("a", "b", "c")␤»

返回类型约束

Raku 有很多方式来指定函数的返回类型:

sub foo(--> Int)      {}; say &foo.returns; # (Int)
sub foo() returns Int {}; say &foo.returns; # (Int)
sub foo() of Int      {}; say &foo.returns; # (Int)
my Int sub foo()      {}; say &foo.returns; # (Int)

尝试返回另外一种类型的值会引起编译错误。

sub foo() returns Int { "a"; }; foo; # Type check fails

注意,NilFailure 是免于返回类型约束,并且可以从任何子例程返回,而不管其约束:

sub foo() returns Int { fail   }; foo; # Failure returned
sub bar() returns Int { return }; bar; # Nil returned

多重分派

Raku 允许你使用同一个名字但是不同签名写出几个子例程。当子例程按名字被调用时, 运行时环境决定哪一个子例程是最佳匹配, 然后调用那个候选者。你使用 multi 声明符来声明每个候选者。

multi congratulate($name) {
    say "祝你生日快乐, $name";
}

multi congratulate($name, $age) {
    say "祝 $age 岁生日快乐, $name";
}

congratulate 'Camelia'; # 祝你生日快乐, Camelia
congratulate 'Rakudo', 15; # 祝你 15 岁生日快乐, Rakudo

分发/分派(dispatch) 可以发生在参数的数量(元数)上, 但是也能发生在类型上:

multi as-json(Bool $d) { $d ?? 'true' !! 'false' }
multi as-json(Real $d) { ~$d }
multi as-json(@d)      { sprintf '[%s]', @d.map(&as-json).join(', ') }

say as-json([True, 42]); # [true, 42]

不带任何指定例程类型的 multi 总是默认为 sub, 但是你也可以把 multi 用在方法(methods)上。那些候选者全都是对象的 multi 方法:

class Congrats {
    multi method congratulate($reason, $name) {
        say "Hooray for your $reason, $name";
    }
}

role BirthdayCongrats {
    multi method congratulate('birthday', $name) {
        say "Happy birthday, $name";
    }
    multi method congratulate('birthday', $name, $age) {
        say "Happy {$age}th birthday, $name";
    }
}

my $congrats = Congrats.new does BirthdayCongrats;

$congrats.congratulate('升职', 'Cindy');   #-> 恭喜你升职,Cindy
$congrats.congratulate('birthday', 'Bob'); #-> Happy birthday, Bob

proto

proto 从形式上声明了 multi 候选者之间的共性。 proto 充当作能检查但不会修改参数的包装器。看看这个基本的例子:

proto congratulate(Str $reason, Str $name, |) {*}
multi congratulate($reason, $name) {
   say "Hooray for your $reason, $name";
}
multi congratulate($reason, $name, Int $rank) {
   say "Hooray for your $reason, $name -- you got rank $rank!";
}

congratulate('being a cool number', 'Fred');     # OK
congratulate('being a cool number', 'Fred', 42); # OK
congratulate('being a cool number', 42);         # Proto match error

所有的 multi congratulate 都会遵守基本的签名, 这个签名中有两个字符串参数, 后面跟着可选的更多的参数。 | 是一个未命名的 Capture 形参, 它允许 multi 接收额外的参数。第三个 congratulate 调用在编译时失败, 因为第一行的 proto 的签名变成了所有三个 multi congratulate 的共同签名, 而 42 不匹配 Str

say &congratulate.signature #-> (Str $reason, Str $name, | is raw)

你可以给 proto 一个函数体, 并且在你想执行 dispatch 的地方放上一个 {*}

# attempts to notify someone -- returns False if unsuccessful
proto notify(Str $user,Str $msg) {
   my \hour = DateTime.now.hour;
   if hour > 8 or hour < 22 {
      return {*};
   } else {
      # we can't notify someone when they might be sleeping
      return False;
   }
}

{*} 总是分派给带有参数的候选者。默认参数和类型强制转换会起作用单不会传递。

proto mistake-proto(Str() $str, Int $number = 42) {*}
multi mistake-proto($str,$number) { say $str.WHAT }
mistake-proto(7,42);   #-> (Int) -- coercions not passed on
mistake-proto('test'); #!> fails -- defaults not passed on

约定和惯用法

虽然上面描述的调度系统提供了很多灵活性,但是存在一些大多数内部函数以及许多模块中的函数将遵循的约定。 这些将产生一致的外观和感觉。

吞噬约定

也许最重要的是处理 slurpy 列表参数的方式。 大多数时候,函数不会自动展平吞噬(slurpy)列表。 罕见的例外是在列表的列表上没有合理行为的那些函数(例如chrs),或者与已建立的习语有冲突的函数,例如 poppush 的逆操作。

如果你想匹配这个外观和感觉,任何可迭代(Iterable)参数必须使用 **@slurpy 逐个元素地打开,有两个细微差别:

  • Scalar 容器内的 Iterable 不计数。
  • 在顶层使用 , 创建的列表只能计数为一个 Iterable。

这可以通过使用带有 ++@ 而不是 **的 slurpy 来实现:

sub grab(+@a) { "grab $_".say for @a }

这非常接近于:

multi sub grab(**@a) { "grab $_".say for @a }
multi sub grab(\a) {
    a ~~ Iterable and a.VAR !~~ Scalar ?? nextwith(|a) !! nextwith(a,)
}

这导致以下行为,称为「单参数规则」,并且理解什么时间调用 slurpy 函数很重要:

grab(1, 2);      # grab 1 grab 2
grab((1, 2));    # grab 1 grab 2
grab($(1, 2));   # grab 1 2
grab((1, 2), 3); # grab 1 2 grab 3

这也使得用户请求的展平感觉一致,无论有没有子列表,或很多

grab(flat (1, 2), (3, 4));   # grab 1 grab 2 grab 3 grab 4
grab(flat $(1, 2), $(3, 4)); # grab 1 2 grab 3 4
grab(flat (1, 2));           # grab 1 grab 2
grab(flat $(1, 2));          # grab 1 2

值得注意的是,在这些情况下将绑定和无符号变量混合在一起需要一点技巧,因为在绑定期间没有使用 Scalar 中间人。

my $a = (1, 2);  # Normal assignment, equivalent to $(1, 2)
grab($a);       # grab 1 2
my $b := (1, 2); # Binding, $b links directly to a bare (1, 2)
grab($b);       # grab 1 grab 2
my \c = (1, 2);  # Sigilless variables always bind, even with '='
grab(c);        # grab 1 grab 2

函数是一等对象

函数和其他代码对象可以作为值传递,就像任何其他对象一样。

有几种方法来获取代码对象。 您可以在声明点将其赋值给变量:

my $square = sub (Numeric $x) { $x * $x }
# and then use it:
say $square(6);    # 36

或者,您可以通过使用它前面的 & 来引用现有的具名函数。

sub square($x) { $x * $x };

# get hold of a reference to the function:
my $func = &square

这对于高阶函数非常有用,即,将其他函数作为输入的函数。 一个简单高阶函数的是 map,它对每个输入元素应用一个函数:

sub square($x) { $x * $x };
my @squared = map &square,  1..5;
say join ', ', @squared;        # 1, 4, 9, 16, 25

中缀形式

要像中缀运算符那样调用具有2个参数的子例程,请使用由 [] 包围的子例程引用。

sub plus { $^a + $^b };
say 21 [&plus] 21;
# OUTPUT«42␤»

闭包

Raku 中的所有代码对象都是闭包,这意味着它们可以从外部作用域引用词法变量。

sub generate-sub($x) {
    my $y = 2 * $x;
    return sub { say $y };
    #      ^^^^^^^^^^^^^^  inner sub, uses $y
}
my $generated = generate-sub(21);
$generated(); # 42

这里 $ygenerate-sub 中的词法变量,并且返回的内部子例程使用了 $y。 到内部 sub 被调用时,generate-sub 已经退出。 然而内部 sub 仍然可以使用 $y,因为它关闭了变量。

一个不太明显但有用的闭包示例是使用 map 乘以数字列表:

my $multiply-by = 5;
say join ', ', map { $_ * $multiply-by }, 1..5;     # 5, 10, 15, 20, 25

这里传递给 map 的块从外部作用域引用变量 $multiply-by,使块成为闭包。

没有闭包的语言不能轻易地提供高阶函数,它们像 map 一样易于使用和强大。

Routines

例程是遵守 Routine 类型的代码对象,最明显的是 Sub方法正则表达式Submethod

他们携带除了提供的额外的功能:他们可以作为 multis,你可以包装它们,并使用 return 提前退出:

my $keywords = set <if for unless while>;

sub has-keyword(*@words) {
    for @words -> $word {
        return True if $word (elem) $keywords;
    }
    False;
}

say has-keyword 'not', 'one', 'here';       # False
say has-keyword 'but', 'here', 'for';       # True

这里 return 不仅仅是将离开它所调用的块的内部,而是离开整个程序。 一般来说,块对于 return 是透明的,它们附加到外部程序。

例程(Routines)可以是内联的,并且因此为包装设置了障碍。 使用指令 use soft; 以防止内联在运行时允许包装。

sub testee(Int $i, Str $s){
    rand.Rat * $i ~ $s;
}

sub wrap-to-debug(&c){
    say "wrapping {&c.name} with arguments {&c.signature.perl}";
    &c.wrap: sub (|args){
        note "calling {&c.name} with {args.gist}";
        my \ret-val := callwith(|args);
        note "returned from {&c.name} with return value {ret-val.perl}";
        ret-val
    }
}

my $testee-handler = wrap-to-debug(&testee);
# OUTPUT«wrapping testee with arguments :(Int $i, Str $s)»

say testee(10, "ten");
# OUTPUT«calling testee with \(10, "ten")␤returned from testee with return value "6.151190ten"␤6.151190ten»
&testee.unwrap($testee-handler);
say testee(10, "ten");
# OUTPUT«6.151190ten␤»

定义操作符

操作符只是有趣名字的子例程。 有趣的名称由类别名称(中缀,前缀,后缀,环缀,后环缀)组成,后面跟着冒号,以及一个或多个操作符名称的列表(在环缀和后环缀的情况下为两个组件)。

这既适用于向现有运算符添加多个候选项,也适用于定义新的运算符。 在后一种情况下,新子例程的定义自动将新运算符安装到 语法(grammar)中,但仅在当前词法作用域中。 通过 useimport 导入操作符也使其可用。

# adding a multi candidate to an existing operator:
multi infix:<+>(Int $x, "same") { 2 * $x };
say 21 + "same";            # 42

# 定义一个新的操作符
sub postfix:<!>(Int $x where { $x >= 0 }) { [*] 1..$x };
say 6!;                     # 720

运算符声明变得尽快可用,因此您甚至可以递归到刚才定义的运算符中,如果您真的想要:

sub postfix:<!>(Int $x where { $x >= 0 }) {
    $x == 0 ?? 1 !! $x * ($x - 1)!
}
say 6!;                     # 720

环缀和后环缀操作符由两个分隔符组成,一个开口和一个闭合。

sub circumfix:<START END>(*@elems) {
    "start", @elems, "end"
}

say START 'a', 'b', 'c' END;        # start a b c end

后环缀也接收这个术语,在它们被作为参数解析之后:

sub postcircumfix:<!! !!>($left, $inside) {
    "$left -> ( $inside )"
}
say 42!! 1 !!;      # 42 -> ( 1 )

块可以直接赋值给操作符名。 使用变量声明符,并在操作符名前加上一个 & 符号。

my &infix:<ieq> = -> |l { [eq] l>>.fc };
say "abc" ieq "Abc";
# OUTPUT«True␤»

优先级

Raku 中的运算符优先级相对于现有运算符指定。 is tighteris equivis looser 特性能使用一个运算符提供,新的运算符优先级与之相关。 可以应用更多的特征。

例如,infix:<*> 的优先级高于 infix:<+>,并且在中间挤压一个像这样:

sub infix:<!!>($a, $b) is tighter(&infix:<+>) {
    2 * ($a + $b)
}

say 1 + 2 * 3 !! 4;     # 21

这里 1 + 2 * 3 !! 4 被解析为 1 + ((2 * 3) !! 4),因为新的 !! 运算符的优先级在 +* 之间。

可以使用下面的代码实现相同的效果:

sub infix:<!!>($a,$b) is looser(&infix:<x>) { ... }

要将新运算符置于与现有运算符相同的优先级别上,请使用 is equiv(&other-operator)

结合性

当同一个操作符在一行中连续出现多次时,有多种可能的解释。 例如

1 + 2 + 3

能被解析为

(1 + 2) + 3 # 左结合性

或者解析为

1 + (2 + 3) # 右结合性

对于实数的加法,区别有点模糊,因为 +数学上相关的

但对其他运算符来说它很重要。 例如对于指数/幂运算符,infix:<**>

say 2 ** (2 ** 3);      # 256
say (2 ** 2) ** 3;      # 64

Raku 拥有以下可能的结合性配置:

A Assoc Meaning of $a ! $b ! $c
L left ($a ! $b) ! $c
R right $a ! ($b ! $c)
N non ILLEGAL
C chain ($a ! $b) and ($b ! $c)
X list infix:<!>($a; $b; $c)

您可以使用 is assoc trait 指定运算符的结合性,其中 left 是默认的结合性。

sub infix:<§>(*@a) is assoc<list> {
    '(' ~ @a.join('|') ~ ')';
}

say 1 § 2 § 3;      # (1|2|3)

Traits

特性(traits)是在编译时运行以修改类型,变量,例程,属性或其他语言对象的行为的子例程。

traits 的例子有:

class ChildClass is ParentClass { ... }
#                ^^ trait, with argument ParentClass
has $.attrib is rw;
#            ^^^^^  trait with name 'rw'
class SomeClass does AnotherRole { ... }
#               ^^^^ trait
has $!another-attribute handles <close>;
#                       ^^^^^^^ trait

还有之前章节中的 is tighteris looseris equivis assoc 等。

Traits 是 trait_mod<VERB> 形式的 subs, 其中 VERB 代表像 isdoeshandles 那样的名字。它接受修改后的东西作为参数, 还有名字作为具名参数。

multi sub trait_mod:<is>(Routine $r, :$doubles!) {
    $r.wrap({
        2 * callsame;
    });
}

sub square($x) is doubles {
    $x * $x;
}

say square 3;       # 18

请参阅内置常规性状文档的类型例程

重新分派

在某些情况下,例程可能想从链中调用下一个方法。 这个链可以是类层次结构中的父类的列表,或者它可以是来自多分派的较不具体的 multi 候选者,或者它可以是来自wrap的内部例程。

在所有这些情况下,您可以使用 callwith 通过您自己选择的参数调用链中的下一个例程。

multi a(Any $x) {
    say "Any $x";
    return 5;
}
multi a(Int $x) {
    say "Int $x";
    my $res = callwith($x + 1);
    say "Back in Int with $res";
}

a 1;
# OUTPUT:
# Int 1
# Any 2
# Back in Int with 5

这里,a 1 首先调用最具体的 Int 候选者,并且 callwith 重新调度到较不具体的 Any 候选者。

通常,重新分派传递和调用者接收到的相同的参数,因此有一个特殊的例程:callsame

multi a(Any $x) {
    say "Any $x";
    return 5;
}
multi a(Int $x) {
    say "Int $x";
    my $res = callsame;
    say "Back in Int with $res";
}

a 1;        # Int 1\n Any 1\n Back in Int with 5

另一个常见的用例是重新分派到链中的下一个例程,之后不执行任何其他操作。 这就是为什么我们有 nextwithnextsame,它使用任意的参数调用下一个例程(nextwith)或与调用者接收(nextsame)相同的参数,但不会返回给调用者。 或者对其进行不同的措辞,nextsamenextwith 变体用下一个候选项替换当前的调用帧(callframe)。

multi a(Any $x) {
    say "Any $x";
    return 5;
}
multi a(Int $x) {
    say "Int $x";
    nextsame;
    say "back in a";    # never executed, because 'nextsame' doesn't return
}

a 1;        # Int 1\n Any 1

如前所述,multi sub 不是唯一能在 call,call me,nextwith 和 next 中有帮助的情况。 下面是是调度到包装的例程:

# enable wrapping:
use soft;

# function to be wrapped:
sub square-root($x) { $x.sqrt }

&square-root.wrap(sub ($num) {
   nextsame if $num >= 0;
   1i * callwith(abs($num));
});

say square-root(4);     # 2
say square-root(-4);    # 0+2i

最后一个用例是从父类中重分派给方法。

class LoggedVersion is Version {
    method new(|c) {
        note "New version object created with arguments " ~ c.perl;
        nextsame;
    }
}

say LoggedVersion.new('1.0.2');

如果你需要对被包装的代码进行多次调用或获得一个引用,例如内省它,你可以使用 nextcallee

sub power-it($x) { $x * $x }
sub run-it-again-and-again($x) {
    my &again = nextcallee;
    again again $x;
}

&power-it.wrap(&run-it-again-and-again);
say power-it(5);    # 625

强制类型

强制类型可以帮助您在例程中拥有特定类型,但接受更宽的输入。 当调用例程时,参数将自动转换为较窄的类型。

sub double(Int(Cool) $x) {
    2 * $x
}

say double '21'; # 42
say double Any;  # Type check failed in binding $x; expected 'Cool' but got 'Any'

这里的 Int 是参数将被强制的目标类型,而 Cool 是例程接受的作为输入的类型。

如果接受的输入类型为 Any,则可以将 Int(Any) 缩写为 Int()

强制只需查找与目标类型具有相同名称的方法即可。 所以你可以为你自己的类型定义强制,像这样:

class Bar {...}

class Foo {
   has $.msg = "I'm a foo!";

   method Bar {
       Bar.new(:msg($.msg ~ ' But I am now Bar.'));
   }
}

class Bar {
   has $.msg;
}

sub print-bar(Bar() $bar) {
   say $bar.WHAT; # (Bar)
   say $bar.msg;  # I'm a foo! But I am now Bar.
}

print-bar Foo.new;

强制类型应该在类型工作的任何地方工作,但 Rakudo 当前(2015.02)仅针对子例程参数实现了它们。

sub MAIN

具有特殊名称 MAIN 的 sub 在所有相关 parsers 之后执行,并且其签名是可以解析命令行参数的装置。 支持 multi 方法,如果未提供命令行参数,则会自动生成并显示使用方法。 所有命令行参数在 @*ARGS 中也可用,它可以在被 MAIN 处理之前进行变换。

MAIN 的返回值被忽略。 要提供除 0 以外的退出代码,请调用 exit

sub MAIN( Int :$length = 24,
           :file($data) where { .IO.f // die "file not found in $*CWD" } = 'file.dat',
           Bool :$verbose )
{
    say $length if $length.defined;
    say $data   if $data.defined;
    say 'Verbosity ', ($verbose ?? 'on' !! 'off');

    exit 1;
}

sub USAGE

如果对于给定的命令行参数没有找到 MAIN 的多个候选者,则调用 sub USAGE。 如果没有找到此类方法,则输出生成的使用消息。

sub MAIN(Int $i){ say $i == 42 ?? 'answer' !! 'dunno' }

sub USAGE(){
print Q:c:to/EOH/;
Usage: {$*PROGRAM-NAME} [number]

Prints the answer or 'dunno'.
EOH
}