例程是 Raku 重用代码的一种方式。它们有几种形式,最显著的是 Method,它属于类和角色,并与对象相关联;以及函数(也称为子例程Sub,简称),它可以独立于对象调用。

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

子程序可以有一个Signature,也称为参数列表,它指定签名期望哪些(如果有)参数。它可以指定(或保留)参数的数量和类型以及返回值。

对子程序的内省通过Routine提供。

定义/创建/使用函数§

子程序§

创建子程序的基本方法是使用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 添加宏后变得更有用。

要使子程序接受参数,Signature位于子程序名称与其主体之间,用括号括起来

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?'# OUTPUT: «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?' # OUTPUT: «foo\x[23]bar\x[3F]␤» 
}
 
# Back to original escape function 
say escape 'foo#bar?'# OUTPUT: «foo\#bar\?␤» 

子程序不必命名。如果未命名,它们被称为匿名子程序。

say sub ($a$b{ $a ** 2 + $b ** 2 }(34# OUTPUT: «25␤»

但在这种情况下,通常希望使用更简洁的Block语法。子程序和块可以在适当的位置调用,如上面的示例所示。

say -> $a$b { $a ** 2 + $b ** 2 }(34)    # OUTPUT: «25␤»

或者甚至

say { $^a ** 2 + $^b ** 2 }(34)            # OUTPUT: «25␤»

块和 lambda 表达式§

无论何时看到类似{ $_ + 42 }-> $a, $b { $a ** $b }{ $^text.indent($:spaces) } 的内容,那就是Block语法;-> 也被认为是块的一部分。诸如ifforwhile 之类的语句后面跟着这种类型的块。

for 1234 -> $a$b {
    say $a ~ $b;
}
# OUTPUT: «12␤34␤»

它们也可以作为匿名代码块独立使用。

say { $^a ** 2 + $^b ** 2}(34# OUTPUT: «25␤»

请注意,这意味着,尽管诸如if 之类的语句没有定义主题变量,但实际上它们可以定义

my $foo = 33;
if $foo ** 33 -> $a {
    say "$a is not null"# 
} # OUTPUT: «129110040087761027839616029934664535539337183380513 is not null␤» 

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

签名§

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

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

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

自动签名§

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

sub s { say @_%_ };
say &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), 45);       # pass only 42 to g()

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

sub f(|c){};
f :named(35);     # A named argument (in "adverb" form) 
f named => 35;    # Also a named argument 
f :35named;       # A named argument using abbreviated adverb form 
f 'named' => 35;  # Not a named argument, a Pair in a positional argument 
f 'hi':1x, :2y; # Positional and named arguments 
my \c = <a b c>.Capture;
f |c;             # Merge the contents of Capture $c as if they were supplied

传递给函数的参数在概念上首先被收集到一个Capture容器中。有关这些容器的语法和用法的详细信息,请参阅Capture的文档。

使用命名参数时,请注意,正常的列表“配对链接”允许在命名参数之间省略逗号。

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

如果也传递了位置参数,那么它们必须放在函数名称后面的括号内,或者在缩写副词形式中链接命名参数时,必须保留最后一个位置参数后的逗号。链接的命名参数和位置参数列表之间的空格是可选的。

sub p($x$y:$translate:$rotate{};
p(11:translate:rotate); # normal way 
p 11:translate:rotate;  # also normal way 
 
p(11:translate  :rotate;  # parentheses + chained named arguments 
p(11:translate:rotate;
p(11):translate:rotate;
 
p 11:translate  :rotate;  # dangling comma + chained named arguments 
p 11:translate:rotate;
p 11,:translate:rotate;

返回值§

任何 BlockRoutine 将提供其最后一个表达式的值作为返回值给调用者。如果调用了 returnreturn-rw,则其参数(如果有)将成为返回值。默认返回值为 Nil

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

多个返回值作为列表返回,或者通过创建 Capture 返回。可以使用解构来解开多个返回值。

sub a { 42'answer' };
put a.raku;
# OUTPUT: «(42, "answer")␤» 
 
my ($n$s= a;
put [$s$n];
# OUTPUT: «answer 42␤» 
 
sub b { <a b c>.Capture };
put b.raku;
# OUTPUT: «\("a", "b", "c")␤»

返回值类型约束§

Raku 有多种方法来指定函数的返回值类型

sub foo(--> Int)      {}say &foo.returns# OUTPUT: «(Int)␤» 
sub foo() returns Int {}say &foo.returns# OUTPUT: «(Int)␤» 
sub foo() of Int      {}say &foo.returns# OUTPUT: «(Int)␤» 
my Int sub foo()      {}say &foo.returns# OUTPUT: «(Int)␤» 

尝试返回其他类型的返回值会导致编译错误。

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

returnsof 是等效的,它们都只接受一个类型,因为它们声明的是 Callable 的特征。最后一个声明实际上是一个类型声明,显然只能接受一个类型。另一方面,--> 可以接受未定义的值或确定性的值。

请注意,NilFailure 不受返回值类型约束的限制,可以从任何例程中返回,无论其约束如何。

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

多重分派§

Raku 允许编写多个具有相同名称但签名不同的例程。当例程按名称调用时,运行时环境会确定合适的候选者并调用它。

每个候选者都用 multi 关键字声明。分派根据参数 元数(数量)、类型和名称进行;在某些情况下,还根据多重声明的顺序进行。请考虑以下示例

# version 1 
multi happy-birthday$name ) {
    say "Happy Birthday $name !";
}
 
# version 2 
multi happy-birthday$name$age ) {
    say "Happy {$age}th Birthday $name !";
}
 
# version 3 
multi happy-birthday:$name:$age:$title  = 'Mr' ) {
    say "Happy {$age}th Birthday $title $name !";
}
 
 
# calls version 1 (arity) 
happy-birthday 'Larry';                        # OUTPUT: «Happy Birthday Larry !␤» 
# calls version 2 (arity) 
happy-birthday 'Luca'40;                     # OUTPUT: «Happy 40th Birthday Luca !␤» 
# calls version 3 
# (named arguments win against arity) 
happy-birthdayage => '50'name => 'John' ); # OUTPUT: «Happy 50th Birthday Mr John !␤» 
# calls version 2 (arity) 
happy-birthday'Jack'25 );                  # OUTPUT: «Happy 25th Birthday Jack !␤» 

happy-birthday 子例程的前两个版本仅在元数(参数数量)上有所不同,而第三个版本使用命名参数,只有在使用命名参数时才会被选中,即使其元数与另一个 multi 候选者的元数相同。

当两个子例程具有相同的元数时,参数的类型会驱动分派;当存在命名参数时,即使它们的类型与另一个候选者的类型相同,它们也会驱动分派。

multi happy-birthdayStr $nameInt $age ) {
    say "Happy {$age}th Birthday $name !";
}
 
multi happy-birthdayStr $nameStr $title ) {
    say "Happy Birthday $title $name !";
}
 
multi happy-birthdayStr :$nameInt :$age ) {
    say "Happy Birthday $name, you turned $age !";
}
 
happy-birthday 'Luca'40;                 # OUTPUT: «Happy 40th Birthday Luca !␤» 
happy-birthday 'Luca''Mr';               # OUTPUT: «Happy Birthday Mr Luca !␤» 
happy-birthday age => 40name => 'Luca';  # OUTPUT: «Happy Birthday Luca, you turned 40 !␤» 

命名参数参与分派,即使它们在调用中未提供。因此,具有命名参数的多重候选者将优先考虑。

有关类型约束的更多信息,请参阅有关 签名 字面量的文档。

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-jsonTrue );                        # OUTPUT: «true␤» 
say as-json10.3 );                        # OUTPUT: «10.3␤» 
say as-json( [ True10.3False24 ] );   # OUTPUT: «[true, 10.3, false, 24]␤»

对于某些签名差异(特别是当使用 where 子句或子集时),会使用多重方法或子例程的定义顺序,依次评估每个可能性。有关示例,请参阅下面的 按定义顺序进行多重解析

multi 在没有特定例程类型的情况下始终默认为 sub,但您也可以将其用于方法。候选者是对象的所有多重方法。

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('promotion','Cindy'); # OUTPUT: «Hooray for your promotion, Cindy␤» 
$congrats.congratulate('birthday','Bob');    # OUTPUT: «Happy birthday, Bob␤»

sub 不同,如果您在多重方法中使用命名参数,则参数必须是必需参数才能按预期工作。

请注意,非多重子例程或运算符将隐藏任何父作用域或子作用域中具有相同名称的多重候选者。对于导入的非多重候选者也是如此。

多重分派也可以作用于参数特征,具有 is rw 参数的例程优先于没有 is rw 参数的例程。

proto þoo (|) {*}
multi þoo$ðar is rw ) { $ðar = 42 }
multi þoo$ðar ) { $ðar + 42 }
my $bar = 7;
say þoo($bar); # OUTPUT: «42␤» 

proto§

proto 是一种正式声明 multi 候选者之间共性的方法。它充当一个包装器,可以验证但不能修改参数。请考虑这个基本示例

proto congratulate(Str $reasonStr $name|{*}
multi congratulate($reason$name{
   say "Hooray for your $reason$name";
}
multi congratulate($reason$nameInt $rank{
   say "Hooray for your $reason$name -- 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 

proto 坚持所有 multi congratulate 子例程都符合两个字符串的基本签名,可选地后跟其他参数。| 是一个未命名的 Capture 参数,允许 multi 接受其他参数。前两次调用成功,但第三次调用失败(在编译时),因为 42Str 不匹配。

say &congratulate.signature # OUTPUT: «(Str $reason, Str $name, | is raw)␤» 

您可以为 proto 提供一个函数体,并将 {*}(注意大括号内没有空格)放在您希望进行分派的位置。当您的例程中存在一个“漏洞”,导致其根据给定的参数表现出不同的行为时,这很有用。

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

由于 protomulti 候选者的包装器,因此例程的 multi 候选者的签名不必与 proto 的签名匹配;multi 候选者的参数可以是 proto 参数的子类型,并且 multi 候选者的返回值类型可能与 proto 的返回值类型完全不同。当为 proto 提供函数体时,使用这种不同的类型特别有用。

enum DebugType <LOG WARNING ERROR>;
 
#|[ Prints a message to stderr with a color-coded key. ] 
proto debug(DebugType:D $typeStr:D $message --> Bool:_{
    note sprintf qb/\e[1;%dm[%s]\e[0m %s/{*}$type.key$message
}
multi debug(LOG;; Str:D --> 32)     { }
multi debug(WARNING;; Str:D --> 33{ }
multi debug(ERROR;; Str:D --> 31)   { }

{*} 始终将参数传递给调用的候选函数。参数默认值和类型强制转换将生效,但不会被传递。

proto mistake-proto(Str() $strInt $number = 42{*}
multi mistake-proto($str$number{ say $str.^name }
mistake-proto(742);  # OUTPUT: «Int␤» -- not passed on 
mistake-proto('test'); # fails -- not passed on 

使用 proto 的方法的更长示例展示了如何将通用功能提取到 proto 方法中。

class NewClass {
    has $.debug is rw = False;
    has $.value is rw = 'Initial value';
    proto method handle| ) {
        note "before value is 「$.value" if $.debug;
        {*}
        note "after value is 「$.value" if $.debug;
    }
    multi method handle(Str $s{
        $.value = $s;
        say 'in string'
    }
    multi method handle(Positional $s{
        $.value = $s[0];
        say 'in positional'
    }
    multi method handle$a$b ) {
        $.value = "$a is looking askance at $b";
        say 'with more than one value'
    }
}
my NewClass $x .= new;
$x.handle('hello world');
$x.handle(<hello world>);
$x.debug = True;
$x.handle('hello world');
$x.handle(<hello world>);
$x.handle('Claire''John');
# OUTPUT: 
# in string 
# in positional 
# before value is 「hello」 
# in string 
# after value is 「hello world」 
# before value is 「hello world」 
# in positional 
# after value is 「hello」 
# before value is 「hello」 
# with more than one value 
# after value is 「Claire is looking askance at John」 

only§

submethod 之前使用 only 关键字表示它将是给定命名空间中唯一具有该名称的函数。

only sub you () {"Can make all the world seem right"};

这将使同一命名空间中的其他声明(例如)

sub you ( $can ) { "Make the darkness bright" }

引发类型为 X::Redeclaration 的异常。only 是所有子例程的默认值;在上面的情况下,不将第一个子例程声明为 only 将产生完全相同的错误;但是,没有什么可以阻止未来的开发人员声明一个 proto 并用 multi 作为名称前缀。在例程之前使用 only 是一个 防御性编程 功能,它声明了将来不会在同一命名空间中声明具有相同名称的例程的意图。

(exit code 1)
===SORRY!=== Error while compiling /tmp/only-redeclaration.raku
Redeclaration of routine 'you' (did you mean to declare a multi-sub?)
at /tmp/only-redeclaration.raku:3
------> <BOL>⏏<EOL>

匿名子例程不能声明为 onlyonly sub {} 将抛出类型为 X::Anon::Multi 的错误,这令人惊讶。

按定义顺序的多重解析§

当按参数类型进行细分不足以找到明确的匹配项时,会有一些不同的 tie breaker,它们可以按方法或子例程的声明顺序进行评估:这些包括 where 子句和子集、命名参数和签名解包。

在此代码示例中,两个多重子例程仅通过 where 子句区分,其中有一个可能通过其中任何一个的模糊情况,即值 4。在这种情况下,哪个多重子例程先定义,哪个就获胜。

{
  multi check_range ( Int $n where {$_ > 3} ) {
      return "over 3";
  };
  multi check_range ( Int $n where {$_ < 5} ) {
      return "under 5";
  };
  say check_range(4);  # OUTPUT: over 3 
}
{
  multi check_range ( Int $n where {$_ < 5} ) {
      return "under 5";
  };
  multi check_range ( Int $n where {$_ > 3} ) {
      return "over 3";
  };
  say check_range(4);  # OUTPUT: under 5 
}

在以下示例中,使用三个子集将字符串限制为某些允许的值,其中三个子集之间存在重叠。

subset Monster  of Str where { $_ eq any( <godzilla gammera ghidora> ) };
subset Hero     of Str where { $_ eq any( <godzilla ultraman inframan> ) };
subset Knockoff of Str where { $_ eq any( <gammera inframan thorndike> ) };
 
{
    multi speak (Monster $name{
        say "The monster, $name roars!";
    }
    multi speak (Hero $name{
        say "The hero, $name shouts!";
    }
    multi speak (Knockoff $name{
        say "The knockoff, $name, slinks away...";
    }
    speak('godzilla');     # OUTPUT: The monster, godzilla roars! 
    speak('gammera');      # OUTPUT: The monster, gammera roars! 
    speak('inframan');     # OUTPUT: The hero, inframan shouts! 
}

请注意,这里“godzilla”被视为 Monster,而不是 Hero,因为 Monster 多重子例程排在前面;“gammera”或“inframan”均不被视为 Knockoff,因为该多重子例程排在最后。

应该注意的是,定义顺序是 Raku 看到它们的顺序,如果例如多重子例程是从不同的模块导入的,则可能难以辨别。随着代码库组织变得更加复杂,对象类可能比使用子集作为类型更具可扩展性,如本示例所示。

约定和习惯用法§

虽然上面描述的调度系统提供了很大的灵活性,但大多数内部函数以及许多模块中的函数都会遵循一些约定。

贪婪约定§

也许这些约定中最重要的是处理贪婪列表参数的方式。大多数情况下,函数不会自动扁平化贪婪列表。少数例外是那些在列表列表上没有合理行为的函数(例如,chrs)或与既定习惯用法冲突的函数(例如,poppush 的逆运算)。

如果您希望匹配这种外观和感觉,任何 Iterable 参数都必须使用 **@ 贪婪逐个元素分解,有两个细微差别。

这可以通过使用带有 ++@ 而不是 **@ 的贪婪来实现。

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

这是对以下内容的简写,它非常接近

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

这会导致以下行为,称为“单参数规则”,在调用贪婪函数时了解这一点很重要。

grab(12);      # OUTPUT: «grab 1␤grab 2␤» 
grab((12));    # OUTPUT: «grab 1␤grab 2␤» 
grab($(12));   # OUTPUT: «grab 1 2␤» 
grab((12), 3); # OUTPUT: «grab 1 2␤grab 3␤» 

这也使用户请求的扁平化在存在一个子列表或多个子列表的情况下感觉一致。

grab(flat (12), (34));   # OUTPUT: «grab 1␤grab 2␤grab 3␤grab 4␤» 
grab(flat $(12), $(34)); # OUTPUT: «grab 1 2␤grab 3 4␤» 
grab(flat (12));           # OUTPUT: «grab 1␤grab 2␤» 
grab(flat $(12));          # OUTPUT: «grab 1␤grab 2␤» 

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

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

有关如何使用遵循单参数规则的例程的更多示例,请参阅 list 子例程的文档

函数是一等公民§

函数和其他代码对象可以像其他任何对象一样传递。

有几种方法可以获取代码对象。您可以在声明时将其分配给一个变量

my $square = sub (Numeric $x{ $x * $x }
# and then use it: 
say $square(6);    # OUTPUT: «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;        # OUTPUT: «1, 4, 9, 16, 25␤»

您可以对运算符使用相同的方法,只是在这种情况下,必须使用使用 infix: 声明的名称

my $exp := &infix:<**>say $exp(7,3); # OUTPUT: «343␤»

即使在运算符自动生成的情况下也可以这样做,例如在这种情况下,XX 是应用于 X 运算符的元运算符 X

my $XX := &infix:<XX>say $XX( [1,(2,3)] , [(4,5),6]  );
# OUTPUT: «(((1 (4 5))) ((1 6)) (((2 3) (4 5))) (((2 3) 6)))␤»

基本原则是,在运算符的情况下,您实际上不必担心它们的实际定义方式,只需使用 &infix< > 获取指向它们的指针即可。

中缀形式§

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

sub plus { $^a + $^b };
say 21 [&plus21;
# 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(); # OUTPUT: «42␤»

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

另一个闭包示例是使用 map 来乘以数字列表

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

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

没有闭包的语言无法轻松提供像 map 一样易于使用且功能强大的高阶函数。

例程§

例程是符合 Routine 类型(最显著的是 SubMethodRegexSubmethod)的代码对象。

除了 Block 提供的功能外,它们还具有额外的功能:它们可以作为 多重方法,您可以 包装 它们,并使用 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';       # OUTPUT: «False␤» 
say has-keyword 'but''here''for';       # OUTPUT: «True␤»

这里,return 不仅离开它被调用的代码块,还离开整个例程。一般来说,代码块对 return 是透明的,它们附加到最外层的例程。

例程可以内联,因此为包装提供了障碍。使用 use soft; 编译指示来防止内联,以便在运行时允许包装。

sub testee(Int $iStr $s){
    rand.Rat * $i ~ $s;
}
 
sub wrap-to-debug(&c){
    say "wrapping {&c.name} with arguments {&c.signature.raku}";
    &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.raku}";
        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␤»

定义运算符§

运算符只是具有奇怪名称的子例程。奇怪的名称由类别名称(infixprefixpostfixcircumfixpostcircumfix)组成,后面跟着一个冒号,以及运算符名称列表(在 circumfix 和 postcircumfix 的情况下为两个组件)。所有这些运算符及其含义的扩展解释包含在 此表中

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

# adding a multi candidate to an existing operator: 
multi infix:<+>(Int $x"same"{ 2 * $x };
say 21 + "same";            # OUTPUT: «42␤» 
 
# defining a new operator 
sub postfix:<!>(Int $x where { $x >= 0 }{ [*1..$x };
say 6!;                     # OUTPUT: «720␤» 

运算符声明尽快生效,因此您可以递归到刚刚定义的运算符中

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

Circumfix 和 postcircumfix 运算符由两个分隔符组成,一个开头和一个结尾。

sub circumfix:<START END>(*@elems{
    "start"@elems"end"
}
 
say START 'a''b''c' END;        # OUTPUT: «(start [a b c] end)␤» 

后缀词也接收它们被解析为参数的术语

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

块可以直接分配给运算符名称。使用变量声明符,并在运算符名称前加上&符号。

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

优先级§

Raku 中的运算符优先级是相对于现有运算符指定的。is tighteris equivis looser 特性可以与运算符一起提供,以指示新运算符的优先级与其他现有运算符的关系。可以应用多个特性。

例如,infix:<*> 的优先级高于 infix:<+>,将一个运算符挤在两者之间,效果如下

sub infix:<!!>($a$bis tighter(&infix:<+>{
    2 * ($a + $b)
}
 
say 1 + 2 * 3 !! 4;     # OUTPUT: «21␤» 

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

同样的效果也可以通过以下方式实现

sub infix:<!!>($a$bis looser(&infix:<*>{ ... }

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

结合性§

当同一个运算符连续出现多次时,会有多种可能的解释。例如

1 + 2 + 3

可以解析为

(1 + 2+ 3         # left associative

或者

1 + (2 + 3)         # right associative

对于实数的加法,这种区别有点无关紧要,因为 +数学上的结合性

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

say 2 ** (2 ** 3);      # OUTPUT: «256␤» 
say (2 ** 2** 3;      # OUTPUT: «64␤»

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

结合性$a ! $b ! $c 的含义
($a ! $b) ! $c
$a ! ($b ! $c)
非法
($a ! $b) 和 ($b ! $c)
列表infix:<!>($a; $b; $c)

有关运算符结合性的更多详细信息,请参阅 运算符文档

可以使用 is assoc 特性指定中缀运算符的结合性,其中 left 是默认的结合性。指定非中缀运算符的结合性正在计划中,但尚未实现。

sub infix:<§>(*@ais assoc<list> {
    '(' ~ @a.join('|'~ ')';
}
 
say 1 § 2 § 3;      # OUTPUT: «(1|2|3)␤» 

特性§

特性是编译时运行的子例程,它们修改类型、变量、例程、属性或其他语言对象的行为。

特性的例子有

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

特性是以 trait_mod<VERB> 的形式声明的子例程,其中 VERB 代表名称,如 isdoeshandles。它接收修改后的内容作为参数,以及名称作为命名参数。有关详细信息,请参阅 Sub

multi trait_mod:<is>(Routine $r:$doubles!{
    $r.wrap({
        2 * callsame;
    });
}
 
sub square($xis doubles {
    $x * $x;
}
 
say square 3;       # OUTPUT: «18␤» 

有关内置例程特性的文档,请参阅 类型例程

重新调度§

在某些情况下,例程可能希望从链中调用下一个方法。此链可以是类层次结构中父类的列表,也可以是来自多重调度的不太具体的多个候选者,或者可以是来自 wrap 的内部例程。

幸运的是,我们有一系列重新调度工具,可以帮助我们轻松实现这一点。

sub callsame§

callsame 调用下一个匹配的候选者,使用与当前候选者相同的参数,并返回该候选者的返回值。

proto a(|) {*}
 
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;        # OUTPUT: «Int 1␤Any 1␤Back in Int with 5␤» 

sub callwith§

callwith 调用与原始签名匹配的下一个候选者,即下一个可能用于用户提供的参数的函数,并返回该候选者的返回值。

proto a(|) {*}
 
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 候选者。请注意,虽然我们的参数 $x + 1 是一个 Int,但我们仍然调用链中的下一个候选者。

例如,在这种情况下

proto how-many(|) {*}
 
multi how-manyAssociative $a ) {
    say "Associative $a ";
    my $calling = callwith1 => $a );
    return $calling;
}
 
multi how-manyPair $a ) {
    say "Pair $a ";
    return "There is $a "
 
}
 
multi how-manyHash $a ) {
    say "Hash $a";
    return "Hashing $a";
}
 
my $little-piggy = little => 'piggy';
say $little-piggy.^name;        # OUTPUT: «Pair␤» 
say &how-many.cando( \( $little-piggy ));
# OUTPUT: «(sub how-many (Pair $a) { #`(Sub|68970512) ... } sub how-many (Associative $a) { #`(Sub|68970664) ... })␤» 
say how-many$little-piggy  ); # OUTPUT: «Pair little     piggy␤There is little piggy␤» 

唯一接受用户提供的 Pair 参数的候选者是首先定义的两个函数。虽然一个 Pair 可以很容易地强制转换为 Hash,但以下是签名匹配的方式

say :Pair ) ~~ :Associative ); # OUTPUT: «True␤» 
say :Pair ) ~~ :Hash );        # OUTPUT: «False␤» 

我们提供的参数是 Pair。它不匹配 Hash,因此相应的函数不包含在候选者列表中,如 &how-many.cando( \( $little-piggy )); 的输出所示。

sub nextsame§

nextsame 使用与当前候选者相同的参数调用下一个匹配的候选者,并且 **从不** 返回。

proto a(|) {*}
 
multi a(Any $x{
    say "Any $x";
    return 5;
}
multi a(Int $x{
    say "Int $x";
    nextsame;
    say "never executed because nextsame doesn't return";
}
 
a 1;        # OUTPUT: «Int 1␤Any 1␤» 

sub nextwith§

nextwith 使用用户提供的参数调用下一个匹配的候选者,并且 **从不** 返回。

proto a(|) {*}
 
multi a(Any $x{
    say "Any $x";
    return 5;
}
multi a(Int $x{
    say "Int $x";
    nextwith($x + 1);
    say "never executed because nextwith doesn't return";
}
 
a 1;        # OUTPUT: «Int 1␤Any 2␤» 

sub samewith§

samewith 使用调用点提供的参数再次调用多重,并返回调用提供的的值。这可用于自递归。

proto factorial(|) {*}
 
multi factorial(Int $x where * ≤ 1{ 1 }
multi factorial(Int $x{
    $x * samewith($x-1);
}
 
say (factorial 10); # OUTPUT: «36288000␤» 

sub nextcallee§

可能需要重新调度以调用不是当前范围的块,该块为 nextsame 及其朋友提供了引用错误范围的问题。使用 nextcallee 来捕获正确的候选者并在需要时调用它。

proto pick-winner(|) {*}
 
multi pick-winner (Int \s{
    my &nextone = nextcallee;
    Promise.in(π²).then: { nextone s }
}
multi pick-winner { say "Woot! $^w won" }
 
with pick-winner ^5 .pick -> \result {
    say "And the winner is...";
    await result;
}
 
# OUTPUT: 
# And the winner is... 
# Woot! 3 won 

Int 候选者接受 nextcallee,然后启动一个 Promise 以在一段时间后并行执行,然后返回。我们不能在这里使用 nextsame,因为它会尝试 nextsame Promise 的块而不是我们原始的例程。

包装的例程§

除了上面提到的那些之外,重新调度在更多情况下很有用。例如,用于调度到包装的例程

# enable wrapping: 
use soft;
 
# function to be wrapped: 
sub square-root($x{ $x.sqrt }
 
&square-root.wrap(sub ($num{
   nextsame if $num >= 0;
   1* callwith(abs($num));
});
 
say square-root(4);     # OUTPUT: «2␤» 
say square-root(-4);    # OUTPUT: «0+2i␤» 

父类的例程§

另一个用例是重新调度到父类的方法。

say Version.new('1.0.2'# OUTPUT: v1.0.2 
class LoggedVersion is Version {
    method new(|c{
        note "New version object created with arguments " ~ c.raku;
        nextsame;
    }
}
 
say LoggedVersion.new('1.0.2');
 
# OUTPUT: 
# New version object created with arguments \("1.0.2") 
# v1.0.2 

强制类型§

强制类型为例程参数强制使用特定类型,同时允许例程本身接受更广泛的输入。调用时,参数会自动缩小到更严格的类型,因此在例程中,参数始终具有所需的类型。

如果参数无法转换为更严格的类型,则会抛出 *类型检查* 错误。

sub double(Int(Cool$x{
    2 * $x
}
 
say double '21';# OUTPUT: «42␤» 
say double  21# OUTPUT: «42␤» 
say double Any# Type check failed in binding $x; expected 'Cool' but got 'Any' 

在上面的示例中,Int 是参数 $x 将强制转换为的目标类型,而 Cool 是例程接受的更广泛的输入类型。

如果接受的更广泛的输入类型是 Any,则可以通过省略 Any 类型来缩写强制 Int(Any),从而得到 Int()

强制通过查找与目标类型同名的函数来工作:如果在参数上找到此类函数,则调用它以将后者转换为预期的窄类型。从上面可以清楚地看出,可以通过提供所需的函数来提供用户类型之间的强制

class Bar {
   has $.msg;
}
 
class Foo {
   has $.msg = "I'm a foo!";
 
   # allows coercion from Foo to Bar 
   method Bar {
       Bar.new(:msg($.msg ~ ' But I am now Bar.'));
   }
}
 
# wants a Bar, but accepts Any 
sub print-bar(Bar() $bar{
   say $bar.^name# OUTPUT: «Bar␤» 
   say $bar.msg;   # OUTPUT: «I'm a foo! But I am now Bar.␤» 
}
 
print-bar Foo.new;

在上面的代码中,一旦将 Foo 实例作为参数传递给 print-bar,就会调用 Foo.Bar 函数,并将结果放入 $bar 中。

强制类型应该在类型起作用的任何地方起作用,但 Rakudo 目前(2018.05)只在签名中实现它们,用于参数和返回类型。

强制转换也适用于返回值类型

sub are-equal (Int $xInt $y --> Bool(Int) ) { $x - $y };
 
for (2,4X (1,2-> ($a,$b{
    say "Are $a and $b equal? "are-equal($a$b);
} #  OUTPUT: «Are 2 and 1 equal? True␤Are 2 and 2 equal? False␤Are 4 and 1 equal? True␤Are 4 and 2 equal? True␤» 

在这种情况下,我们将一个 Int 强制转换为一个 Bool,然后在调用该函数的 for 循环中将其打印(放入字符串上下文)。

sub MAIN§

在 Raku 脚本中声明 sub MAIN 不是强制性的,但你可以提供一个来为你的脚本创建一个 命令行界面