Raku 官方文档

on

1. 通过例子学习 Raku

假设你举办了一场乒乓球锦标赛。裁判员以 Player1 Player2 | 3:2 的格式告诉你每场比赛的结果, 这意味着 Player1 以 3:2 的比分战胜了 Player2。你需要一个脚本来总结每个选手赢了多少场比赛和多少局, 以确定总冠军。

输入数据(存储在一个名为 scores.txt 的文件中)如下所示:

Beth Ana Charlie Dave
Ana Dave | 3:0
Charlie Beth | 3:1
Ana Beth | 2:3
Dave Charlie | 3:0
Ana Charlie | 3:1
Beth Dave | 0:3

第一行是选手名单。之后的每一行都记录了一场比赛的结果。

在 Raku 中, 有一种方法可以解决这个问题。

use v6;

# start by printing out the header.
say "Tournament Results:\n";

my $file  = open 'scores.txt'; # get filehandle and...
my @names = $file.get.words;   # ... get players.

my %matches;
my %sets;

for $file.lines -> $line {
    next unless $line; # ignore any empty lines

    my ($pairing, $result) = $line.split(' | ');
    my ($p1, $p2)          = $pairing.words;
    my ($r1, $r2)          = $result.split(':');

    %sets{$p1} += $r1;
    %sets{$p2} += $r2;

    if $r1 > $r2 {
        %matches{$p1}++;
    } else {
        %matches{$p2}++;
    }
}

my @sorted = @names.sort({ %sets{$_} }).sort({ %matches{$_} }).reverse;

for @sorted -> $n {
    my $match-noun = %matches{$n} == 1 ?? 'match' !! 'matches';
    my $set-noun   = %sets{$n} == 1 ?? 'set' !! 'sets';
    say "$n has won %matches{$n} $match-noun and %sets{$n} $set-noun";
}

这会产生输出:

Tournament Results:

Ana has won 2 matches and 8 sets
Dave has won 2 matches and 6 sets
Charlie has won 1 match and 4 sets
Beth has won 1 match and 4 sets

1.1. 指令 v6

每个 Raku 程序都应该以类似于 use v6; 的行开头。这一行告诉编译器程序所期望的 Raku 版本。例如, 6.c 就是一个 Raku 版本的例子。如果你不小心用 Perl 运行了这个文件, 你会得到一个有用的错误信息。

1.2. 语句

# start by printing out the header.
say "Tournament Results:\n";

一个 Raku 程序由零条或多条语句组成。语句的结尾是分号或花括号。

在 Raku 中, 单行注释以一个单一的散列字符 # 开始, 并延伸到行末。Raku 也支持多行/嵌入式注释。编译器不会将注释作为程序文本来计算, 而且它们只为人类读者服务。

1.3. 词法作用域和块

my $file = open 'scores.txt';

my 声明了一个词法变量, 这些变量只有在当前块中从声明点到块的末端可见。如果没有包围块, 那么它在整个文件的其余部分都是可见的(这实际上就是包围块)。代码块是指用花括号 { } 括起来的代码的任何部分。

1.3.1. 魔符和标识符

变量名的开头是一个魔符(sigil), 它是一个非字母数字符号, 如 $,@,%,或 & - 有时也用双冒号 ::。魔符(sigils)表明了变量的结构接口, 比如它是否应该被当作一个单一值、一个复合值、一个子程序等。魔符之后是一个标识符, 可以由字母、数字和下划线组成。在字母之间还可以使用破折号 - 或者撇号 ', 所以 isn’tdouble-click 是有效的标识符。

1.3.2. 标量

魔符(sigils)表示变量的默认访问方式, 带 @ 魔符的变量按位置访问, 带 % 魔符的变量按字符串键访问。然而, $ 魔符表示的是一个通用的标量容器, 它可以容纳任何单一的值, 并以任何方式进行访问。标量甚至可以包含一个像 ArrayHash 这样的复合对象;$ 魔符表示它应该被视为一个单一的值, 即使是在一个需要多个值的上下文中(像 ArrayHash 那样)。

1.3.3. 文件句柄和赋值

内置函数 open 打开一个文件, 这里名为 scores.txt, 并返回一个文件句柄—一个代表该文件的对象。赋值运算符 = 将该文件句柄赋值给左边的变量, 这意味着 $file 现在存储了该文件句柄。

1.3.4. 字符串字面量

'scores.txt' 是一个字符串字面量。字符串是一段文本, 字符串字面量是直接出现在程序中的字符串。在这一行中, 是提供给 open 的参数。

1.4. 数组、方法和调用

my @names = $file.get.words;

右侧在 $file 中存储的文件句柄上调用一个方法-一个命名的行为组-命名为 getget 方法从文件中读取并返回一行, 删除行尾。如果你在调用 get 后打印 $file 的内容, 你会发现第一行已经不在里面了。words 也是一个方法, 在 get 返回的字符串上调用。words 将它的调用者-它操作的字符串-分解成一个单词列表, 这里指的是用空格隔开的字符串。它将单个字符串 'Beth Ana Charlie Dave' 转化为字符串 'Beth'、'Ana'、'Charlie'、'Dave' 的列表。

最后, 这个列表被存储在数组 @names 中。@ 魔符标志着声明的变量是一个数组(Array)。数组存储的是有序列表。

1.5. 散列

my %matches;
my %sets;

这两行代码声明了两个散列值。% 魔符标志着每个变量都是一个 HashHash 是键值对的无序集合。其他编程语言称之为散列表、字典或映射。你可以用 %hash{$key} 来查询一个散列表, 寻找对应于某个 $key 的值。

在计分程序中, %matches 存储的是每个选手赢得的比赛数。%sets 存储了每个选手赢得的局数。这两个散列值都是以选手的名字为索引的。

1.6. for 和 block

for $file.lines -> $line {
    ...
}

for 产生一个循环, 对列表中的每一项运行一次花括号分隔的块, 将变量 $line 设置为每次迭代的当前值。$file.lines 产生一个从文件 scores.txt 中读取的行的列表, 从文件的第二行开始, 因为我们已经调用了一次 $file.get, 一直到文件的最后。

在第一次迭代时, $line 将包含字符串 Ana Dave | 3:0;在第二次迭代时, Charlie Beth | 3:1, 以此类推。

my ($pairing, $result) = $line.split(' | ');

my 可以同时声明多个变量。赋值的右侧是对名为 split 的方法的调用, 将字符串 ' | ' 作为参数传递。

split 将其调用者分解为一个字符串列表, 因此用分隔符 ' | ' 连接列表中的项就会产生原始字符串。

$pairing 得到返回列表中的第一项, $result 得到第二项。

在处理完第一行后, $pairing 将持有字符串 Ana Dave, $result 将持有 3:0

接下来的两行遵循同样的模式。

my ($p1, $p2) = $pairing.words;
my ($r1, $r2) = $result.split(':');

第一行提取并存储两个选手的名字放在变量 $p1$p2 中。第二行提取每个选手的结果, 并将其存储在 $r1$r2 中。

在处理完第一行文件后, 变量中包含了如下值。

变量

内容

$line

'Ana Dave | 3:0'

$pairing

'Ana Dave'

$result

'3:0'

$p1

'Ana'

$p2

'Dave'

$r1

'3'

$r2

'0'

然后, 程序会计算每个选手赢得的局数:

%sets{$p1} += $r1;
%sets{$p2} += $r2;

以上两个语句涉及 += 复合赋值运算符。它们是下面两个使用简单赋值运算符 = 的语句的变体, 乍一看可能更容易理解:

%sets{$p1} = %sets{$p1} + $r1;
%sets{$p2} = %sets{$p2} + $r2;

对于你的程序, 使用 += 复合赋值运算符的速记版本比使用简单赋值运算符 = 的普通写法版本更受欢迎。这不仅是因为短版本需要更少的输入, 而且还因为 += 运算符默默地将散列键值对儿的未定义值初始化为零。换句话说:通过使用 +=, 在有意义地加上 $r1 之前, 不需要包含一个单独的语句, 比如 %sets{$p1} = 0。我们将在下面更详细地研究这种行为。

1.6.1. Any 和 +=

%sets{$p1} += $r1;
%sets{$p2} += $r2;

%sets{$p1} += $r1 表示将左边变量的值增加 $r1。在第一次迭代中, %sets{$p1} 还没有被定义, 所以它默认为一个特殊的值 Any+= 运算符方便地将未定义的值 Any 视为一个值为 0 的数字, 允许它合理地加上一些其他值。为了执行加法, 字符串 $r1$r2 等会自动转换为数字, 因为加法是一种数值操作。

1.6.2. 胖箭头、对儿和 autovivification

在这两行执行之前, %sets 是空的。在散列中添加一个还没有的条目, 会导致该条目及时出现, 其值从零开始。这种行为被称为 autovivification。在这两行第一次运行后, %sets 包含 'Ana'⇒ 3, 'Dave'⇒ 0。(胖箭头 分隔了 Pair 中的键和值)。

1.6.3. 后自增和前自增

if $r1 > $r2 {
    %matches{$p1}++;
} else {
    %matches{$p2}++;
}

如果 $r1 在数值上大于 $r2, 则 %matches{$p1} 增加 1。如果 $r1 不大于 $r2, %matches{$p2} 增加 1。就像 += 的情况一样, 如果任何一个散列值之前不存在, 那么它就会被增量操作自动转换(autovivified)。

$thing++$thing += 1 的一个变体;它与后者的不同之处在于, 表达式的返回值是增量前的 $thing, 而不是增量后值。与许多其他编程语言一样, 你可以使用 ++ 作为前缀。然后它返回增量后的值:my $x = 1; say ++$x 打印 2。

1.7. 主题变量

my @sorted = @names.sort({ %sets{$_} }).sort({ %matches{$_} }).reverse;

这一行由三个单独的简单步骤组成。数组的 sort 方法会返回数组内容的排序版本。然而, 数组的默认排序是按其内容排序的。如果要按照胜者优先的顺序打印选手的名字, 代码必须按照选手的分数而不是他们的名字对数组进行排序。排序方法的参数是一个用于将数组元素(选手的名字)转换为数据的块, 通过这些数据进行排序。数组的项是通过主题变量 $_ 传递进来的。

1.7.1. 块

你以前见过块:for 循环 → $line { …​ }if 语句都是在块上工作的。块是一段自足的 Raku 代码, 有一个可选的签名(→ $line 部分)。

最简单的方法是用 @names.sort({ %matches{$_} }) 来按分数对选手进行排序, 它是按获胜场次来排序的。然而 AnaDave 都赢了两场比赛。这种简单的排序方式并没有考虑到获胜局数, 而获胜局数是决定谁赢得比赛的次要标准。

1.7.2. 稳定的排序

当两个数组项的值相同时, sort 会以发现它们的相同顺序离开。计算机科学家称之为稳定排序。该程序利用 Raku 排序的这一特性, 通过两次排序来实现目标:先按胜出的组数(主要标准), 再按胜出的比赛数(次要标准)。

经过第一步排序后, 名字的顺序是 Beth Charlie Dave Ana。第二步排序后, 还是一样, 因为没有人比别人赢的比赛少, 但赢的场次多。这种情况是完全可能的, 尤其是在大型比赛中。

sort 按升序排序, 从最小的到最大的。这与期望的顺序相反。因此, 代码对第二次排序的结果调用 .reverse 方法, 并将最终的列表存储在 @sorted 中。

1.8. 标准输出

for @sorted -> $n {
    my $match-noun = %matches{$n} == 1 ?? 'match' !! 'matches';
    my $set-noun   = %sets{$n} == 1 ?? 'set' !! 'sets';
    say "$n has won %matches{$n} $match-noun and %sets{$n} $set-noun";
}

为了打印出选手及其分数, 代码在 @sorted 上循环, 依次设置 $n 为每个选手的名字。将这段代码理解为 "对于排序后的每个元素, 将 $n 设置为元素, 然后执行下面的块的内容。" 变量 $match-noun 将存储字符串 match(如果选手赢得了一场比赛)或 matches(如果选手赢得了0场或更多场比赛)。为了做到这一点, 使用三元运算符(?? !!)。如果 %matches{$n} == 1 的值为 True, 则返回 match。否则, 将返回 matches。无论哪种方式, 返回的值都会存储在 $match-noun 中。同样的方法也适用于 $set-noun

语句 say 将其参数打印到标准输出(通常是屏幕), 后面是一个换行符。(如果不想在结尾有换行符, 请使用 print)。

请注意, say 会通过调用 .gist 方法来截断某些数据结构, 所以如果你想要精确的输出, 那么 put 比较安全。

1.8.1. 变量插值

当你运行程序时, 你会发现 say 并没有逐字打印该字符串的内容。取代 $n, 它打印的是变量 $n 的内容-$n 中存储的一个选手的名字。这种自动替换代码与其内容的做法叫做插值。这种插值只发生在以双引号 "…​ " 分隔的字符串中。单引号的字符串 '…​' 不进行插值。

1.8.2. 双引号字符串和单引号字符串

my $names = 'things';
say 'Do not call me $names'; # OUTPUT: «Do not call me $names␤»
say "Do not call me $names"; # OUTPUT: «Do not call me things␤»

在 Raku 中, 双引号的字符串可以用 $ 魔符插值变量, 也可以用花括号插值代码块。由于任何任意的 Raku 代码都可以出现在花括号中, 因此数组和散列可以通过将它们放在花括号中进行插值。

花括号内的数组在每个项之间用一个空格字符进行插值。花括号内的散列是以一系列行来插值的。每一行都包含一个键, 然后是一个制表符, 然后是与该键相关的值, 最后是一个换行符。

现在让我们来看一个例子。

在这个例子中, 你会看到一些特殊的语法, 使其更容易制作一个字符串列表。这就是 <…​> 引号单词结构。当你把单词放在 <> 之间时, 它们都被认为是字符串, 所以你不需要用双引号 "…​ " 来包裹它们。

say "Math: { 1 + 2 }";
# OUTPUT: «Math: 3␤»

my @people = <Luke Matthew Mark>;
say "The synoptics are: {@people}";
# OUTPUT: «The synoptics are: Luke Matthew Mark␤»

say "{%sets}␤";
# OUTPUT (From the table tennis tournament):

# Charlie 4
# Dave    6
# Ana     8
# Beth    4

当数组和散列变量直接出现在双引号字符串中时(而不是出现在花括号内), 只有当它们的名称后面跟了一个后环缀(postcircumfix)-一个跟在语句后面的括号对时, 它们才会被插值。在变量名和后环缀之间有一个方法调用也是可以的。

1.9. 禅切

my @flavors = <vanilla peach>;

say "we have @flavors";           # OUTPUT: «we have @flavors␤»
say "we have @flavors[0]";        # OUTPUT: «we have vanilla␤»
# so-called "Zen slice"
say "we have @flavors[]";         # OUTPUT: «we have vanilla peach␤»

# method calls ending in postcircumfix
say "we have @flavors.sort()";    # OUTPUT: «we have peach vanilla␤»

# chained method calls:
say "we have @flavors.sort.join(', ')";
                                  # OUTPUT: «we have peach, vanilla␤»

1.10. 练习

  • 1. 示例程序的输入格式是多余的:第一行包含所有选手的名字是没有必要的, 因为你可以通过查看后面几行选手的名字来找出哪些选手参加了比赛。

如果不使用 @names 变量, 如何使程序运行?提示:%hash.keys 返回存储在 %hash 中的所有键的列表。

答案:删除该行 my @names = $file.get.words;, 然后将如下代码:

my @sorted = @names.sort({ %sets{$_} }).sort({ %matches{$_} }).reverse;

改成:

my @sorted = %sets.keys.sort({ %sets{$_} }).sort({ %matches{$_} }).reverse;
  • 2. 与其删除多余的 @names 变量, 你还可以用它来警告, 如果出现了一个在第一行中没有提到的选手, 例如由于拼写错误。你将如何修改你的程序来实现这个目的?

提示:尝试使用成员运算符

答案:将 @names 改为 @valid-players。当循环浏览文件的行时, 检查 $p1$p2 是否在 @valid-players 中。注意, 对于会员运算符, 你也可以使用 (elem)!(elem)

...;
my @valid-players = $file.get.words;
...;

for $file.lines -> $line {
    my ($pairing, $result) = $line.split(' | ');
    my ($p1, $p2)          = $pairing.split(' ');
    if $p1 ∉ @valid-players {
        say "Warning: '$p1' is not on our list!";
    }
    if $p2 ∉ @valid-players {
        say "Warning: '$p2' is not on our list!";
    }
    ...
}

2. 关于文档

本文档集代表了正在进行的记录 Raku 编程语言的努力, 其目标是全面的、易于使用的; 易于导航的、对新手和有经验的 Raku 程序员都有用。

该文档的 HTML 版本位于线上的 https://docs.raku.org

该文档的官方源码位于 GitHub 上的 raku/doc

本文档是对 GitHub 上的贡献中详细描述的过程的快速概述。该文档还提供了一个编写 Raku Pod 文件的简短介绍, 这些文件可以被渲染成HTML和其他格式。

2.1. 结构

所有文档都是用 Raku Pod 编写的, 并保存在 doc/ 目录下, 以及 doc/Language/doc/Type/ 子目录下。这些文件被处理成定义的集合或 "可记录文件", 然后经过后处理并链接在一起。

2.2. 从 Pod 生成 HTML

要从 Pod 文件生成 HTML, 你需要:

  • 一个最新版本的 Rakudo Raku 编译器

  • Raku 模块 Pod::To::HTML, Pod::To::BigPageURI::Escape 都可以通过 zef 安装。例如, zef install Pod::To::HTML 来安装 Pod::To::HTML

  • Documentable, 文档 API。

  • 可选GraphViz, 用于创建 Raku 类型之间关系图。

  • 可选Atom Highlightslanguage-raku, 用于语法高亮。

要将文档生成到 html/ 文件夹中, 请运行:

documentable start -a -v --highlight

要从 Web 服务器上托管文档, 需要安装 Perl 和 Mojolicious::Lite;如果你有 Perl, 请运行:

cpanm --installdeps .

然后运行:

perl app.pl daemon

2.3. 贡献

文档是用 Raku Pod 编写的。

关于 Raku Pod 的快速介绍, 请参见 Raku Pod

关于 Raku Pod 规范的完整细节, 请参见概要26,文档

2.3.1. 添加定义

文档可以使用 =headN Pod 指令来定义, 其中 N 大于零(例如, =head1, =head2, …​)。

该指令之后的所有段落和块, 直到同一级别的下一个指令, 都将被视为可记录的一部分。因此, 在:

    =head2 My Definition

    Some paragraphs, followed by some code:

    =begin code
    my Code $examples = "amazing";
    =end code

    Mind === blown.

    =head3 Minor details about My Definition

    It's fantastic.

    =head2 And now, for something completely different

    …

可记录文件的 My Definition 一直延伸到 =head2 And now…

可记录文件可以包含其他可记录文件。例如, 类的可记录文件通常包含了类所实现的方法。

定义必须以下列形式之一才能被识别为一个名为 þ 的可记录文件的开始。首先是文档源中的代码。

=item X<C<How to use the þ infix> | infix,þ> (This a special case, which
is always considered a definition)

=item C<The þ Infix>

=item B<The C<þ> Infix>

=item C<Infix þ>

=item B<Infix C<þ>>

=item C<trait is cached> (A special case for the L<trait|/language/functions#Traits> documentables)

然后是渲染页面上的结果:

  • How to use the þ infix (这是一种特殊情况, 始终被视为定义)

  • The þ Infix

  • The þ Infix

  • Infix þ

  • Infix þ

  • trait is cachedtrait 文档的特例)

这些项目现在应该可以使用 HTML 文档中的搜索栏进行搜索。

你可以使用粗体(B<>)或斜体(I<>)添加强调, 有或没有代码格式 ( C<> ) 。由于当前解析器的限制, 必须采取特殊的步骤来使用 X<> 和其他格式代码, 例如:

=item X<B<foo>|foo> a fancy subroutine

像这样渲染

  • foo a fancy subroutine

请注意, 管道("|")后面的文本没有格式。另外请注意, C<> 保留了空格, 并将文本视为逐字处理。

3. 简介

记录像 Raku 这样的大型语言, 必须平衡几个矛盾的目标, 比如既要简短又要全面, 既要满足有丰富经验的专业开发人员的需求, 又要让新来的人也能使用这门语言。

有关快速上手的介绍, 我们提供了一个简短的注释编程示例

对于有其他语言经验的程序员来说, 有许多迁移指南, 将 Raku 和其他语言的功能进行了比较和对比。

一些教程涵盖了 Raku 特别创新的几个领域。本节的标题应该有助于浏览其余的文档。

在 raku.org 网站的其他地方列出了许多有用的资源。这些资源包括文章、书籍、幻灯片演示和视频。

我们发现, Raku 的新人经常会提出一些问题, 这些问题表明他们的假设是从其他编程范式中延续下来的。我们建议首先回顾基本主题部分的以下章节。

  • 签名 - 每个例程, 包括子例程和方法, 都有一个签名。了解子程序或方法的签名中给出的信息, 可以快速掌握例程的操作和效果。

  • 容器 - 变量, 就像计算机语言的名词一样, 是存储信息的容器。容器正式名称中的第一个字母, 例如 $my-variable'$', 或 @an-array-of-things'@', 或 %the-score-in-the'%' - 携带有关容器的信息。然而, Raku 对于可以存储在容器中的内容比其他语言更加抽象。所以, 例如, $scalar 容器可以包含一个实际上是数组的对象。

  • 类和角色 - Raku 从根本上讲是基于对象的, 而对象是用类和角色来描述的。与某些语言不同的是, Raku 并没有强制使用面向对象的编程实践, 并且可以编写有用的程序, 就好像 Raku 纯粹是程序性的。然而, 复杂的软件, 例如 Raku 的 Rakudo 编译器, 用面向对象的惯用法来编写, 会变得简单很多, 这也是为什么 Raku 的文档更容易通过回顾什么是类, 什么是角色来理解。如果不了解类和角色, 就很难理解类型, 本文档中有一整节专门讨论类型。

  • 要避免的陷阱 - 有几个常见的假设会导致代码不能按照程序员的意图工作。本节将指出其中的一些问题。当有些东西不能正常工作时, 它值得回顾。

4. 特殊变量

4.1. 描述

一个(希望)全面的 Perl 5 特殊变量列表及其 Raku 等价物,并在必要时记录它们之间的变化。

4.2. 注意

本文档试图引导读者从 Perl 5 中的特殊变量到 Raku 中的等效变量。有关 Raku 特殊变量的完整文档,请参阅每个变量的 Raku 文档。

4.3. 特殊变量

4.3.1. 通用变量

  • $ARG

  • $_

值得庆幸的是,$ 是 Perl 5 中的常规默认变量。Raku 的主要区别在于现在你可以在它身上调用方法。 例如,Perl 5 的 say $ 可以在 Raku 中以 $_.say 表示。 此外,因为它是默认变量,您甚至不需要使用变量名称。 前面的例子也可以 通过使用 .say 实现。

  • @ARG

  • @_

由于 Raku 现在具有函数签名,您的参数可以去那里,而不是依赖于 @。 事实上,如果你使用函数签名,使用 @ 会吐出你告诉它不能覆盖一个现有签名。

但是,如果您不使用函数签名,则 @ 将包含您传递给函数的参数, 就像它在 Perl 5 中那样。再次,与 $ 一样 ,您可以在其上调用方法。 与 $ 不同,你不能假设 @ 为 这些方法的默认变量(即 @_.shift works, .shift 不 work)。

  • $LIST_SEPARATOR

  • $"

目前,Raku 中没有与 List Separator 变量等效的设计文档 S28 在那里说 不是一个,所以你可能不想屏住呼吸。

  • $PROCESS_ID

  • $PID

  • $$

在 Raku 中用 $*PID 替换 $$

  • $PROGRAM_NAME

  • $0

您可以通过 $*PROGRAM-NAME 访问 Raku 中的程序名称 。 注意: Raku 中的 $0 是保持正则表达式匹配中第一个捕获值的变量(即捕获变量现在从 $0 而不是 $1 开始 )。

  • $REAL_GROUP_ID

  • $GID

  • $(

在 Raku 中,组信息由 $*GROUP 处理 ,它包含一个 IntStr 类型的对象 因此 可以在字符串或数字上下文中使用。 因此,组 ID 通过 +$*GROUP 获得 , 而组名通过 ~$*GROUP 获得 。

  • $EFFECTIVE_GROUP_ID

  • $EGID

  • $)

Raku 目前似乎没有提供有效的组 ID。

  • $REAL_USER_ID

  • $UID

  • $<

在 Raku 中,用户信息由 $*USER 处理 ,后者持有 IntStr 类型的对象,因此可以 可以在字符串或数字上下文中使用(这类似于处理组信息的方式) 由 $*GROUP 对象)。 因此,用户 ID 通过 +$*USER 获得 ,而用户名通过 ~$*USER 获得 。

  • $EFFECTIVE_USER_ID

  • $EUID

  • $>

Raku 当前似乎没有提供有效的用户 ID。

  • $SUBSCRIPT_SEPARATOR

  • $SUBSEP

  • $;

Raku 中不包含下标分隔符变量。坦率地说,如果你的 Perl 5 代码正在使用它,那就是 几乎可以肯定,真的很老。

  • $a

  • $b

$a$b 在 Raku 中没有特殊含义 .sort() 不会将它们用于任何特殊的东西。 他们只是常规的旧变量。 通过使用具有更多功能的占位符参数的块来扩展此功能。 占位符变量是使用 ^twigil 创建的 (例如 $^z 。它们可以在裸块中使用或在没有显式参数列表的子程序。 块的参数将分配给占位符 Unicode 顺序中的变量。 I. e。 即使变量出现在块中的顺序 ($^q, $^z, $^a) ,它们将按顺序分配 ($^a, $^q, $^z) 。 人机工程学:

sort { $^a cmp $^z }, 1, 5, 6, 4, 2, 3;
# OUTPUT: «(1 2 3 4 5 6)␤»
sort { $^g cmp $^a }, 1, 5, 6, 4, 2, 3;
# OUTPUT: «(6 5 4 3 2 1)␤»
for 1..9 { say $^c, $^a, $^b; last }
# OUTPUT: «312␤»

有关占位符变量的更多信息,请参阅 此页面

  • %ENV

%ENV 已被 Raku 中的%*ENV 取代。请注意,此哈希的键可能不完全是 在 Perl 5 和 Raku 之间相同。例如, Raku 的%ENV 中缺少 OLDPWD 。

  • $OLD_PERL_VERSION

  • $]

Raku 的运行版本由 $*PERL 特殊变量保存,即一个对象。 正在运行的版本是 通过 $*PERL.version 检索 ,返回类似 v6.c 的内容 ; Perl 的完整字符串化版本解释器是通过 ~$*PERL 获得的 ,它返回类似于 Raku(6.c)的内容 。

  • $SYSTEM_FD_MAX

  • $^F

虽然设计文件(S28)表明这可能会变成 $*SYS_FD_MAX ,但这还没有 b 被实现。

  • @F

[需要进一步研究]此时有点混乱。 设计文档 S28 表示 @F in Perl 5 在 Raku 中被 @_ 取代 ,但目前还不清楚它是如何工作的。 另一方面,它是目前的 有点问题,因为 Perl 5 to Raku Translation doc 指出 -a-F 命令 - rakudo 尚未实现行开关。

  • @INC

Raku 中不再存在。请使用“use lib”来操作要搜索的模块存储库。 该 最接近 @INC 的是 $*REPO。 但这与@INC 完全不同 因为 Raku 的预编译功能。

# Print out a list of compunit repositories
.say for $*REPO.repo-chain;
  • %INC

Raku 中不再存在。因为每个 Repository 都负责记住哪些模块已经装好了。 您可以获得所有已加载模块(编译单元)的列表,如下所示:

use Test;
use MyModule;
say flat $*REPO.repo-chain.map(*.loaded); #-> (MyModule Test)
  • $INPLACE_EDIT

  • $^I

S28 建议使用 $*INPLACE_EDIT,但它尚不存在。

  • $^M

S28 建议使用 $*EMERGENCY_MEMORY,但它尚不存在。

  • $OSNAME

  • $^o

这有点不清楚。 它可能取决于你的意思是“操作系统的名称” 作为设计文档 S28 有三个不同的建议,所有建议都给出了不同的答案。

目前有三个主要对象包含有关“运行环境”的信息:

  • $*KERNEL 提供有关正在运行的操作系统内核的信息;

  • $*DISTRO 提供有关操作系统分发的信息;

  • $*VM 提供有关 Raku 的运行后端机器的信息。

以上所有对象都有共同的方法:

  • version 提供该组件的版本号;

  • name 提供该组件的助记符名称;

  • auth 为该组件提供已知作者。

作为一个简短示例,以下代码打印有关上述所有组件的信息:

for $*KERNEL, $*DISTRO, $*VM -> $what {
    say $what.^name;
    say 'version '  ~ $what.version
        ~ ' named ' ~ $what.name
        ~ ' by '    ~ $what.auth;
}

# Kernel
# version 4.10.0.42.generic named linux by unknown
# Distro
# version 17.04.Zesty.Zapus named ubuntu by https://www.ubuntu.com/
# VM
# version 2017.11 named moar by The MoarVM Team

上面所有的 Str 方法产生了当前时间的信息的短版本名字 。

所有对象都有其他方法,在尝试识别正确运行的实例时非常有用, 有关更多信息,请使用 <.^methods> 来内省以上所有内容。

  • %SIG

没有等效变量。 要在接收信号时执行代码,您可以调用 signal 子程序,返回可以点击的 Supply

$SIG{"INT"} = sub { say "bye"; exit }
signal(SIGINT).tap: { say "bye"; exit }; loop {}

或者,如果您有一个通用代码,想知道它得到了哪个信号:

signal(SIGINT).tap: -> $signal { say "bye with $signal"; exit }; loop {}

在事件驱动的情况下使用信号的更惯用的方式:

react {
    whenever signal(SIGINT) {
        say "goodbye";
        done
    }
}
  • $BASETIME

  • $^T

在 Raku 中用 $*INIT-INSTANT 替换 。 与 Perl 5 不同,这不是自纪元以来的秒数,而是一个 Instant 对象,以原子秒计,带有分数。

  • $PERL_VERSION

  • $^V

$] 一样,这已被 $*PERL.version 取代。

  • ${^WIN32_SLOPPY_STAT}

在 Raku 中没有类似的东西。

  • $EXECUTABLE_NAME

  • $^X

这已被 $*EXECUTABLE-NAME 取代 。 请注意,还有 $*EXECUTABLE ,这在 Raku 中是一个 IO 对象。

4.3.2. 与正则表达式相关的变量

性能问题

如下所示, $`, $&$' 从 Raku 中删除了,主要由 $/ 和它的变体代替, 消除它们,Perl 5 中的相关性能问题不适用。

  • $<digits> ($1, $2, …​)

Raku 中的这些现有变量与 Perl 5 中的相同,除了它们现在从 $0 开始而不是 $1。 此外,它们是匹配变量 $/ 中索引项的同义词。 例如 $0 相当于 $/[0], `$1 相当于 $/[1] 等。

  • $MATCH

  • $&

$/ 现在包含 匹配对象,因此 $& 的 Perl 5 行为可以通过字符串化来获得,即 ~$/ 。 请注意,虽然 $/.Str 也可以工作, 但 ~$/ 目前是更常见的用法。

  • ${^MATCH}

由于以前的性能问题已经废除,因此 Raku 中没有使用此变量。

  • $PREMATCH

  • $`

替换为 $/.prematch

  • ${^PREMATCH}

    由于以前的性能问题已经废除,因此 Raku 中没有使用此变量。
  • $POSTMATCH

  • $'

替换为 $/.postmatch

  • ${^POSTMATCH}

由于以前的性能问题已经废除,因此 Raku 中没有使用此变量。

  • $LAST_PAREN_MATCH

  • $+

在 Raku 中不存在,但你可以使用 $/[-1].Str 获得相同的信息。($/[-1] 将是匹配对象,而不是实际的字符串)。

如果您想了解其工作原理,可以查看以下文档:

  • %20#language_documentation_Operators[[]routine] 例程

  • Whatever

可能

虽然设计文件并不总是最新的。

  • $LAST_SUBMATCH_RESULT

  • $^N

S28 建议 $*MOST_RECENT_CAPTURED_MATCH ,但似乎没有任何实现的变量匹配 $^N.

  • @LAST_MATCH_END

  • @+

与大多数正则表达式相关的变量一样,此功能至少部分地移至 Raku 中的 $/ 变量。或者,在这种情况下,编号变量是索引的别名。 偏移是通过使用 .to 方法找到。 例如, 第一个偏移是 $/[0].to ,它与 $0.to 同义。 Perl 5 提供的 $+[0]$/.to 提供。

  • %LAST_PAREN_MATCH

  • %+

再一次,我们转移到 $/。 前面的 $+{$match}$/{$match}

  • @LAST_MATCH_START

  • @-

类似于使用 .to 方法替换 @+ ,使用 $/ 上的 .from 方法替换 @- 及其变化。 第一个偏移是 $/[0].from 或等价的 $0.from。 Perl 5 的 $-[0]$/.from

  • %LAST_MATCH_START

  • %-

%+ 非常相似 ,使用 %-{$match} 将替换为 $/{$match}

  • $LAST_REGEXP_CODE_RESULT

  • $^R

没有等价物。

  • ${^RE_DEBUG_FLAGS}

没有等价物。

  • ${^RE_TRIE_MAXBUF}

没有等价物。

与文件句柄相关的变量
  • $ARGV

读取行时当前文件的名称可以通过 $*ARGFILES.path 获得。

  • @ARGV

@*ARGS 包含命令行参数。

  • ARGV

这被 $*ARGFILES 取代 。

  • ARGVOUT

由于尚未实现 -i 命令行开关,因此还没有相当于 ARGVOUT 的功能 。

  • $OUTPUT_FIELD_SEPARATOR

  • $OFS

  • $

目前没有明显的等价物

  • $INPUT_LINE_NUMBER

  • $NR

  • $.

不存在直接替代品。

迭代时使用 行方法 IO::PathIO::Handle 类型,您可以在其上调用 .kv 方法 获取交错的索引和值列表(然后每个循环迭代 2 次):

for "foo".IO.lines.kv -> $n, $line {
    say "{$n + 1}: $line"
}
# OUTPUT:
# 1: a
# 2: b
# 3: c
# 4: d

对于 IO::CatHandle 类型(其中 $*ARGFILES 是一个),你可以使用 on-switch hook 在句柄开关上重置行号,并手动递增。 另请参阅 IO::CatHandle::AutoLinesLN 模块简化此操作。

  • $INPUT_RECORD_SEPARATOR

  • $RS

  • $/

这可以通过文件句柄上的 .nl-in 方法访问 。 例如。 $*IN.nl-in

  • $OUTPUT_RECORD_SEPARATOR

  • $ORS

  • $\

这可以通过文件句柄上的 .nl-out 方法访问 。 例如 $*OUT.nl-out

  • $OUTPUT_AUTOFLUSH

  • $|

没有全球替代品。 对于其他设置,TTY 句柄默认是无缓冲的 out-buffer 设置为零或者使用 :!out-buffer 在特定的 IO::Handle上和 open 一块使用 。

  • ${^LAST_FH}

在 Raku 中没有实现。

与格式相关的变量

Raku 中没有内置格式。

4.3.3. 错误变量

关于 Raku 中的错误变量如何变化,因此这里不再详细说明。

引用 Raku docs 中的说法,$! 是错误变量。

与 Raku 的其余部分一样,它是一个根据类型返回各种内容的错误类型或 异常

特别是在处理 异常时, $! 提供有关抛出异常的信息, 假设程序没有停止:

try {
    fail "Boooh";
    CATCH {
        # within the catch block
        # the exception is placed into $_
        say 'within the catch:';
        say $_.^name ~ ' : ' ~ $_.message;
        $_.resume; # do not abort
    }
}

# outside the catch block the exception is placed
# into $!
say 'outside the catch:';
say $!.^name ~ ' : ' ~ $!.message;

以上代码生成以下输出

within the catch:
X::AdHoc : Boooh
outside the catch:
X::AdHoc : Boooh

因此,如前所述, $! 变量保存异常对象。

4.3.4. 与解释器状态相关的变量

  • $COMPILING

  • $^C

  • $^D

目前没有这些变量的等价物。

  • ${^ENCODING}

虽然在 Perl 5 中已弃用 ,但在 $?ENC 中 可能有某种等价物 ,但这还远未明朗。

  • ${^GLOBAL_PHASE}

没有 Raku 等价物。

  • $^H

  • %^H

  • ${^OPEN}

在 Raku 中可能有也可能没有这些等价物,但它们是内部的,你不应该搞乱 与他们在一起 - 当然,如果您对 Raku 的理解需要您阅读本文,那么肯定不会阅读该文献…​

  • $PERLDB

  • $^P

Raku 调试器与 Perl 5 调试器相似的可能性最小,此时此处也是如此,似乎不等于这个变量。

  • ${^TAINT}

S28 声称这个变量是“待定”的。 目前不在 Raku 中。

  • ${^UNICODE}

  • ${^UTF8CACHE}

  • ${^UTF8LOCALE}

这些与 Unicode 相关的变量似乎不存在于 Raku 中,但是 - 也许? - 可能在某处有 $?ENC 类似物吗? 然而,这完全未经证实。

4.3.5. 弃用和删除变量

不言而喻,因为已经从 Perl 5 中删除了这些,所以应该没有必要告诉你如何在 Raku 中使用它们。

5. Javascript 到 Raku - 简而言之

此页面试图为有经验的 Node.js 用户提供学习 Raku 的方法。这里将解释两种语言之间通用的功能,以及语法和功能上的主要差异。

这不是学习 Raku 的教程; 对于中高级技能级别的 Node.js 用户,这只是一个参考。

5.1. 基础语法

5.1.1. "Hello, world!"

让我们从学习新语言时第一个典型的程序开始。在 Node.js 中,hello world 程序将编写如下:

console.log('Hello, world!');

以下是在 Raku 中以相同方式编写此内容的几种方法:

say('Hello, world!');
say 'Hello, world!';

对于 Raku 中的函数调用,括号是可选的。虽然分号在 Node.js 中大多是可选的,但对于 Raku 中的表达式而言分号是必需的。

现在我们对世界打过招呼了,让我们迎接我们的好朋友 Joe。我们将再次从 Node.js 开始:

let name = 'Joe';
console.log('What\'s up,' + name + '?');
console.log(`What's up, {name}?`);
console.log("What's up, ", name, "?");

因为他没有听到我们,所以让我再问候他一次,这次是在 Raku 中:

my $name = 'Joe';
say 'What\'s up, ' ~ $name ~ '?';
say "What's up, $name?";
say "What's up, ", $name, "?";

这里只有几个不同之处:Raku 中的大多数变量都有所谓的 sigils,这就是它名称前面的 $,字符串连接使用 ~ 运算符代替 +。这两种语言的共同点是支持字符串插值。

基本的例子就到这里了,让我们更详细地解释两种语言之间的相似之处。

5.1.2. 变量

Node.js 中的变量可以像这样定义;

var foo = 1;    // Lexically scoped with functions and modules
let foo = 1;    // Lexically scoped with blocks
const foo = 1;  // Lexically scoped with blocks; constant

global.foo = 1; // Dynamically scoped; global
foo = 1;        // Ditto, but implicit; forbidden in strict mode

在 Raku 中没有 var 的等价物。需要注意的一点是,Raku 中没有可变的吊装; 变量在它们所在的行上定义和分配,未在其作用域的顶部定义,稍后在该行赋值。

这是在 Raku 中定义等效类型的变量的方式:

my $foo = 1;      # Lexically scoped with blocks
our $foo = 1;     # Lexically scoped with blocks and modules
constant foo = 1; # Lexically scoped with blocks and modules; constant

my $*foo = 1;       # Dynamically scoped with blocks
OUR::<$foo> = 1;    # Dynamically scoped with blocks and modules
GLOBAL::<$foo> = 1; # Dynamically scoped; global

使用 my 您使用的位置 letour 您需要在最外层范围内定义的变量以及 constant 您使用的位置 const

动态范围变量的引用方式与它们在 Node.js 中的词汇范围变量相同。用户定义的那些使用一个 $@%,或 & twigil。有关 sigils,twigils 和变量容器的更多信息,请参阅有关 变量的文档。

Node.js 中的变量可以覆盖具有相同名称的外部作用域中的其他变量(尽管 linters 通常会根据它们的配置方式来抱怨它):

let foo = 1;
function logDupe() {
    let foo = 2;
    console.log(foo);
}

logDupe(2);       // 2
console.log(foo); // 1

Raku 也允许这样:

my $foo = 1;
sub log-dupe {
    my $foo = 2;
    say $foo;
}

log-dupe; # 2
say $foo; # 1

5.1.3. 运算符

赋值

= 运算符可以跨两种语言相同。

Raku 中的 := 运算符将值绑定到变量。将变量绑定到另一个变量会为它们提供相同的值和容器,这意味着一个变量属性也会改变另一个变量。绑定变量不能被重新分配 = 或突变 ++-- 等,但它们可以被重新绑定到另一个值:

my %map;            # This is a hash, roughly equivalent to a JS object or map
my %unbound = %map;
my %bound := %map;
%map<foo> = 'bar';
say %unbound;       # {}
say %bound;         # {foo => bar}

%bound := %unbound;
say %bound;         # {}
相等

Node.js 有两个相等运算符:=====

== 是松散的平等算子。比较具有相同类型的操作数时,如果两个操作数相等,则返回 true。但是,如果操作数是不同的类型,它们在被比较之前都被转换为它们的基元,这意味着它们将返回 true:

console.log(1 == 1);   // true
console.log('1' == 1); // true
console.log([] == 0);  // true

类似地,在 Raku 中,如果它们不共享相同的类型,则在比较之前将两个操作数强制转换为 Numeric:

say 1 == 1;       # True
say '1' == 1;     # True
say [1,2,3] == 3; # True, since the array has three elements

倒数 ==!=

Raku 有另一个类似于的运算符 ==eq。如果它们是不同的类型,而不是将操作数转换为 Numeric,而不是 eq 将它们转换为字符串:

say '1' eq '1'; # True
say 1 eq '1';   # True

逆的 eqne!eq

=== 是严格的相等运算符。如果两个操作数是相同的值,则返回 true。比较对象时,如果它们是完全相同的对象, 则只 返回 true:

console.log(1 === 1);   // true
console.log('1' === 1); // false
console.log({} === {}); // false

let obj = {};
let obj2 = obj;
console.log(obj === obj2); // true;

在 Raku 中,运算符的行为相同,但有一个例外:两个具有相同值但容器不同的对象将返回 false:

say 1 === 1; # True
say '1' === 1; # True
say {} === {};  # False

my \hash = {};
my %hash = hash;
say hash === %hash; # False

在最后一种情况下,它是相同的对象,但容器是不同的,这就是它返回 False 的原因。

倒数 ===!==

这是 Raku 的其他相等运算符很有用的地方。如果值具有不同的容器,则 eqv 可以使用操作员。此运算符也可用于检查深度相等性,通常需要在 Node.js 中使用库:

say {a => 1} eqv {a => 1}; # True;

my \hash = {};
my %hash := hash;
say hash eqv %hash; # True

如果您需要检查两个变量是否具有相同的容器和值,请使用 =:= 运算符。

my @arr = [1,2,3];
my @arr2 := @arr;   # Bound variables keep the container of the other variable
say @arr =:= @arr2; # True
Smartmatching

Raku 有一个用于比较值的最后一个运算符,但它不完全是一个相等运算符。这就是 ~~ 智能匹配运算符。这有几个用途:它可以像 instanceof 在 Node.js 中一样使用,以匹配正则表达式,并检查值是否是散列,包,集或映射中的键:

say 'foo' ~~ Str; # True

my %hash = a => 1;
say 'a' ~~ %hash; # True

my $str = 'abc';
$str ~~ s/abc/def/; # Mutates $str, like foo.replace('abc', 'def')
say $str;           # def

在我们讨论 Raku 中 instanceof 的时候, Node.js 对象的 constructor 属性相当于 WHAT 属性:

console.log('foo'.constructor); // OUTPUT: String
say 'foo'.WHAT; # OUTPUT: Str
Numeric

Node.js 的有 +-/%,和(在 ES6)* 作为数字运算符。当操作数是不同类型时,类似于相等运算符,在执行操作之前会转换为它们的基元,从而使这成为可能:

console.log(1 + 2);   // 3
console.log([] + {}); // [object Object]
console.log({} + []); // 0

在 Raku 中,它们再次转换为数字类型,如前所述:

say 1 + 2;        # 3
say [] + {};      # 0
say {} + [1,2,3]; # 3

另外,Raku 有 div%%div 表现得像 int C 中的分裂,同时 %% 检查一个数字是否可以被另一个数字完全整除:

say 4 div 3; # 1
say 4 %% 3;  # False
say 6 %% 3;  # True
Bitwise

Node.js 的有 &|^~<<>>>>>,和 ~ 对位运算符:

console.log(1 << 1);  // 2
console.log(1 >> 1);  // 0
console.log(1 >>> 1); // 0
console.log(1 & 1);   // 1
console.log(0 | 1);   // 1
console.log(1 ^ 1);   // 0
console.log(~1);      // -2

在 Raku 中,没有相当于 >>>。所有按位运算符都以前缀为前缀 +,但是使用两个补码 +^ 而不是 ~

say 1 +< 1; # 2
say 1 +> 1; # 0
            # No equivalent for >>>
say 1 +& 1; # 1
say 0 +| 1; # 1
say 1 +^ 1; # 0
say +^1;    # -2
Custom operators and operator overloading

Node.js 不允许运算符重载而不必使用 Makefile 或使用自定义版本的 V8 构建 Node.js. Raku 允许自定义操作符和操作符本机重载!由于所有运算符都是子程序,因此您可以像这样定义自己的运算符:

multi sub infix:<||=>($a, $b) is equiv(&infix:<+=>) { $a || $b }

my $foo = 0;
$foo ||= 1;
say $foo; # OUTPUT: 1

运算符可以定义为 prefixinfix,或 postfix。的 is tighteris equivis looser 性状选择定义操作的优先级。在这种情况下,||= 具有相同的优先级 +=

注意 multi 在声明操作符子例程时如何使用。这允许声明具有相同名称的多个子例程,同时具有不同的签名。这将在 函数部分中详细说明。目前,我们需要知道的是它允许我们覆盖我们想要的任何本机运算符:

=== Using the `is default` trait here forces this subroutine to be chosen first,
=== so long as the signature of the subroutine matches.
multi sub prefix:<++>($a) is default { $a - 1 }

my $foo = 1;
say ++$foo; # OUTPUT: 0

5.1.4. Control flow

if/else

您应该熟悉 JavaScript 中的 if/ else

let diceRoll = Math.ceil(Math.random() * 6) + Math.ceil(Math.random() * 6);
if (diceRoll === 2) {
    console.log('Snake eyes!');
} else if (diceRoll === 16) {
    console.log('Boxcars!');
} else {
    console.log(`Rolled ${diceRoll}.`);
}

在 Raku 中,if/else 的工作方式基本相同,只有一些关键的区别。一,括号不是必需的。二,else if 写成 elsif。三,if 语句可以在声明后写出:

my Int $dice-roll = ceiling rand * 12 + ceiling rand * 12;
if $dice-roll == 2 {
    say 'Snake eyes!';
} elsif $dice-roll == 16 {
    say 'Boxcars!';
} else {
    say "Rolled $dice-roll.";
}

或者,虽然效率较低,但可以 if 在语句后使用:

my Int $dice-roll = ceiling rand * 12 + ceiling rand * 12;
say 'Snake eyes!'        if $dice-roll == 2;
say 'Boxcars!'           if $dice-roll == 16;
say "Rolled $dice-roll." if $dice-roll !~~ 2 | 16;

Raku 也有 when,就像是 if,但是如果给出的条件为真,when 那么执行它所执行的块中没有代码超过块:

{
    when True {
        say 'In when block!'; # OUTPUT: In when block!
    }
    say 'This will never be output!';
}

此外,Raku 的有 withorwithwithout,这是一样 ifelse if 和,else 分别但是,不是检查自己的条件是否为真,他们检查,如果它被定义。

switch

Switch 语句是一种检查给定值和值列表之间相等性的方法,并在匹配时运行一些代码。case 语句定义要比较的每个值。default,如果包含,则作为给定值不匹配任何情况的后备。在匹配案例之后,break 通常用于防止代码跟随匹配的案例执行,尽管很少有意省略。

const ranklist = [2, 3, 4, 5, 6, 7, 8, 9, 'Jack', 'Queen', 'King', 'Ace'];
const ranks    = Array.from(Array(3), () => ranklist[Math.floor(Math.random() * ranks.length)]);
let   score    = 0;

for (let rank of ranks) {
    switch (rank) {
        case 'Jack':
        case 'Queen':
        case 'King':
            score += 10;
            break;
        case 'Ace';
            score += (score <= 11) ? 10 : 1;
            break;
        default:
            score += rank;
            break;
    }
}

在 Raku 中,given 可以像 switch 语句一样使用。没有相应的,break 因为 when 块最常用于 case 语句。switch 和之间的一个主要区别 given 是传递给 switch 语句的值只匹配与值完全相等的情况; given 值是 ~~ 针对值的 smartmatched() when

my     @ranklist = [2, 3, 4, 5, 6, 7, 8, 9, 'Jack', 'Queen', 'King', 'Ace'];
my     @ranks    = @ranklist.pick: 3;
my Int $score    = 0;

for @ranks -> $rank {
    # The when blocks implicitly return the last statement they contain.
    $score += do given $rank {
        when 'Jack' | 'Queen' | 'King' { 10 }
        when 'Ace' { $score <= 11 ?? 10 !! 1 }
        default { $_ }
    };
}

如果有多个 when 块与传递的值匹配,given 并且您希望运行多个块,请使用 proceedsucceed 可用于退出 when 它所在的块和给定的块,防止执行以下任何语句:

given Int {
    when Int     { say 'Int is Int';     proceed }
    when Numeric { say 'Int is Numeric'; proceed }
    when Any     { say 'Int is Any';     succeed }
    when Mu      { say 'Int is Mu'               } # Won't output
}

# OUTPUT:
# Int is Int
# Int is Numeric
# Int is Any
for, while 和 do/while

JavaScript 中有三种不同类型的 for 循环:

// C-style for loops
const letters = {};
for (let ord = 0x61; ord <= 0x7A; ord++) {
    let letter = String.fromCharCode(ord);
    letters[letter] = letter.toUpperCase();
}

// for..in loops (typically used on objects)
for (let letter in letters) {
    console.log(letters[letter]);
    # OUTPUT:
    # A
    # B
    # C
    # etc.
}

// for..of loops (typically used on arrays, maps, and sets)
for (let letter of Object.values(letters)) {
    console.log(letter);
    # OUTPUT:
    # A
    # B
    # C
    # etc.
}

Raku for 循环最接近 for..of 循环,因为只要它是可迭代的,它们就可以处理任何东西。C 风格的循环可以使用 loop,但不鼓励这样做,因为它们更好地编写为 for 使用范围的循环。类似 if 语句,for 可以遵循一个语句,当前迭代可以使用 $_ 变量(称为“它”)访问。$_ 可以在不指定变量的情况下调用方法:

my Str %letters{Str};
%letters{$_} = .uc for 'a'..'z';
.say for %letters.values;

# OUTPUT:
# A
# B
# C
# etc.

do/while 循环 repeat/while 在 Raku 中称为循环。同样 whilerepeat/until 循环也存在并循环,直到给定条件为假。

要在 Raku 中编写无限循环,请使用 loop 而不是 forwhile

在 JavaScript 中,continue 用于跳转到循环中的下一个迭代,并 break 用于提前退出循环:

let primes = new Set();
let i      = 2;

do {
    let isPrime = true;
    for (let prime of primes) {
        if (i % prime == 0) {
            isPrime = false;
            break;
        }
    }
    if (!isPrime) continue;
    primes.add(i);
} while (++i < 20);

console.log(primes); # OUTPUT: Set { 2, 3, 5, 7, 11, 13, 17, 19 }

在 Raku 中,这些分别称为 nextlast。还有 redo,它重复当前迭代而不再评估循环的条件。

next/redo/last 语句后跟一个在外部循环之前定义的标签,以使该语句在标签所引用的循环上起作用,而不是该语句所在的循环:

my %primes is SetHash;
my Int $i = 2;

OUTSIDE:
repeat {
    next OUTSIDE if $i %% $_ for %primes.keys;
    %primes{$i}++;
} while ++$i < 20;

say %primes; # OUTPUT: SetHash(11 13 17 19 2 3 5 7)
do

do 目前不是 JavaScript 中的一项功能,但已提出 将其添加到 ECMAScript的提案。do 表达式计算一个块并返回结果:

constant VERSION        = v2.0.0;
constant VERSION_NUMBER = do {
    my @digits = VERSION.Str.comb(/\d+/);
    :16(sprintf "%02x%02x%04x", |@digits)
};
say VERSION_NUMBER; # OUTPUT: 33554432

5.1.5. Types

Creating types

在 JavaScript 中,通过创建类(或 ES5 及更早版本中的构造函数)来创建类型。如果您使用过 TypeScript,则可以将类型定义为其他类型的子集,如下所示:

type ID = string | number;

在 Raku 中,类,角色,子集和枚举被视为类型。创建类和角色将在本文 的 OOP 部分中讨论。创建 ID 子集可以这样完成:

subset ID where Str | Int;

有关更多信息,请参阅 子集连接的文档。

TypeScript 枚举可以包含数字或字符串作为其值。定义值是可选的; 默认情况下,第一个键的值为 0,下一个键为 1,下一个键为 2,等等。例如,这是一个枚举,用于定义扩展 ASCII 箭头符号的方向(可能用于 TUI 游戏):

enum Direction (
    UP    = '↑',
    DOWN  = '↓',
    LEFT  = '←',
    RIGHT = '→'
);

Raku 中的枚举可以使用任何类型作为其键值。枚举键(以及可选的值)可以通过写入来定义 enum,然后是枚举的名称,然后是键列表(以及可选的值),可以使用 <>«»() 来完成。( ) 如果要为枚举键定义值,则必须使用。这是 Raku 中编写的 Direction 枚举:

enum Direction (
    UP    => '↑',
    DOWN  => '↓',
    LEFT  => '←',
    RIGHT => '→'
);

有关更多信息,请参阅 枚举文档。

Using types

在 TypeScript 中,您可以定义变量的类型。尝试分配与变量类型不匹配的值将导致转换器错误。这样做是这样的:

enum Name (Phoebe, Daniel, Joe);
let name: string = 'Phoebe';
name = Phoebe; # Causes tsc to error out

let hobbies: [string] = ['origami', 'playing instruments', 'programming'];

let todo: Map<string, boolean> = new Map([
    ['clean the bathroom', false],
    ['walk the dog', true],
    ['wash the dishes', true]
]);

let doJob: (job: string) => boolean = function (job: string): boolean {
    todo.set(job, true);
    return true;
};

在 Raku 中,变量可以通过将说明符(之间的类型被键入 myour 等)和变量名。分配与变量类型不匹配的值将引发编译时或运行时错误,具体取决于值的计算方式:

enum Name <Phoebe Daniel Joe>;
my Str $name = 'Phoebe';
$name = Phoebe; # Throws a compile-time error

# The type here defines the type of the elements of the array.
my Str @hobbies = ['origami', 'playing instruments', 'programming'];

# The type between the declarator and variable defines the type of the values
# of the hash.
# The type in the curly braces defines the type of the keys of the hash.
my Bool %todo{Str} = (
    'clean the bathroom' => False,
    'walk the dog'       => True,
    'wash the dishes'    => True
);

# The type here defines the return value of the routine.
my Bool &do-job = sub (Str $job --> Bool) {
    %todo{$job} = True;
};
比较 JavaScript 和 Raku 的类型

以下是 Raku 中一些 JavaScript 类型及其等价物的表格:

JavaScript

Raku

Object

Mu, Any, Hash

Array

List, Array, Seq

String

Str

Number

Int, Num, Rat

Boolean

Bool

Map

Map, Hash

Set

Set, SetHash

Object 既是 JavaScript 中所有类型的超类,也是创建哈希的方法。在 Raku 中,是所有类型的超类,尽管通常要使用 任何代替,这是的一个子类 Mu,而且几乎所有类型的超类,与 接线是一个例外。当 Object 用作哈希时,哈希就是你想要使用的。

有三种类型相当于 Array数组最相似 Array,因为它充当可变数组。列表类似于 Array,但是是不可变的。Seq用于创建惰性数组。

StringStr在大多数情况下使用相同。

Raku 中有几种不同的类型相当于 Number,但你最常见的三种是 IntNumRatInt 表示整数。Num 表示一个浮点数,使其最相似 NumberRat 表示两个数字的一小部分,并且在 Num 无法提供足够精确的值时使用。

BooleanBool在大多数情况下使用相同。

Map 既具有可变的,并且在 Raku 的不可变等效 Map是不可变的一个,并且 哈希是可变的一个。不要混淆他们!就像 Map 在 JavaScript 中,Map 并且 Hash 可以有任何类型的键或值,而不仅仅是钥匙串。

MapSet 也都一个可变的和 Raku 中一个不变的等效 Set 是不可变的一个,并且 SetHash 是可变的。

5.1.6. 函数

TBD

5.2. 面向对象编程

TBD

5.3. 异步编程

TBD

5.4. 网络 API

5.4.1. 网络

在 Raku 中,有两个用于处理网络的 API:(IO::Socket::INET 用于同步网络)和 IO::Socket::Async(用于异步网络)。

IO::Socket::INET 目前只支持 TCP 连接。它的 API 类似于 C 的套接字 API。如果您熟悉它,那么理解如何使用它不会花费很长时间。例如,这是一个 echo 服务器,它在收到第一条消息后关闭连接:

my IO::Socket::INET $server .= new:
    :localhost<localhost>,
    :localport<8000>,
    :listen;

my IO::Socket::INET $client .= new: :host<localhost>, :port<8000>;
$client.print: 'Hello, world!';

my IO::Socket::INET $conn = $server.accept;
my Str $msg               = $conn.recv;
say $msg; # OUTPUT: Hello, world!
$conn.print($msg);

say $client.recv; # OUTPUT: Hello, world!
$conn.close;
$client.close;
$server.close;

默认情况下,IO::Socket::INET 连接仅限 IPv4。要使用 IPv6,请 :family(PF_INET6) 在构建服务器或客户端时传递。

相反,IO::Socket::Async 支持 IPv4 和 IPv6,无需指定要使用的族。它还支持 UDP 套接字。以下是如何异步编写与上面相同的 echo 服务器(请注意,这 Supply.tap 是多线程的;如果这是不合需要的,请 Supply.act 改用:

my $supply = IO::Socket::Async.listen('localhost', 8000);
my $server = $supply.tap(-> $conn {
    $conn.Supply.tap(-> $data {
        say $data; # OUTPUT: Hello, world!
        await $conn.print: $data;
        $conn.close;
    })
});

my $client = await IO::Socket::Async.connect('localhost', 8000);
$client.Supply.tap(-> $data {
    say $data; # OUTPUT: Hello, world!
    $client.close;
    $server.close;
});

await $client.print: 'Hello, world!';

Node.js 中的等效代码如下所示:

const net = require('net');

const server = net.createServer(conn => {
    conn.setEncoding('utf8');
    conn.on('data', data => {
        console.log(data); # OUTPUT: Hello, world!
        conn.write(data);
        conn.end();
    });
}).listen(8000, 'localhost');

const client = net.createConnection(8000, 'localhost', () => {
    client.setEncoding('utf8');
    client.on('data', data => {
        console.log(data); # OUTPUT: Hello, world!
        client.end();
        server.close();
    });
    client.write("Hello, world!");
});

5.4.2. HTTP/HTTPS

Raku 本身不支持 HTTP/HTTPS。然而,像 Cro这样的 CPAN 包填补了这个空白。

5.4.3. DNS

Raku 目前不支持 Node.js 的 DNS 模块实现的大多数功能。IO::Socket::INET 并且 IO::Socket::Async 可以解析主机名,但尚未实现解析 DNS 记录和反向 IP 查找等功能。有些模块正在进行中,例如 Net::DNS::BIND ::Manage,旨在改善 DNS 支持。

5.4.4. Punycode

通过 CPAN 上的 Net::LibIDNNet::LibIDN2IDNA::Punycode 模块可以获得 Punycode 支持。

5.5. 文件系统 API

TBD

5.6. 模块和包

TBD

6. Ruby 到 Raku

6.1. 基本语法

6.1.1. 语句结束分号

Ruby 使用换行(有几个例外)来探测大部分语句的结束, 只要表达式已经完成。通过把运算符挂在行的末尾以保证解析会继续而打断一个长的表达式的做法很常见:

foo +     # 在 Ruby  中结尾的运算符意味着解析会继续
  bar +
  baz

在 Raku 中你必须显式地使用 ; 来结束语句, 这允许更好的反馈和更灵活的断行。有两个例外不需要显式的 ;, 块儿中的最后一条语句, 在块自身的闭合花括号之后(如果那一行上没有任何其它东西):

if 5 < $x < 10 {
    say "Yep!";
    $x = 17     # 在闭合花括号 } 之前不需要分号 ;
}               # 因为换行, 在闭合花括号 } 之后不需要分号 ;
say "Done!";    # 如果后面什么也没有, 那么这儿的分号也不需要

6.1.2. 空白

Ruby 中允许使用大量令人吃惊的灵活的空白, 即使在开启了严格模式和警告的情况下:

# 不符合习惯但是在 Ruby 中是合法的
puts"Hello "+
(people [ i]
    . name
    ) . upcase+"!"if$greeted[i]<1

Perl 6 也遵从程序员的自由和创造力,但是平衡的语法灵活性与其设计目标是一致的---确定性的,可扩展的语法,支持单程解析和有用的错误消息,组合功能,如利落地自定义运算符,不会导致程序员意外弄错他们的意图。 此外,不再强调 "代码高尔夫"; Raku 在概念上更为简洁, 而不是在少敲了几次键上。

因此,在语法中有很多地方,在 Ruby 中空格是可选的但是在 Raku 中却是强制的或禁止的。许多这些限制不太可能涉及很多现实的 Perl 代码(例如,在数组变量和它的方括号之间不允许有空格 ),但不幸的是有一些与某些 Ruby 黑客的习惯编码风格冲突:

  • 在参数列表的开括号「(」之前不允许有空格

foo (3, 4, 1); # 在 Ruby 或 Raku 中都不正确 ( 在 Raku 中这会
               # 尝试为 foo 传递一个 List 类型的单个参数)
foo(3, 4, 1);  # Ruby 和 Raku 中都可以
foo 3, 4, 1;   # Ruby 和 Raku 中都可以 - 圆括号是可供选择的-less style
  • 关键字后面立刻需要跟着空格

if(a < 0); ...; end         # OK in Ruby
if ($a < 0) { ... }         # Raku
if $a < 0   { ... }         # Raku, 更地道

while(x > 5); ...; end      # OK in Ruby
while ($x > 5) { ... }      # Raku
while $x > 5   { ... }      # Raku, 更地道
  • 后缀/后环缀 操作符(包括数组/散列下标)前面不允许有空格。

seen [ :fish ] = 1    # Ruby, 不地道, 但是允许这样写
%seen< fish > = 1;    # Raku, 'seen' 后面不允许出现空格
  • 中缀操作符之前需要空格, 如果它和已经存在的后缀/后环缀 操作符冲突的话。

n<1     # Ruby (in Raku this would conflict with postcircumfix < >)
$n < 1; # Raku

6.1.3. 方法调用, .send

方法调用使用点语法, 就像 Ruby 那样:

person.name    # Ruby
$person.name   # Raku

要调用一个直到运行时才直到名字的方法:

object.send(methodname, args);  # Ruby
$object."$methodname"(@args);   # Raku

如果你遗漏了双引号, 那么 Raku 会期望 $methodname 包含一个 Method 对象, 而不是单单是那个方法名的字符串表示。

6.1.4. 变量、符号、作用域 和通用类型

在 Ruby 中,变量主要使用 sigils 指示作用域。 $ 用于全局作用域,@@ 用于类作用域,@ 用作实例作用域,无符号用于局部变量(包括参数)。 & 符号也用于表示方法引用。符号的前缀为 :,但它们不是可变的,所以不是真正的符号。

在 Raku 中,符号主要用于指代包含的值实现的角色,表明值的类型(或至少接口)。 符号是不变的,不管变量是如何使用的 - 你可以把它们看作变量名的一部分。

变量的作用域改为由声明本身(my,has,our,etc)表示。

变量作用域

对于局部变量,Ruby 在赋值时使用隐式变量声明,并限于当前块。 在 Ruby 中,if 或 while 内置结构的内容不是块或作用域。

Raku 使用显式作用域指示符,并且不会隐式地创建变量。 每一个地方你看到的 {…​} 都是一个作用域,它包括一个条件或循环的主体。 常用的作用域声明:

foo = 7        # Ruby, variable scope is defined by first assignment and
               # extends to the end of the current block

my  $foo = 7   # Raku, lexical scoped to the current block
our $foo = 7   # Raku, package scoped
has $!foo = 7  # Raku, instance scoped (attribute)
$ 标量

$ 符号始终与"标量"变量(例如 $name)一起使用。这些是单值(single-value) 容器。

这是最通用的变量类型,对其内容没有限制。 注意,你仍然可以寻址/使用它的内容,如 $x[1]$x{"foo"}$f("foo")

@ 数组

@ 总是与"数组"变量一起使用(例如 @months@months[2]@months[2,4] 用于数组切片)。 使用 @ 符号的变量只能包含执行 Positional 角色的东西,Positional 角色指的是位置索引和切片功能。

  • 索引

puts months[2]; # Ruby
say @months[2]; # Raku
  • 值切片

puts months[8..11].join(',') # Ruby
say @months[8..11].join(',') # Raku
% 散列

% 符号始终与"散列"变量一起使用(例如`%calories`, %calories<apple>, %calories<pear plum>)。 使用 % 符号的变量只能包含执行关联(Associative)角色的内容。

Ruby 使用方括号来访问数组和哈希值。 Raku 使用花括号来代替散列。 尖括号版本也是可用的,它总是自动引起其内容(不带引号的字符串):

副词可以用来控制切片的类型。

  • 索引

puts calories["apple"]  # Ruby
say %calories{"apple"}; # Raku

puts calories["apple"]  # Ruby
puts calories[:apple]   # Ruby, symbols for keys are common
say %calories<apple>;   # Raku - angle brackets instead of single-quotes
say %calories«$key»;    # Raku - double angles interpolate as double-quotes
  • 值切片

puts calories.values_at('pear', 'plum').join(',') # Ruby
puts calories.values_at(%w(pear plum)).join(',')  # Ruby, pretty?

say %calories{'pear', 'plum'}.join(',');          # Raku
say %calories<pear plum>.join(',');               # Raku (prettier)
my $keys = 'pear plum';
say %calories«$keys».join(','); # Raku, interpolated split
  • 键/值切片

say calories.slice('pear', 'plum').join(','); # Ruby, with ActiveRecord
say %calories{'pear', 'plum'}:kv.join(',');   # Raku - 使用 :kv 副词
say %calories<pear plum>:kv.join(',');        # Raku (更好看的版本)
& Sub

& 符号与 Ruby 的 & 非常类似,用于引用一个具名的子例程/操作符的函数对象,而不调用它,即把名字用作"名词"而不是"动词"。 使用 & 符号的变量只能包含 Callable 角色的内容。

add = -> n, m { n + m } # Ruby lambda for an addition function
add.(2, 3)              # => 5, Ruby invocation of a lambda
add.call(2, 3)          # => 5, Ruby invocation of a lambda

my &add = -> $n, $m { $n + $m } # Raku addition function
&add(2, 3)                      # => 5, you can keep the sigil
add(2, 3)                       # => 5, and it works without it

foo_method = &foo;     # Ruby
my &foo_method = &foo; # Raku

some_func(&say) # Ruby pass a function reference
some_func(&say) # Raku passes function references the same way

通常在 Ruby 中,我们传递一个块作为最后一个参数,这是特别用于 DSL 中。 这可以是通过 yield 调用的隐式参数,也可以是带有前缀 & 的显式块。 在 Raku 中,Callable 参数总是被变量名称(而不是 yield)列出和调用,并且有多种调用函数的方法。

# Ruby, declare a method and call the implicit block argument
def f
  yield 2
end

# Ruby, invoke f, pass it a block with 1 argument
f do |n|
  puts "Hi #{n}"
end

# Raku, declare a method with an explicit block argument
sub f(&g:($)) {
  g(2)
}

# Raku, invoke f, pass it a block with 1 argument
# There are several other ways to do this
f(-> $n { say "Hi {$n}" }) # Explicit argument
f -> $n { say "Hi {$n}" }  # Explicit argument, no parenthesis
f { say "Hi {$^n}" }       # Implicit argument

# Additionally, if 'f' is a method on instance 'obj' you can use C<:>
# instead of parenthesis
obj.f(-> $n { say "Hi {$n}" })  # Explicit argument
obj.f: -> $n { say "Hi {$n}" }  # Explicit argument, no parenthesis
obj.f: { say "Hi {$^n}" }       # Implicit argument, no parenthesis
* 吞噬参数/ 参数扩展

在 Ruby 中,你可以声明一个参数,使用 * 前缀将所传递参数的剩余部分传递到数组中。 它在 Raku 中的工作方式相同:

def foo(*args); puts "I got #{args.length} args!"; end # Ruby
sub foo(*@args) { say "I got #{@args.elems} args!" }   # Raku

您可能想将数组扩展为一组参数。 在 Raku 中,这也使用 * 前缀:

args = %w(a b c)         # Ruby
foo(*args)

my @args = <a b c>       # Raku
foo(*@args)

Raku 有许多更高级的传递参数和接收参数的方法,参见签名捕获

Twigils

Raku 另外还使用 "twigs",它是关于变量的进一步指示符,并且在符号和变量名的其余部分之间。 例子:

$foo     # Scalar with no twigil
$!foo    # 私有实例变量
$.foo    # Instance variable accessor
$*foo    # Dynamically scoped variable
$^foo    # A positional (placeholder) parameter to a block
$:foo    # 具名参数
$=foo    # POD (文档) 变量
$?FILE   # Current source filename. ? twigil 表明这是一个编译时值
$~foo    # Sublanguage seen by parser, uncommon

虽然每个例子都使用 $ 符号,但大多数可以使用 @(Positional)或 %(Associative)。

: 符号

Raku 通常在 Ruby 使用符号的地方使用字符串。 关于这点的一个主要例子是散列键。

address[:joe][:street] # Typical Ruby nested hash with symbol keys
%address<joe><street>  # Typical Raku nested hash with string keys

Raku 有冒号对语法,有时看起来像 Ruby 符号。

:age            # Ruby symbol

# All of these are equivalent for Raku
:age            # Raku pair with implicit True value
:age(True)      # Raku pair with explicit True value
age => True     # Raku pair using arrow notation
"age" => True   # Raku pair using arrow notation and explicit quotes

很多时候你可能会使用一个没有显式值的冒号对,并假装它是一个 Ruby 符号,但它不是惯用的 Raku。

6.2. 操作符

许多操作符在 Ruby 和 Raku 中有类似的用法:

  • , 列表分割符

  • + 数值加法

  • - 数值减法

  • * 数值乘法

  • / 数值除法

  • % 数值求模

  • ** 数值指数

  • ! && || 布尔, 高优先级

  • not and or 布尔, 低优先级

您可以使用 $x` 而不是 `x += 1` 作为递增变量的快捷方式。这可以用作预增量 `$x(增量,返回新值)或后增量 $x++(增量,返回旧值)。

您可以使用 $x-- 而不是 x -= 1 作为递减变量的快捷方式。这可以用作预减量 --$x(递减,返回新值)或递减后 $x--(递减,返回旧值)。

6.2.1. == != < > ⇐ >= 比较

Raku 中, 数字和字符串之间比较是分开的,以避免常见错误。

  • == != < > ⇐ >= 比较

  • eq ne lt gt le ge 字符串比较

例如,使用 == 尝试将值转换为数字,并且 eq 尝试将值转换为字符串。

6.2.2. <⇒ 三向比较

在 Ruby 中,<⇒ 运算符返回 -1,0 或 1。 在 Raku 中,它们返回 `Order

Less`,Order :: SameOrder :: More

<⇒ 用于强制数字上下文比较。

leg("Less,Equal 或者 Greater?")用于强制字符串上下文比较。

cmp 要么是 <⇒ 比较, 要么是 leg 比较,这取决于它的参数的现有类型。

6.2.3. ~~ 智能匹配运算符

这是一个非常常见的匹配运算符,它不存在于 Ruby 中。这里有些例子:

say "match!" if $foo ~~ /bar/;      # Regex match
say "match!" if $foo ~~ "bar";      # String match
say "match!" if $foo ~~ :(Int, Str) # Signature match (destructure)

6.2.4. & | ^ 数字位操作

6.2.5. & | ^ 布尔运算

在 Raku 中,这些单字符操作被移除了,并被两个字符操作代替,它们将它们的参数强制到所需的上下文中。

# Infix ops (two arguments; one on each side of the op)
+&  +|  +^  And Or Xor: Numeric
~&  ~|  ~^  And Or Xor: String
?&  ?|  ?^  And Or Xor: Boolean

# Prefix ops (one argument, after the op)
+^  Not: Numeric
~^  Not: String
?^  Not: Boolean (same as the ! op)

6.2.6. &. 条件链式操作符

Ruby 使用 &. 运算符链接方法,而不会在一个返回 nil 的调用中产生错误。在 Raku 中因为同样的目的使用 .?

6.2.7. << >> 数值左/右移位操作符,铲(shovel)操作符

替换为 +<+>

puts 42 << 3  # Ruby
say  42 +< 3; # Raku

注意,Ruby 经常使用 << 运算符作为"铲操作符",这类似于`.push`。这种用法在 Raku 中不常见。

6.2.8. ⇒`和 `: 键-值分隔符

在 Ruby 中, 用于 Hash 字面声明和参数传递的键/值对的上下文中。 当左边是符号时用 : 作速记符。

在 Raku 中, 是对(Pair)运算符,这在原理上是非常不同的,但在许多情况下工作相同。

如果你在哈希字面值中使用 ,那么用法非常类似:

hash = { "AAA" => 1, "BBB" => 2 }  # Ruby, though symbol keys are more common
my %hash = ( AAA => 1, BBB => 2 ); # Raku, uses ()'s though {} usually work

6.2.9. ?: 三目运算符

在 Raku 中,这被拼写为两个问号,而不是一个问号,和两个感叹号而不是一个冒号。这种与常见三目运算符的偏离消除了多种歧义的情况,并使得假的情况更突出。

result     = (  score > 60 )  ? 'Pass'  : 'Fail'; # Ruby
my $result = ( $score > 60 ) ?? 'Pass' !! 'Fail'; # Raku

6.2.10. + 字符串连接

替换为波浪线符号(~)。助记符:想想用针和线缝合两个字符串。

$food = 'grape' + 'fruit'  # Ruby
$food = 'grape' ~ 'fruit'; # Raku

6.2.11. 字符串插值

在 Ruby 中,"{foo}s" 界定嵌入在双引号字符串中的块。在 Raku 中删除 前缀:"{$foo}s"。和 Ruby 一样,你可以将任意代码放在嵌入式块中,它将在字符串上下文中渲染。

简单变量可以插入到双引号字符串中,而不使用块语法:

# Ruby
name = "Bob"
puts "Hello! My name is #{name}!"

# Raku
my $name = "Bob"
say "Hello! My name is $name!"

Ruby 中的嵌入式块的结果使用 .to_s 来获取字符串上下文。 Raku 使用 .Str.gist 得到相同的效果。

6.3. 复合语句

6.3.1. 条件

if elsif else unless§

这在 Ruby 和 Raku 之间非常相似,但是 Raku 使用 {} 来清楚地描述块。

# Ruby
if x > 5
    puts "Bigger!"
elsif x == 5
    puts "The same!"
else
    puts "Smaller!"
end

# Raku
if x > 5 {
    say "Bigger!"
} elsif x == 5 {
    puts "The same!"
} else {
    puts "Smaller!"
}

将条件表达式绑定到变量上有一点不同:

if x = dostuff(); ...; end   # Ruby
if dostuff() -> $x {...}     # Raku, block-assignment uses arrow

unless 条件仅允许 Raku 中的单个块; 它不允许 elsifelse 子句。

cese-when

Raku 的 given-when 结构像一个 if-elsif-else 语句链或者类似于 Ruby 中的 case-when。一个很大的区别是,Ruby 使用 == 比较每个条件,但 Raku 使用更一般的智能匹配 ~~ 运算符。

它具有以下一般结构:

given EXPR {
    when EXPR { ... }
    when EXPR { ... }
    default { ... }
}

在其最简单的形式中,构造如下:

given $value {
    when "a match" {
        do-something();
    }
    when "another match" {
        do-something-else();
    }
    default {
        do-default-thing();
    }
}

这在 when 语句中匹配标量值的情况下是简单的。更一般地,匹配实际上是对输入值的智能匹配,使得可以使用更复杂的诸如正则表达式的实体的而非标量值来查找。

6.3.2. 循环

while until

大部分不变;圆括号周围的条件是可选的,但如果使用了,不能立即跟随关键字,否则它将被视为一个函数调用。将条件表达式绑定到变量上也有一些不同:

while x = dostuff(); ...; end    # Ruby
while dostuff() -> $x {...}      # Raku
for .each

for 循环在 Ruby 中是罕见的,我们通常在可枚举上使用 .each。对 Raku 的最直接的翻译是对 .each.map 都使用 .map,但是我们通常直接使用 for 循环。

# Ruby for loop
for n in 0..5
    puts "n: #{n}"
end

# Ruby, more common usage of .each
(0..5).each do |n|
    puts "n: #{n}"
end

# Raku
for 0..5 -> $n {
    say "n: $n";
}

# Raku, mis-using .map
(0..5).map: -> $n {
    say "n: $n";
}

在 Ruby 中,.each 的迭代变量是列表元素的副本,修改它对原始列表没有影响。请注意,它是 REFERENCE 的副本,因此您仍然可以更改其引用的值。

在 Raku 中,该别名是只读的(为了安全起见),因此它的行为与 Ruby 完全一样,除非把 改为 <→

cars.each { |car| ... }    # Ruby; read-only reference
for @cars  -> $car   {...} # Raku; read-only
for @cars <-> $car   {...} # Raku; read-write

6.3.3. 流程中断语句

与 Ruby 相同:

  • next

  • redo

  • break

这在 Raku 中是 last

6.4. 正则表达式(Regex / Regexp)

Raku 中的正则表达式与 Ruby 中的正则表达式明显不同,它更强大。例如,默认情况下,Raku 将忽略空格,所有字符必须转移。正则表达式可以很容易地以组合和声明的方式建立高效的 grammars。

有很多强大的 Raku regex 的特性,特别是使用相同的语法定义整个 gramamrs。请参阅正则表达式Grammars

6.4.1. .match 方法和 =~ 运算符

在 Ruby 中,可以使用 =~ regexp 匹配运算符或 .match 方法对变量执行正则表达式匹配。在 Raku 中,使用 ~~ 智能匹配运算符,或 .match 方法。

next if line   =~ /static/   # Ruby
next if $line  ~~ /static/;  # Raku

next if line  !~  /dynamic/ ; # Ruby
next if $line !~~ /dynamic/ ; # Raku

next if line.match(/static/)    # Ruby
next if $line.match(/static/);  # Raku

或者,可以使用 .match`和 `.subst 方法。注意 .subst 是不可变的。参见 S05/替换

6.4.2. .sub.sub!

在 Raku 中,通常使用 s/// 运算符来执行正则表达式替换。

fixed = line.sub(/foo/, 'bar')        # Ruby, non-mutating
my $fixed = $line.subst(/foo/, 'bar') # Raku, non-mutating

line.sub!(/foo/, 'bar')   # Ruby, mutating
$line ~~ s/foo/bar/;      # Raku, mutating

6.4.3. 正则表达式选项

将任何选项从正则表达式的结尾移动到开头。这可能需要您在 /abc/ 等纯匹配中添加可选的 m

next if $line =~    /static/i # Ruby
next if $line ~~ m:i/static/; # Raku

6.4.4. 空格被忽略,大多数东西必须被引起来

为了帮助可读性和可重用性,在 Raku 的正则表达式中,空格并不重要。

/this is a test/ # Ruby, boring string
/this.*/         # Ruby, possibly interesting string

/ this " " is " " a " " test / # Raku, each space is quoted
/ "this is a test" / # Raku, quoting the whole string
/ this .* /          # Raku, possibly interesting string

6.4.5. 特殊匹配器通常属于 <> 语法

Raku 的正则表达式有很多支持特殊匹配语法的情况。它们不会全部列在这里,但通常不是被 () 包围,断言将被 <> 包围。

对于字符类,这意味着:

  • [abc] 变为 <[abc]>

  • [^abc] 变为 ←[abc]>

  • [a-zA-Z] 变为 <[a..zA..Z]>

  • 变为 <:upper>

  • [abc[:upper:]] 变为 <[abc]+:Upper>

对于环视断言:

  • (?=[abc]) 变为 <?[abc]>

  • (?=ar?bitrary* pattern) 变为 <before ar?bitrary* pattern>

  • (?!=[abc]) 变为 <![abc]>

  • (?!=ar?bitrary* pattern) 变为 <!before ar?bitrary* pattern>

  • (?⇐ar?bitrary* pattern) 变为 <after ar?bitrary* pattern>

  • (?<!ar?bitrary* pattern) 变为 <!after ar?bitrary* pattern>

  • (Unrelated to <> syntax, the "lookaround" /foo\Kbar/ 变为 /foo <( bar )> /

  • (?(?{condition))yes-pattern|no-pattern) 变为 [ <?{condition}> yes-pattern | no-pattern ]

6.4.6. 最长令牌匹配(LTM)替代交替

在 Raku regexes 中,| 执行最长令牌匹配(LTM),它决定哪个备选分支根据一组规则赢得模棱两可的匹配,而不是根据在正则表达式中首先写出哪个备选分支。

要避免新的逻辑,请在你的 Ruby 正则表达式中把任何 | 更改为 ||

6.5. 文件相关操作

6.5.1. 将文本文件的行读入数组

Ruby 和 Raku 都很容易将文件中的所有行读取到单个变量中,在这两种情况下,每一行都删除了换行符。

lines = File.readlines("file")   # Ruby
my @lines = "file".IO.lines;     # Raku, create an IO object from a string

6.5.2. 迭代文本文件的行

不建议将整个文件读入内存。 Raku 中的 .lines 方法返回一个延迟序列,但是赋值给数组会强制读取文件。最好迭代结果:

# Ruby
File.foreach("file") do |line|
    puts line
end

# Raku
for "file".IO.lines -> $line {
    say $line
}

6.6. 面向对象

6.6.1. 基本类,方法,属性

在 Ruby 和 Raku 之间类的定义是相似的。 Ruby 使用 def 定义方法,而 Raku 使用 method 定义方法。

# Ruby
class Foo
    def greet(name)
        puts "Hi #{name}!"
    end
end

# Raku
class Foo {
    method greet($name) {
        say "Hi $name!"
    }
}

在 Ruby 中,你可以使用一个属性而不预先声明它,你可以告诉它这是一个属性,因为 @ 符号。您还可以使用 attr_accessor 及其变体轻松创建访问器。在 Raku 中,你使用 has 声明符和各种符号。你可以使用 ! twigil 作为私有属性或 . 创建一个访问器。

# Ruby
class Person
    attr_accessor :age    # Declare .age as an accessor method for @age
    def initialize
        @name = 'default' # Assign default value to private instance var
    end
end

# Raku
class Person {
    has $.age;              # Declare $!age and accessor methods
    has $!name = 'default'; # Assign default value to private instance var
}

使用 .new 方法创建类的新实例。在 Ruby 中,您必须在 initialize 内根据需要手动给实例变量赋值。在 Raku 中,您将获得一个接受访问器属性的键/值对的默认构造函数,并可以在 BUILD 方法中进一步设置。像 Ruby 一样,你可以重写 new 自身以获取更高级的功能,但这是罕见的。

# Ruby
class Person
    attr_accessor :name, :age
    def initialize(attrs)
        @name = attrs[:name] || 'Jill'
        @age  = attrs[:age] || 42
        @birth_year = Time.now.year - @age
    end
end
p = Person.new( name: 'Jack', age: 23 )

# Raku
class Person
    has $.name = 'Jill';
    has $.age  = 42;
    has $!birth_year;
    method BUILD {
        $!birth_year = now.Date.year - $.age;
    }
}
p = Person.new( name => 'Jack', age => 23 )

6.6.2. 私有方法

Raku 中的私有方法声明的时候在他们的名字前置一个 ! 符号,并且调用的时候使用 ! 代替 .

# Ruby
class Foo
    def visible
        puts "I can be seen!"
        hidden
    end

    private
    def hidden
        puts "I cannot easily be called!"
    end
end
# Raku
class Foo {
    method visible {
        say "I can be seen!"
        self!hidden
    }

    method !hidden {
        say "I cannot easily be called!"
    }
}

一个重要的注意事项是,在 Ruby 中孩子对象可以看到父对象中的私有方法(所以他们更像是其他语言中的"受保护"的方法)。在 Raku 中,孩子对象不能调用父对象中的私有方法。

6.6.3. 元

这里有一些元编程的例子。注意,Raku 将元方法与常规方法分离开了。

person = Person.new       # Ruby, create a new person
my $person = Person.new   # Raku, create a new person

person.class              # Ruby, returns Person (class)
$person.WHAT              # Raku, returns Person (class)

person.methods            # Ruby
$person.^methods          # Raku, using .^ syntax to access meta-methods

person.instance_variables # Ruby
$person.^attributes       # Raku

像 Ruby 一样,在 Raku 中,一切都是对象,但并不是所有的操作都等同于 .send。许多运算符是使用类型化多重分派(具有类型的函数签名)来决定使用哪个实现的全局函数。

5.send(:+, 3)    # => 8, Ruby
&link:5, 3[+]       # => 8, Raku, reference to infix addition operator

&[+].^candidates # Raku, lists all signatures for the + operator

有关更多详细信息,请参阅元对象协议

6.7. 环境变量

6.7.1. Perl 模块库路径

在 Ruby 中,为模块指定额外搜索路径的环境变量之一是 RUBYLIB

$ RUBYLIB="/some/module/lib" ruby program.rb

在 Raku 中,这是相似的,你只需要更改名称。正如你可能猜到的,你只需要使用 PERL6LIB

$ PERL6LIB="/some/module/lib" raku program.p6

与 Ruby 一样,如果不指定 PERL6LIB,则需要通过 use lib 指令在程序中指定库路径:

# Ruby and Raku
use lib '/some/module/lib';

6.8. Misc.

6.8.1. 从模块导入特定函数

在 Ruby 中没有内置的方法来选择性地从模块中导入/导出方法。

在 Raku 中,通过在相关的 subs 上使用 "is export" 角色来指定要导出的函数,然后导出所有具有此角色的 subs。因此,下面的 Bar 模块导出 subs foobar,但不导出 baz

unit module Bar; # remainder of the file is in module Bar { ... }

sub foo($a) is export { say "foo $a" }
sub bar($b) is export { say "bar $b" }
sub baz($z) { say "baz $z" }

要使用此模块,只需 use Bar,函数 foobar 将可用

use Bar;
foo(1);    #=> "foo 1"
bar(2);    #=> "bar 2"

如果您尝试使用 baz, 那么在编译时会引发 "Undeclared routine" 的错误。

一些模块允许选择性地导入函数,它们看起来像:

use Bar <foo>; # Import only foo
foo(1);        #=> "foo 1"
bar(2);        # Error!

6.9. OptionParser,解析命令行标志

Raku 中的命令行参数开关解析由 MAIN 子例程的参数列表完成。

# Ruby
require 'optparse'
options = {}
OptionParser.new do |opts|
    opts.banner = 'Usage: example.rb --length=abc'
    opts.on("--length", "Set the file") do |length|
        raise "Length must be > 0" unless length.to_i > 0
        options[:length] = length
    end
    opts.on("--filename", "Set the file") do |filename|
        options[:file] = filename
    end
    opts.on("--verbose", "Increase verbosity") do |verbose|
        options[:verbose] = true
    end
end.parse!

puts options[:length]
puts options[:filename]
puts 'Verbosity ', (options[:verbose] ? 'on' : 'off')
ruby example.rb --filename=foo --length=42 --verbose
    42
    foo
    Verbosity on

ruby example.rb --length=abc
    Length must be > 0
# Raku
sub MAIN ( Int :$length where * > 0, :filename = 'file.dat', Bool :$verbose ) {
    say $length;
    say $data;
    say 'Verbosity ', ($verbose ?? 'on' !! 'off');
}
raku example.p6 --file=foo --length=42 --verbose
    42
    foo
    Verbosity on
raku example.p6 --length=abc
    Usage:
      c.p6 [--length=<Int>] [--file=<Any>] [--verbose]

注意,Raku 在命令行解析错误时会自动生成一个完整的使用消息。

6.10. RubyGems,外部库

请参阅 https://modules.raku.org/,其中提供了越来越多的 Raku 库以及管理它们的工具。

如果您使用的模块尚未转换为 Raku,并且本文档中未列出任何备选方案,那么它在 Raku 下的使用可能尚未解决。

你可以尝试使用 Inline::Ruby 从 Raku 程序中调用现有的 Ruby 代码。这使用 ruby 解释器的嵌入式实例来运行从 Raku 脚本调用的 Ruby 代码。注意,这是一个 EXPERIMENTAL 库。类似地你可以使用 Inline::Perl5Inline::Python 和其他调用其他语言的库。

7. Haskell 到 Raku - 简而言之

Haskell 和 Raku 是非常不同的语言。这很明显。 但是,这并不意味着没有相似之处或共同的想法! 此页面尝试让一个 Haskell 用户启动并运行 Raku。Haskell 用户可能会发现,在用 Raku 编写脚本时,他们不需要放弃所有 Haskelly 的想法。

请注意,这不应该被误认为是初学者教程或 Raku 概述; 它旨在作为具有强大 Haskell 背景的 Raku 学习者的技术参考。

7.1. 类型

7.1.1. 类型 vs 值

在 Haskell 中, 您有类型级编程, 然后进行值级编程。

plusTwo :: Integer -> Integer   -- Types
plusTwo x = x + 2               -- Values

您不要像下面那样在 Haskell 中混合类型和值。

plusTwo 2          -- This is valid
plusTwo Integer    -- This is not valid

在 Raku 中, 类型(亦称为类型对象)和值处于同样的级别

sub plus-two(Int $x --> Int) { $x + 2 }

plus-two(2);   # This is valid
plus-two(Int); # This is valid

我将再用一个例子来说明 Raku 这个独特之处:

multi sub is-string(Str $ --> True)  {}
multi sub is-string(Any $ --> False) {}

is-string('hello');    #True
is-string(4);          #False

7.1.2. Maybe

在 Haskell 中,您有一个 Maybe 类型, 可以让您放弃空类型的烦恼。 假设您有一个将 String 解析为 Integer 的假设函数:

parseInt :: String -> Maybe Integer

case parseInt myString of
  Just x  -> x
  Nothing -> 0

在 Raku 中, 由于类型对象与常规对象共存,因此我们拥有 DefinedUndefined 对象的概念。 平常的类型对象是 undefined 的, 而实例化后的对象是 defined 的。

8. Perl 5 到 Raku 的指南 - 函数

由于太长, 请参阅原文。

9. Perl 到 Raku 的指南 - 运算符

9.1. 描述

一个(希望)全面的 Perl 5 运算符列表及其 Raku 等价物,并在必要时记录它们之间的差异。

9.2. 注意

本文档*没有*详细解释运算符。本文档旨在指导您从 Perl 5 perlop 文档中的操作符过渡到 Raku 中的等效文档。有关 Raku 等效文档的完整文档,请参阅Raku 文档

9.2.1. 运算符优先级和关联性

运算符优先级表在 Raku 中与在 Perl 5 中有所不同,因此这里不再详述。如果您需要知道 Raku 中给定运算符的优先级和关联性,请参阅运算符优先级

9.2.2. 项和列表运算符

Perl 5 perlop 文档中列出的作为一元运算符和列表运算符的内容在这个章节里往往可以被视为函数,例如 printchdir。因此,您可以在函数指南中找到有关它们的信息。括号仍用于分组。有一点需要注意:在 Raku 中,是`,`(逗号)创建列表而不是圆括号。所以:

my @foo = 1,2,3,4,5;   # no parentheses needed
.say for 1,2,3,4,5;    # also no parentheses

my $scalar = (1);      # *not* a list, as there is no comma
my $list   = (1,);     # a List in a scalar container

9.2.3. 箭头运算符

由于您通常不会在 Raku 中使用引用,因此箭头作为解除引用运算符可能不太有用。但是,如果您确实需要解引用某些内容,则箭头就是点号。它也是方法调用的中的点号。因此,Perl 5 中的 $arrayref→7 在 Raku 中变成 $arrayref.[7],类似地, $user→name 变成了 $user.name 箭头用于构建 Pair 对,参考[Pair 术语文档]。

9.2.4. 自动递增和自动递减

和 Perl 5 中工作的一样。一个可能的警告是对于 ` 它们调用 `succ` 方法, 对于 `--` 他们调用 `pred` 方法。对于内置数字类型,这是不太可能做一些不寻常的,但自定义类型可以定义自己的 `succ` 和 `pred` 方法,所以在这种情况下,你应该注意的是什么是 `-- *真正*能做的。

9.2.5. 指数

像你期望的那样工作。在 Perl 5 的 perlop 中关于 绑定比一元减号更紧密的警告(即 -2 4 被计算为 -(2 4) 而不是 (-2) 4)也适用于 Raku。

9.2.6. 符号一元运算符

如在 Perl 5 中那样,一元 !- 进行逻辑和算术否定。?^ 用于按位逻辑否定,文档指出这相当于 !。值得注意的是,这些分别强制他们的参数 为 BoolNumeric 类型。

一元 ~ 是 Raku 中的字符串上下文运算符,因此使用前缀 +^ 进行逐位整数否定。假设两个补码。

+ *确实*在 Raku 中产生作用,强转其参数为数值类型。

一元 \ 没有了。如果你真的想要对现有的命名变量进行“引用”,你可以使用项上下文,如下所示:$aref = item(@array) 或者可以通过更熟悉的前缀 $$aref = $@array。请注意,您并没有真正获得引用,而是一个带有引用对象的标量容器。

您可以使用 & sigil 获取命名子例程的“引用” :$sref = &foo。匿名数组,散列和 sub 创建过程中立即返回底层对象:$sref = sub { }

9.2.7. 绑定运算符

=~!~ 分别被 ~~!~~ 取代。那些认为 Perl 5 中的智能匹配坏掉的人会很高兴听到它在 Raku 中运行得更好,因为更强的坚定意味着更少的猜测。有关smartmatch在 Raku 中的工作原理的更详细说明,请参阅smartmatch 文档

9.2.8. 乘法运算符

二元 */% 分别执行乘法,除法和取模运算,和 Perl 5 中一样。

二元 x 运算符在 Raku 中略有不同,并且他有一个同伴儿。print '-' x 80; 给你一个 80 个破折号的字符串,但是对于 @ones = (1) x 80; 给你一个 80 个 “1” 的列表的 Perl 5 行为,你会使用 @ones = 1 xx 80;

9.2.9. 加法运算符

二元 +- 分别进行加法和减法运算,如您所料。

由于 . 是方法调用运算符,所以二元 ~ 在 Raku 中充当字符串连接运算符。

9.2.10. Shift operators

<<>> 已被 +<+> 取代。

9.2.11. 命名一元运算符

如上所述,您可以在函数指南中找到它们。

9.2.12. 关系运算符

这些都像 Perl 5 中那样工作。

9.2.13. 相等运算符

==!= 都像 Perl 5 中那样工作。

<⇒ ` 和 `cmp 在 Raku 中有不同的行为。<⇒ ` 做数值比较,但返回 `Order::LessOrder::Same 或者 Order::More 而不是Perl 5 中的 -101。要获得 cmp 的 Perl 5 行为(使用它返回 Order 对象而不是整数的更改),您应该使用 leg 运算符。

cmp 要么做 <⇒ 要么做 leg,这取决于其参数的现有类型。

~~ 是 Perl 5 中的智能匹配运算符,但它也*只是* Raku 中的匹配运算符,如上所述。有关智能匹配在 Raku 中的工作原理,请参阅smartmatch 文档

9.2.14. 智能匹配运算符

有关smartmatch在 Raku 中的工作原理的更详细说明,请参阅smartmatch 文档

9.2.15. Bitwise And

二元 & 在 Raku 中是 +&

9.2.16. Bitwise Or and Exclusive Or

按位或已经从 Perl 5 中的 | 变成 Raku 中的 +|。同样地,按位 XOR ^ 变成了 +^

9.2.17. C-style 的逻辑和

不变。

9.2.18. C-style 逻辑或

不变。

9.2.19. Logical Defined-Or

在 Raku 中保持为 //。返回第一个有定义的操作数,或者返回最后一个操作数。此外,还有一个低优先级版本,称为 orelse

9.2.20. Range 运算符

在列表上下文中,.. 作为范围运算符运行,不需要更改。也就是说,存在可能有用的排他性范围运算符。这些是:

  • 中缀 ..^ 不包括末端;

  • 中缀 ^.. 不包括起点;

  • 中缀 .. 不包括起点和末端;

  • ^ 从零开始的前缀,不包括末端。

以下示例显示了所有上述范围运算符的效果(请注意圆括号仅用于允许方法调用):

(1..^5).list;  # (1 2 3 4)
(1^..5).list;  # (2 3 4 5)
(1^..^5).list; # (2 3 4)
(^5).list;     # (0 1 2 3 4)

在 Perl 5 中,在标量上下文中,运算符 ..…​ 像触发器(flip-flop)操作符一样,即使它们鲜为人知且可能较少使用。Raku 中的那些运算符分别由fffff代替。

9.2.21. 条件运算符

条件运算符 ?: 已替换为 ?? !!:

$x = $ok  ? $yes  : $no;  # Perl 5
$x = $ok ?? $yes !! $no;  # Raku

9.2.22. 赋值运算符

虽然没有完整记录,但 S03 表明数学和逻辑赋值运算符应该像您期望的那样工作。一个值得注意的变化是 .= 在左侧的对象上调用可变方法(也可以是类型对象)。这允许以下有用的惯用法:

class LongClassName {
    has $.frobnicate;
}
my LongClassName $bar .= new( frobnicate => 42 ); # no need to repeat class name

这确保了 $bar 只能包含一个 LongClassName 对象,并且不必重复(并且可能拼写错误)类名。

~= 是字符串连接赋值,正如您可能期望的更改 .~。此外,按位赋值运算符可能不会分为数字和字符串版本(&= 等等,相对 &.= 等),因为该功能目前在 Perl 5 本身中是实验性的 - 尽管,这并没有具体记录。

9.2.23. 逗号运算符

逗号运算符大多按预期工作,但从技术上讲,它创建列表)或分隔函数调用中的参数。此外,还有一个 : 变体可以将函数调用转换为方法调用 - 请参阅此页面

`⇒ `运算符,或*胖箭头*,工作方式类似于 Perl 5 的“胖逗号”,因为它允许在其左侧的无引号(普通)标识符,但在 Raku 中它构造 Pair 对象,而不是仅仅作为分隔符发挥作用。如果您试图将一行 Perl 5 代码直接翻译为 Raku,它应该会按预期运行。

9.2.24. 列表运算符 (rightward)

与命名一元运算符一样,您可以在函数下找到这些。

9.2.25. 逻辑非

! 的优先级较低版本。对于 !,强转其参数为 Bool

9.2.26. 逻辑和

如 Perl 5 中的 较低优先级版本的 && 一样。

9.2.27. 逻辑或或独占或

or 是低优先级版本的 ||,并且 xor 是低优先级版本的 ^^

此外,还有一个低优先级版本的 //,称为 orelse

9.2.28. 引用和引用类似的运算符

有关引用构造的所有详细信息,请参阅引用

有一个引用运算符,允许绝对的文字字符串:Q 或者 「…」,尽管后者可能很难在你的键盘上找到,这取决于你的键盘…​…​反斜杠转义也*没有*应用在 Q 引用的字符串上。例如 Q{This is still a closing curly brace → \} 合成的是 "This is still a closing curly brace → \"。

q 做你期望的,允许反斜杠转义。例如 q{This is not a closing curly brace → \}, but this is → } 返回 "This is not a closing curly brace → }, but this is →"。与 Perl 5 一样,您可以使用单引号获得此行为。

qq 允许变量插值。但是,默认情况下,只插入标量变量。要获得其他变量插值,您需要在它们后面放置方括号(所谓的zen-slice)以使它们进行插值。例如:

my @a = <1 2 3>;
say qq/@a[] example@example.com/;

结果为 “1 2 3 example@example.com”。哈希以相同的方式进行插值:

my %a = 1 => 2, 3 => 4;
say "%a{}";

导致空格分隔 Pair 对儿, Tab 将每对中的键与值分开(因为这是 Pair 的标准字符串化,并且哈希在字符串化时充当 Pair 的列表)。您还可以使用花括号在字符串中插入 Raku 代码。有关所有详细信息,请参阅插值

qw 像 Perl 5 中那样工作,也可以呈现为 <…​>。例如 qw/a b c/ 相当于 <a b c>

还有一个能插值的 qw 版本,即 qqw。所以:

my $a = 42;
say qqw/$a b c/;

给你 “42 b c”。

Shell 引用可以通过 qx 获得,但是你应该注意,反引号不像 Perl 5 那样进行 shell 引用,并且 Perl 变量*不*在 qx 字符串中进行插值。如果需要在 shell 命令字符串中插入 Perl 变量,则可以改为使用 qqx

Raku 中没有 qr 运算符了。

tr/// 与 Perl 5 中的工作方式类似。需要注意的是范围的指定方式不同。您可以使用“a..z”代替使用范围“a-z”,即使用 Perl 的范围运算符。tr/// 有一个方法版本,记录的更好,称为 .trans.trans 使用 Pair 对儿的列表,如下所示:可以在https://design.raku.org/S05.html#Transliteration 中找到更广泛的使用说明。

$x.trans(
    ['a'..'c'] => ['A'..'C'],
    ['d'..'q'] => ['D'..'Q'],
    ['r'..'z'] => ['R'..'Z']
    );

等价的 y/// 已经废除了。

在 Raku 中 :to 以不同方式指定了 Heredocs。您可以使用引号运算符,例如,q:to/END/; 将以 “END” 开头的 heredoc 结尾。类似地,您可以根据引用运算符进行转义和插值,即带有 Q 的文字值, 带有 q 的反斜杠转义和带有插值的 qq

9.2.29. I/O 运算符

有关 Raku 中输入/输出的完整详细信息,请参阅io

因为 <…​> ` 与 Raku 中的 quote-words 构造一样,<> ` 不用于从文件中读取行。您可以通过 IO 从文件名创建对象或使用打开的文件句柄然后在任何一种情况下在它身上调用 .lines 来实现。或者例如 my @a = "filename".IO.lines;my $fh = open "filename", :r;my @a = $fh.lines;(在后一种情况下,我们使用 :r 专门打开用于读取的文件)。要以迭代方式执行此操作,可以用以下方式 使用 for 循环:

for 'huge-csv'.IO.lines -> $line {
    # Do something with $line
}

注意那里的 用法。这是块语法的一部分,而在 Raku 中要用在 ifforwhile 等块中。

如果你想将整个文件 slurp 为标量,你会惊讶的!- 使用 .slurp 方法。例如:

my $x = "filename".IO.slurp;
=== ... or ...
my $fh = open "filename", :r;
my $x = $fh.slurp;

特殊变量指南中所述,ARGV 魔术输入文件句柄已被替换 $*ARGFILES,并且`@ARGV` 命令行参数数组已被替换 @*ARGS

9.2.30. No-ops

1 while foo(); 与 Perl 5 中的工作方式相同,但它会生成警告。在 Raku 中,这个惯用法现在被写成了 Nil while foo();

9.2.31. 按位字符串运算符

单独记录在上面了,但总结如下…​…​

按位整数否定加上前缀 +^。按位布尔否定是 ?^

按位与是 +&

按位整数或是 +|。按位整数 xor 是中缀 +^。按位布尔或是 ?|

左移和右移是 +<+>

10. Python 到 Raku - 简而言之

此页面试图为来自 Python 背景的人们提供学习 Raku 的方法。我们在 Raku 中讨论了许多 Python 构造和惯用法的等价语法。

10.1. 基本语法

10.1.1. Hello, world

让我们从打印 "Hello, world!" 开始吧。 Raku 中的 put 关键字相当于 Python 中的 print。与 Python 2 一样,括号是可选的。换行符添加到行尾。

  • Python 2

print "Hello, world!"
  • Python 3

print("Hello, world!")
  • Raku

put "Hello, world!"

还有 say 关键字,其行为类似,但会调用其参数的 gist 方法。

  • Raku

my $hello = "Hello, world!";
say $hello;  # also prints "Hello, world!"
             # same as: put $hello.gist

在 Python 中 '" 是可互换的。在 Raku 中两者都可用于引用, 但双引号(")表示应该进行插值。例如, 以 $ 开头的变量和包含在花括号中的表达式会被插值。

  • Raku

my $planet = 'earth';
say "Hello, $planet";   # Hello, earth
say 'Hello, $planet';   # Hello, $planet
say "Hello, planet number { 1 + 2 }"; # Hello, planet number 3

10.1.2. 语句分隔符

在 Python 中,换行符表示语句的结束。有一些例外:换行符之前的反斜杠继续跨行语句。此外,如果有一个不匹配的开括号,方括号或花括号,则该语句将继续跨行,直到匹配的花括号被关闭。

在 Raku 中,分号表示语句的结束。如果分号是块的最后一个语句,则可以省略分号。如果有一个结束花括号后跟换行符,也可以省略分号。

  • Python

print 1 + 2 + \
    3 + 4
print ( 1 +
    2 )
  • Raku

say 1 + 2 +
    3 + 4;
say 1 +
    2;

10.1.3. 块儿

在 Python 中,缩进用于表示块。 Raku 使用花括号表示块儿。

  • Python

if 1 == 2:
    print "Wait, what?"
else:
    print "1 is not 2."
  • Raku

if 1 == 2 {
    say "Wait, what?"
} else {
    say "1 is not 2."
}

对于条件句中的表达式,括号在两种语言中都是可选的,如上所示。

10.1.4. 变量

在 Python 中,变量是同时声明和初始化的:

foo = 12
bar = 19

在 Raku 中,my 声明符声明了一个词法变量。变量可以用 = 初始化。此变量可以先声明,然后再初始化或声明并立即初始化。

my $foo;       # declare
$foo = 12;     # initialize
my $bar = 19;  # both at once

此外,你可能已经注意到,Raku 中的变量通常以符号开头 - 符号表示其容器的类型。 以 $ 开头的变量持有标量。 以 @ 开头的变量持有数组和以 % 开头的变量持有一个 hash(dict)。 如果用 \ 声明它们,则不可变变量可以是无符号的。

  • Python

s = 10
l = [1, 2, 3]
d = { a : 12, b : 99 }

print s
print l[2]
print d['a']
# 10, 2, 12
  • Raku

my $s = 10;
my @l = 1, 2, 3;
my %d = a => 12, b => 99;
my \x = 99;

say $s;
say @l[1];
say %d<a>;  # or %d{'a'}
say x;
# 10, 2, 12, 99

10.1.5. 作用域

在 Python 中,函数和类创建一个新的作用域,但没有其他的块构造函数(例如循环,条件)创建一个作用域。在 Python 2 中,列表推导不会创建新的作用域,但在 Python 3 中,它们创建新的作用域。

在 Raku 中,每个块都创建了一个词法作用域

  • Python

if True:
    x = 10
print x
# x is now 10
  • Raku

if True {
    my $x = 10
}
say $x
# error, $x is not declared in this scope
my $x;
if True {
    $x = 10
}
say $x
# ok, $x is 10
  • Python

x = 10
for x in 1, 2, 3:
   pass
print x
# x is 3
  • Raku

my \x = 10;
for 1, 2, 3 -> \x {
    # do nothing
    }
say x;
# x is 10

Python 中的 Lambdas 可以在 Raku 中写为块或尖号块。

  • Python

l = lambda i: i + 12
  • Raku

my $l = -> $i { $i + 12 }

构建 lambdas 的另一个Raku 惯用法是使用 Whatever star, *

  • Raku

my $l = * + 12    # same as above

表达式中的 将成为参数的占位符,并在编译时将表达式转换为 lambda。 表达式中的每个 都是一个单独的位置参数。

有关子例程和块的更多结构,请参阅以下部分。

另一个例子(来自Python FAQ):

  • Python

squares = []
for x in range(5):
    squares.append(lambda: x ** 2)
print squareslink:[2]
print squareslink:[4]
# both 16 since there is only one x
  • Raku

my \squares = [];
for ^5 -> \x {
    squares.append({ x² });
}
say squareslink:[2];
say squareslink:[4];
# 4, 16 since each loop iteration has a lexically scoped x,

注意,^N 类似于 range(N)。 类似地,N..^M 的作用类似于 range(N,M)(从 N 到 M-1 的列表)。 范围 N..M 是从 N 到 M 的列表。.. 之前或之后的 ^ 表示应排除列表的开始或结束端点(或两者都)。

另外, 是一种编写 x ** 2 的可爱方式(也可以正常工作); unicode 上标 2 是一个数字。 许多其他 unicode 运算符正如你所期望的那样工作(指数, 分数, π),但是可以在 Raku 中使用的每个 unicode 运算符或符号都具有 ASCII 等价物。

10.1.6. 控制流

Python 有 for 循环和 while 循环:

for i in 1, 2:
    print i
j = 1
while j < 3:
    print j
    j += 1

# 1,2,1,2

Raku 也有 for 循环和 while 循环:

for 1, 2 -> $i {
    say $i
}
my $j = 1;
while $j < 3 {
    say $j;
    $j += 1
}

(Raku 还有一些循环结构:repeat …​ untilrepeat …​ whileuntilloop。)

last 在 Raku 中退出一个循环,类似于 Python 中的 break。 Python 中的 continue 在 Raku 中是 next

  • Python

for i in range(10):
    if i == 3:
        continue
    if i == 5:
        break
    print i
  • Raku

for ^10 -> $i {
    next if $i == 3;
    last if $i == 5;
    say $i;
}

使用 if 作为语句修饰符(如上所述)在 Raku 中是可接受的,甚至在列表解析之外也可以。

Python for 循环中的 yield 语句生成一个 generator,就像 Raku 中的 gather/take 构造一样。这两个都打印 1,2,3。

  • Python

def count():
    for i in 1, 2, 3:
        yield i

for c in count():
    print c
  • Raku

sub count {
    gather {
        for 1, 2, 3 -> $i {
            take $i
        }
    }
}

for count() -> $c {
    say $c;
}

10.1.7. Lambdas, 函数和子例程

在 Python 中用 def 声明的函数(子例程)在 Raku 中是用 sub 来完成的。

def add(a, b):
    return a + b

sub add(\a, \b) {
    return a + b
}

return 是可选的; 最后一个表达式的值被用作返回值:

sub add(\a, \b) {
    a + b
}
# using variables with sigils
sub add($a, $b) {
    $a + $b
}

可以使用位置参数或关键字参数调用 Python 2 函数。这些是由调用者决定的。在 Python 3 中,一些参数可能是"keyword only"的。在 Raku 中,位置参数和命名参数由例程的签名确定。

  • Python

def speak(word, times):
    for i in range(times):
        print word
speak('hi', 2)
speak(word='hi', times=2)
  • Raku

位置参数

sub speak($word, $times) {
  say $word for ^$times
}
speak('hi', 2);

以冒号开头的命名参数:

sub speak(:$word, :$times) {
  say $word for ^$times
}
speak(word => 'hi', times => 2);
speak(:word<hi>, :times<2>);      # Alternative, more idiomatic

Raku 支持多重分派,因此可以通过将例程声明为 multi 来提供多个签名。

multi sub speak($word, $times) {
  say $word for ^$times
}
multi sub speak(:$word, :$times) {
    speak($word, $times);
}
speak('hi', 2);
speak(:word<hi>, :times<2>);

可以使用多种格式发送命名参数:

sub hello {...};
# all the same
hello(name => 'world'); # fat arrow syntax
hello(:name('world'));  # pair constructor
hello :name<world>;     # <> quotes words and makes a list
my $name = 'world';
hello(:$name);          # lexical var with the same name

创建匿名函数可以使用带有块或尖号块的 sub 来完成。

  • Python

square = lambda x: x ** 2
  • Raku

my $square = sub ($x) { $x ** 2 };  # anonymous sub
my $square = -> $x { $x ** 2 };     # pointy block
my $square = { $^x ** 2 };          # placeholder variable
my $square = { $_ ** 2 };           # topic variable

占位符变量按字典顺序排列以形成位置参数。 因此这些是相同的:

my $power = { $^x ** $^y };
my $power = -> $x, $y { $x ** $y };

10.1.8. 列表解析

可以组合 Postfix 语句修饰符和块以在 Raku 中轻松创建列表解析。

  • Python

print [ i * 2 for i in 3, 9 ]                      # OUTPUT: «[6, 18]␤»
  • Raku

say ( $_ * 2 for 3, 9 );                           # OUTPUT: «(6 18)␤»
say ( { $^i * 2 } for 3, 9 );                      # OUTPUT: «(6 18)␤»
say ( -> \i { i * 2 } for 3, 9 );                  # OUTPUT: «(6 18)␤»

可以应用条件,但 if 关键字首先出现,而不像 Python 那样,if 是第二个出现。

  • Python

print [ x * 2 for x in 1, 2, 3 if x > 1 ]          # OUTPUT: «[4, 6]␤»

vs

say ( $_ * 2 if $_ > 1 for 1, 2, 3 );              # OUTPUT: «(4 6)␤»

对于嵌套循环,交叉乘积运算符 X 将会有帮助:

print [ i + j for i in 3,9 for j in 2,10 ]         # OUTPUT: «[5, 13, 11, 19]␤»

变成以下任何一个:

say ( { $_[0] + $_[1] } for (3,9) X (2,10) );      # OUTPUT: «(5 13 11 19)␤»
say ( -> (\i, \j) { i + j } for (3,9) X (2,10) );  # OUTPUT: «(5 13 11 19)␤»

使用 map(就像 Python 的 map 一样)和 grep(就像 Python 的 filter 一样)是另一种选择。

10.1.9. 类和对象

这是 Python 文档中的一个示例。首先让我们回顾一下"实例变量",这些变量在 Raku 中称为属性:

  • Python

class Dog:
    def __init__(self, name):
        self.name = name
  • Raku

class Dog {
    has $.name;
}

对于每个创建的类,Raku 默认提供构造函数方法 new,它接受命名参数。

  • Python

d = Dog('Fido')
e = Dog('Buddy')
print d.name
print e.name
  • Raku

my $d = Dog.new(:name<Fido>); # or: Dog.new(name => 'Fido')
my $e = Dog.new(:name<Buddy>);
say $d.name;
say $e.name;

Raku 中的类属性可以通过几种方式声明。一种方法是仅声明一个词法变量和一个访问它的方法。

  • Python

class Dog:
    kind = 'canine'                # class attribute
    def __init__(self, name):
        self.name = name           # instance attribute
d = Dog('Fido')
e = Dog('Buddy')
print d.kind
print e.kind
print d.name
print e.name
  • Raku

class Dog {
    my $kind = 'canine';           # class attribute
    method kind { $kind }
    has $.name;                    # instance attribute
}

my $d = Dog.new(:name<Fido>);
my $e = Dog.new(:name<Buddy>);
say $d.kind;
say $e.kind;
say $d.name;
say $e.name;

为了在 Raku 中改变属性,必须在属性上使用 is rw trait:

  • Python

class Dog:
    def __init__(self, name):
        self.name = name
d = Dog()
d.name = 'rover'
  • Raku

class Dog {
    has $.name is rw;
}
my $d = Dog.new;
$d.name = 'rover';

继承使用 is 来完成:

  • Python

class Animal:
    def jump(self):
        print ("I am jumping")

class Dog(Animal):
    pass

d = Dog()
d.jump()
  • Raku

class Animal {
    method jump {
        say "I am jumping"
    }
}

class Dog is Animal {
}

my $d = Dog.new;
$d.jump;

根据需要多次使用 is trait 可以实现多重继承。或者,它可以与 also 关键字一起使用。

  • Python

class Dog(Animal, Friend, Pet):
    pass
  • Raku

class Animal {}; class Friend {}; class Pet {};
...;
class Dog is Animal is Friend is Pet {};

class Animal {}; class Friend {}; class Pet {};
...;
class Dog is Animal {
    also is Friend;
    also is Pet;
    ...
}

10.1.10. 装饰器

Python 中的装饰器是一种将函数包装在另一个函数中的方法。在 Raku 中,这是通过 wrap 完成的。

  • Python

def greeter(f):
    def new():
        print 'hello'
        f()
    return new

@greeter
def world():
    print 'world'

world();
  • Raku

sub world {
    say 'world'
}

&world.wrap(sub () {
    say 'hello';
    callsame;
});

world;

另一种方法是使用 trait:

# declare the trait 'greeter'
multi sub trait_mod:<is>(Routine $r, :$greeter) {
    $r.wrap(sub {
        say 'hello';
        callsame;
    })
}

sub world is greeter {
    say 'world';
}

world;

10.1.11. 上下文管理

Python 中的上下文管理器声明了在进入或退出作用域时发生的操作。

这是一个 Python 上下文管理器,可以打印字符串’hello','world’和’bye'。

class hello:
    def __exit__(self, type, value, traceback):
        print 'bye'
    def __enter__(self):
        print 'hello'

with hello():
    print 'world'

对于 "enter" 和 "exit" 事件,将块作为参数传递将是一种方法:

sub hello(Block $b) {
    say 'hello';
    $b();
    say 'bye';
}

hello {
    say 'world';
}

一个相关的想法是’Phasers',它可以设置为在进入或离开一个区块时运行。

{
    LEAVE say 'bye';
    ENTER say 'hello';
    say 'world';
}

10.1.12. input

在 Python 3 中,input 关键字用于提示用户。可以为此关键字提供可选参数,该参数将写入标准输出而不带尾随换行符:

user_input = input("Say hi → ")
print(user_input)

出现提示时,您可以输入 Hi 或任何其他字符串,这些字符串将存储在 user_input 变量中。这类似于 Raku 中的 prompt

my $user_input = prompt("Say hi → ");
say $user_input; # OUTPUT: whatever you entered.

11. Perl 到 Raku 的指南 - 简而言之

这个页面试图提供从 Perl 5 到 Raku 的语法和语义变化的快速路径。无论在 Perl 5 中有什么用,必须在 Raku 中以不同的方式编写,这里应该列出(而许多新的 Raku 特性和惯用法)不需要)。

因此,这不应该被误认为初学者教程或 Raku 的宣传概述;它旨在作为 Raku 学习者的技术参考,具有强大的 Perl 5 背景,以及任何将 Perl 5 代码移植到 Raku 的人(尽管注意到自动翻译可能更方便)。

关于语义的注释;当我们在本文档中说“现在”时,我们大多只是说“现在你正在试用 Raku”。我们并不是要暗示 Perl 5 现在突然过时了。恰恰相反,我们大多数人都喜欢 Perl 5,我们期望 Perl 5 能够继续使用多年。实际上,我们更重要的目标之一是使 Perl 5 和 Raku 之间的交互顺利进行。然而,我们也喜欢 Raku 中的设计决策,它们比 Perl 5 中的许多历史设计决策更新,可以说是更好的集成。我们很多人都希望在接下来的十年或两年内,Raku 将成为更主要的语言。如果你想在未来的意义上采取“现在”,那也没关系。但是我们根本不会对导致战斗的任何/或者思考感兴趣。

11.1. CPAN

如果您使用的模块尚未转换为 Raku,并且本文档中未列出任何替代方案,则可能尚未解决其在 Raku 下的使用问题。

Inline::Perl5 项目通过使用 Perl 解释器的嵌入式实例来运行 Perl 5 代码,可以直接从 Raku 代码中使用 Perl 5 模块。

这很简单:

# the :from<Perl5> makes Raku load Inline::Perl5 first (if installed)
# and then load the Scalar::Util module from Perl 5
use Scalar::Util:from<Perl5> <looks_like_number>;
say looks_like_number "foo";   # 0
say looks_like_number "42";    # 1

许多 Perl 5 模块已经移植到 Raku,试图尽可能多地维护这些模块的 API,作为 CPAN Butterfly Plan 的一部分。 这些可以在 https://modules.raku.org/t/CPAN5 找到。

许多 Perl 5 内置函数(目前大约 100个)已经以相同的语义移植到 Raku。 考虑一下 Perl 5 中的 shift 函数默认情况下从 @_@ARGV 转移,具体取决于上下文。 这些可以在 https://modules.raku.org/t/Perl5 找到,作为可单独加载的模块,在 P5built-ins 包中可以一次性获取所有这些模块。

11.2. 语法

两种语言之间的语法有一些差异,从如何定义标识符开始。

11.2.1. 标识符

Raku 允许在标识符中使用破折号( - ),下划线(_),撇号(')和字母数字:

sub test-doesn't-hang { ... }
my $ความสงบ = 42;
my \Δ = 72; say 72 - Δ;

11.2.2. 方法调用

如果您已经阅读过任何 Raku 代码,那么很明显,方法调用语法现在使用的是点而不是箭头:

$person->name  # Perl 5
$person.name   # Raku

点符号更容易键入,更符合行业标准。 但我们也想偷取其他东西的箭头。 (如果你想知道的话,现在用 ~ 运算符完成连接。)

要调用在运行时之前名称未知的方法:

$object->$methodname(@args);  # Perl 5
$object."$methodname"(@args); # Raku

如果省略引号,那么 Raku 要求 $methodname 包含一个 Method 对象,而不是该方法的简单字符串名称。 是的,Raku 中的所有内容都可以被视为一个对象。

11.3. 空白

即使启用了严格的模式和警告,Perl 5也允许在使用空格时具有惊人的灵活性:

# unidiomatic but valid Perl 5
say"Hello ".ucfirst  ($people
    [$ i]
    ->
    name)."!"if$greeted[$i]<1;

Raku 还支持程序员的自由和创造力,但平衡语法灵活性与其设计目标一致,即具有支持单遍解析和有用错误消息的一致,确定性,可扩展语法,干净地集成自定义运算符等功能,并且不会引导程序员 意外地错误表达他们的意图。 此外,“代码高尔夫”的实践略微不再强调; Raku 的设计理念比按键更简洁。

因此,语法中有许多地方,其中空格在 Perl 5 中是可选的,但在 Raku 中是强制的或禁止的。许多这些限制不太可能涉及很多真实的 Perl 代码(例如,不允许在空白之间使用空格) sigil和变量的名称),但有一些不幸与 Perl 黑客的习惯编码风格冲突:

在参数列表的左括号之前不允许有空格。

substr ($s, 4, 1); # Perl 5 (in Raku this would try to pass a single
                       #         argument of type List to substr)
substr($s, 4, 1);  # Raku
substr $s, 4, 1;   # Raku - alternative parentheses-less style

如果这真的是一个问题,那么你可能想看看 Raku 生态系统中的 Slang::Tuxic 模块:它改变了 Raku 的语法,你可以在开放之前有一个空格 参数列表的括号。

  • 关键字后立即需要空格

my($alpha, $beta);          # Perl 5, tries to call my() sub in Raku
my ($alpha, $beta);         # Raku
if($a < 0) { ... }          # Perl 5, dies in Raku
if ($a < 0) { ... }         # Raku
if $a < 0 { ... }           # Raku, more idiomatic
while($x-- > 5) { ... }     # Perl 5, dies in Raku
while ($x-- > 5) { ... }    # Raku
while $x-- > 5 { ... }      # Raku, more idiomatic
  • 前缀运算符之后或 postfix/postcircumfix运算符(包括数组/散列下标)之前不允许有空格。

$seen {$_} ++; # Perl 5
%seen{$_}++;   # Raku
  • 中缀运算符之前所需的空白,如果它与现有的 postfix/postcircumfix运算符冲突。

$n<1;   # Perl 5 (in Raku this would conflict with postcircumfix < >)
$n < 1; # Raku
  • 但是,在方法调用期间允许使用空格!

# Perl 5
my @books = $xml
  ->parse_file($file)          # some comment
  ->findnodes("/library/book");
# Raku
my @books = $xml
  .parse-file($file)           # some comment
  .findnodes("/library/book");

但是,请注意,您可以使用unspace在Raku代码中添加空格,否则不允许这样做。

11.4. 符号

在 Perl 5 中,数组和哈希值根据访问方式使用更改的符号。在 Raku 中,无论变量如何被使用,这些符号都是不变的 - 您可以将它们视为变量名称的一部分。

11.4.1. $ 标量

$ 符号现在总是与“标量”变量(例如 $name)一起使用,而不再用于数组索引散列索引。 也就是说,您仍然可以使用 $x[1]$x{"foo"},但它将作用于 $x,对类似名称的 @x%x`没有影响。 现在可以使用@x[1]` 和 %x{"foo"} 访问这些内容。

11.4.2. @ 数组

@ 符号现在总是与"数组"变量一起使用(例如 @months@months2@months[2, 4]),而不再用于[值切片哈希]。

11.4.3. % 散列

% 符号现在总是与“哈希”变量一起使用(例如 %calories, %calories<apple>, %calories<pear plum>),而不再用于键/值切片数组

11.4.4. Sub

& 符号现在一直使用(并且没有反斜杠的帮助)来引用命名子例程/运算符的函数对象而不调用它,即使用名称作为“名词”而不是“动词”:

my $sub = \&foo; # Perl 5
my $sub = &foo;  # Raku
callback => sub { say @_ }  # Perl 5 - can't pass built-in sub directly
callback => &say            # Raku - & gives "noun" form of any sub

由于 Raku 在完成编译后不允许在词法范围内添加/删除符号,因此没有等效于 Perl 5 的 undef&foo;,并且将定义与 Perl 5 定义的 &foo 最接近的符号::('&foo') (使用“动态符号查找”语法)。 但是,您可以使用我的&foo声明一个可变的命名子例程; 然后通过分配给&foo在运行时更改其含义。

在 Perl 5 中,与普通子调用相比,&符号可以另外用于以特殊方式调用子例程,具有略微不同的行为。 在 Raku 中,这些特殊形式不再可用:

  • &foo(…​) 用于规避函数原型

在 Raku 中没有原型,不管你是否传递一个文字代码块或一个包含代码对象的变量作为参数,它就不再有区别了:

# Perl 5:
first_index { $_ > 5 } @values;
&first_index($coderef, @values); # (disabling the prototype that parses a
                                     # literal block as the first argument)
# Raku:
first { $_ > 5 }, @values, :k;   # the :k makes first return an index
first $coderef, @values, :k;

&FOO; 和goto&foo; 重新使用调用者的参数列表/替换调用堆栈中的调用者。 Raku可以使用callame进行重新调度,也可以使用nextsame和nextx,它们在Perl 5中没有完全等效。

sub foo { say "before"; &bar;     say "after" } # Perl 5
sub foo { say "before"; bar(|@_); say "after" } # Raku - have to be explicit
sub foo { say "before"; goto &bar } # Perl 5
proto foo (|) {*};
multi foo ( Any $n ) {
    say "Any"; say $n;
};
multi foo ( Int $n ) {
    say "Int"; callsame;
};
foo(3); # /language/functions#index-entry-dispatch_callsame

11.4.5. * Glob

在Perl 5中,* sigil引用了 Perl 用于存储非词法变量,文件句柄,子和格式的 GLOB 结构。

当文件句柄需要传递给子文件时,您最有可能在早期Perl版本上编写的代码中遇到 GLOB,该版本不支持词法文件句柄。

# Perl 5 - ancient method
sub read_2 {
    local (*H) = @_;
    return scalar(<H>), scalar(<H>);
}
open FILE, '<', $path or die;
my ($line1, $line2) = read_2(*FILE);

在转换为 Raku 之前,您应该重构 Perl 5 代码以消除对 GLOB 的需求。

# Perl 5 - modern use of lexical filehandles
sub read_2 {
    my ($fh) = @_;
    return scalar(<$fh>), scalar(<$fh>);
}
open my $in_file, '<', $path or die;
my ($line1, $line2) = read_2($in_file);

这里只是一个可能的 Raku 翻译:

# Raku
sub read-n($fh, $n) {
    return $fh.get xx $n;
}
my $in-file = open $path or die;
my ($line1, $line2) = read-n($in-file, 2);

11.5. 数组索引/切片

数组上的索引和切片操作不再会影响变量的符号,副词可用于控制切片的类型:

  • 索引

say $months[2]; # Perl 5
say @months[2]; # Raku - @ instead of $
  • 值切片

say join ',', @months[6, 8..11]; # Perl 5 and Raku
  • 键/值切片

say join ',', %months[6, 8..11];    # Perl 5
say join ',', @months[6, 8..11]:kv; # Raku - @ instead of %; use :kv adverb

另请注意,下标方括号现在是一个普通的postcircumfix运算符,而不是一个特殊的句法形式,因此检查元素的存在未设置元素是通过副词完成的。

11.6. {} 散列索引/切片

散列上的索引和切片操作不再影响变量的符号,副词可用于控制切片的类型。此外,单字下标不再在花括号内神奇地自动引用;相反,新的尖括号版本可用,它始终自动引用其内容(使用与 qw //引用构造相同的规则):

  • 索引

say $calories{"apple"}; # Perl 5
say %calories{"apple"}; # Raku - % instead of $
say $calories{apple};   # Perl 5
say %calories<apple>;   # Raku - angle brackets; % instead of $
say %calories«"$key"»;  # Raku - double angles interpolate as a list of Str
  • 值切片

say join ',', @calories{'pear', 'plum'}; # Perl 5
say join ',', %calories{'pear', 'plum'}; # Raku - % instead of @
say join ',', %calories<pear plum>;      # Raku (prettier version)
my $keys = 'pear plum';
say join ',', %calories«$keys»;          # Raku the split is done after interpolation
  • 键/值索引

say join ',', %calories{'pear', 'plum'};    # Perl 5
say join ',', %calories{'pear', 'plum'}:kv; # Raku - use :kv adverb
say join ',', %calories<pear plum>:kv;      # Raku (prettier version)

还要注意,下标花括号现在是一个普通的 postcircumfix 操作符而不是一个特殊的语法形式,因此检查键的存在和删除键是用副词完成的。

11.7. 创建引用并使用它们

在 Perl 5 中,在创建时返回对匿名数组和散列和 subs 的引用。 使用\运算符生成对现有命名变量和 subs 的引用。 “引用/解除引用”这个比喻并没有干净地映射到实际的 Raku 容器系统,所以我们必须关注引用运算符的意图而不是实际的语法。

my $aref = \@aaa  ; # Perl 5

例如,这可能用于将引用传递给例程。但是在Raku中,传递了(单个)底层对象(你可以认为它是一种通过引用传递)。

my @array = 4,8,15;
{ $_[0] = 66 }(@array);   # run the block with @array aliased to $_
say @array; #  OUTPUT: «[66 8 15]»

传递 @array 的基础Array对象,并在声明的例程中修改其第一个值。

在 Perl 5 中,取消引用整个引用的语法是 type-sigil 和花括号,在花括号内引用。 在 Raku 中,这个概念根本不适用,因为参考隐喻并不真正适用。

在 Perl 5 中,箭头运算符 用于单个访问复合引用或通过引用调用 sub。 在 Raku 中,点运算符。 始终用于对象方法,但其余方法并不真正适用。

# Perl 5
    say $arrayref->[7];
    say $hashref->{'fire bad'};
    say $subref->($foo, $bar);

在相对较新版本的 Perl 5(5.20及更高版本)中,新功能允许使用箭头运算符进行解除引用:请参阅 Postfix Dereferencing。 这可以用于从标量创建数组。 此操作通常称为 decont,如在去包容化中,并且在Raku中使用诸如 .list.hash 之类的方法:

# Perl 5.20
    use experimental qw< postderef >;
    my @a = $arrayref->@*;
    my %h = $hashref->%*;
    my @slice = $arrayref->@[3..7];
# Raku
    my @a = $contains-an-array.list;        # or @($arrayref)
    my %h = $contains-a-hash.hash;          # or %($hashref)

“Zen” 切片做同样的事情:

# Raku
    my @a = $contains-an-array[];
    my %h = $contains-a-hash{};

有关详细信息,请参阅文档的“容器”部分

11.8. 运算符

有关所有运算符的完整详细信息,请参阅运算符文档

没发生变化的:

  • + 数字加法

  • - 数字减法

  • * 数字乘法

  • / 数字除法

  • % 数字求模

  • ** 数字指数

  • ++ 数字递增

  • -- 数字递减

  • ! && || ^ 布尔,高优先级

  • not and or xor 布尔,低优先级

  • == != < > ⇐ >= 数字比较

  • eq ne lt gt le ge 字符串比较

11.9. , (逗号) 列表分割符

没有改变,但请注意,为了将数组变量展平为列表(为了追加或添加更多项目的前缀),应该使用|操作员(另见Slip)。例如:

my @numbers = 100, 200, 300;
my @more_numbers = 500, 600, 700;
my @all_numbers = |@numbers, 400, |@more_numbers;

这样就可以连接数组。

请注意,右侧不需要任何括号:List Separator 负责创建列表,而不是括号!

11.9.1. <⇒ cmp 三路比较

在 Perl 5 中,这些运算符返回 -1, 0 或 1。在 Raku 中,它们返回 Order::LessOrder::SameOrder::More

cmp 现在命名为 leg; 它强制字符串上下文进行比较。

<⇒ 仍然强制数字上下文。

Raku 中的 cmp 执行 <⇒leg,具体取决于其参数的现有类型。

11.9.2. ~~ 智能匹配运算符

虽然运算符没有改变,但确切匹配的规则取决于两个参数的类型,并且这些规则在 Perl 5 和 Raku 中大不相同。请参阅 ~smartmatch 运算符

11.9.3. & | ^ 字符串位运算符

11.9.4. & | ^ 数字位运算符

11.9.5. & | ^ 布尔运算符

在 Perl 5 中,& | ^ 根据参数的内容调用。例如,31 | 33 返回与 “31”|“33” 不同的结果。

在 Raku 中,这些单字符操作已被删除,并被两个字符的操作系统取代,这些操作将他们的参数强制转换为所需的上下文。

# Infix ops (two arguments; one on each side of the op)
+&  +|  +^  And Or Xor: Numeric
~&  ~|  ~^  And Or Xor: String
?&  ?|  ?^  And Or Xor: Boolean

# Prefix ops (one argument, after the op)
+^  Not: Numeric
~^  Not: String
?^  Not: Boolean (same as the ! op)

11.9.6. << >> 数字左移|右移运算符

+<+> 代替。

say 42 << 3; # Perl 5
say 42 +< 3; # Raku

11.9.7. ⇒ 胖逗号

在 Perl 5 中, 的行为就像一个逗号,但也引用了它的左侧。

在 Raku 中,Pair 运算符,原理上完全不同,但在许多情况下都是相同的。

如果您在哈希初始化中使用 ,或者将参数传递给期望 hashref 的 sub,则用法可能相同。

sub get_the_loot { ... }; # Raku stub
# Works in Perl 5 and Raku
my %hash = ( AAA => 1, BBB => 2 );
get_the_loot( 'diamonds', { quiet_level => 'very', quantity => 9 }); # Note the curly braces

如果你使用 作为一个方便的快捷方式,不必引用列表的一部分,或者将参数传递给一个需要 KEYVALUEKEYVALUE 的平面列表的子,那么继续使用 可能会破坏你的代码。 最简单的解决方法是将该胖箭头更改为常规逗号,并手动将引号添加到其左侧。 或者,您可以更改 sub 的API以slurp哈希。 一个更好的长期解决方案是将sub的API改为期望Pairs; 但是,这需要您一次更改所有 sub 调用。

# Perl 5
sub get_the_loot {
    my $loot = shift;
    my %options = @_;
    # ...
}
# Note: no curly braces in this sub call
get_the_loot( 'diamonds', quiet_level => 'very', quantity => 9 );
# Raku, original API
sub get_the_loot( $loot, *%options ) { # The * means to slurp everything
    ...
}
get_the_loot( 'diamonds', quiet_level => 'very', quantity => 9 ); # Note: no curly braces in this API

# Raku, API changed to specify valid options
# The colon before the sigils means to expect a named variable,
# with the key having the same name as the variable.
sub get_the_loot( $loot, :$quiet_level?, :$quantity = 1 ) {
    # This version will check for unexpected arguments!
    ...
}
get_the_loot( 'diamonds', quietlevel => 'very' ); # Throws error for misspelled parameter name

11.9.8. ? : 三元运算符

条件运算符 ? : 已经被替换成 `?? !! `。

my $result = $score > 60 ?  'Pass' :  'Fail'; # Perl 5
my $result = $score > 60 ?? 'Pass' !! 'Fail'; # Raku

11.9.9. .(点号) 字符串连接

替换为波浪号。

助记:想到用针和线将两个字符串“拼接”在一起。

$food = 'grape' . 'fruit'; # Perl 5
$food = 'grape' ~ 'fruit'; # Raku

11.9.10. x 列表复制或字符串复制运算符

在 Perl 5 中,x 是复制运算符,它在标量或列表上下文中的行为有所不同:

  • 在标量上下文中,x 重复一个字符串;

  • 在列表上下文中 x 重复一个列表,但前提是左参数是括号!

Raku 使用两个不同的复制运算符来实现上述目的:

  • x 表示字符串重复(在任何上下文中);

  • xx 表示列表重复(在任何上下文中)。

助记符:x 很短,xx 很长,所以 xx 是用于列表的。

# Perl 5
    print '-' x 80;             # Print row of dashes
    @ones = (1) x 80;           # A list of 80 1's
    @ones = (5) x @ones;        # Set all elements to 5
# Raku
    print '-' x 80;             # Unchanged
    @ones = 1 xx 80;            # Parentheses no longer needed
    @ones = 5 xx @ones;         # Parentheses no longer needed

11.9.11. .. …​ 两个点或三个点,范围操作或 flipflop 运算符

在 Perl 5 中,.. 是两个完全不同的运算符之一,具体取决于上下文。

在列表上下文中,.. 是熟悉的范围运算符。 Perl 5 代码的范围不应该要求翻译。

在标量上下文中,..…​ 是鲜为人知的 Flipflop 运算符。 它们已被 fffff 取代。

11.9.12. 字符串插值

在 Perl 5 中,"${foo}s" 从其旁边的常规文本中删除变量名。 在 Raku 中,只需将花括号扩展为包括sigil:"{$foo}s"。 事实上,这是插入表达式的一个非常简单的例子。

11.9.13. 复合语句

这些语句包括条件和循环。

条件语句
if elsif else unless

大部分没有变化; 条件周围的括号现在是可选的,但如果使用,则不能立即跟随关键字,否则它将被视为函数调用。 将条件表达式绑定到变量也有一点不同:

if (my $x = dostuff()) {...}  # Perl 5
if dostuff() -> $x {...}      # Raku

(您仍然可以在 Raku 中使用我的表单,但它将扩展到外部块,而不是内部。)

除非条件仅允许 Raku 中的单个块;它不允许使用 elsif 或 else 子句。

given-when

给定时构造类似于 if-elsif-else 语句链或类似于例如 switch-case 构造。 C. 它具有一般结构:

given EXPR {
    when EXPR { ... }
    when EXPR { ... }
    default { ... }
}

在其最简单的形式中,构造如下:

given $value {                   # assigns $_
    when "a match" {             # if $_ ~~ "a match"
        # do-something();
    }
    when "another match" {       # elsif $_ ~~ "another match"
        # do-something-else();
    }
    default {                    # else
        # do-default-thing();
    }
}

这是很简单的,因为标量值在 when 语句中与 $ 匹配,这是由给定的设置。更一般地说,匹配实际上是 $ 上的智能匹配,这样可以使用更复杂的实体(如regexp)进行查找而不是标量值。

另请参阅上面的smartmatch op上的警告。

11.10. 循环

11.10.1. while until

大部分没有变化;条件周围的括号现在是可选的,但如果使用,则不能立即跟随关键字,否则它将被视为函数调用。将条件表达式绑定到变量也有一点不同:

while (my $x = dostuff()) {...}  # Perl 5
while dostuff() -> $x {...}      # Raku

(您仍然可以在 Raku 中使用我的表单,但它将扩展到外部块,而不是内部。)

请注意,从文件句柄逐行读取已更改。

在 Perl 5 中,它是使用菱形运算符在while循环中完成的。使用for而不是while是一个常见的错误,因为for会导致整个文件立即被吸入,从而淹没了程序的内存使用情况。

在 Raku 中,for 语句是惰性的,所以我们使用 .lines 方法在 for 循环中逐行读取。

while (<IN_FH>)  { } # Perl 5
for $IN_FH.lines { } # Raku

另请注意,在 Raku 中,默认情况下会 chomp 行。

11.10.2. do while/until

# Perl 5
do {
    ...
} while $x < 10;

do {
    ...
} until $x >= 10;

该构造仍然存在,但是 do 被重命名为 repeat,以更好地表示构造的作用:

# Raku
repeat {
    ...
} while $x < 10;

repeat {
    ...
} until $x >= 10;

11.10.3. for foreach

首先要注意关于 forforeach 关键字的这种常见误解:许多程序员认为他们区分C风格的三表达形式和列表迭代器形式;他们不!事实上,关键词是可以互换的; Perl 5 编译器在括号中查找分号以确定要解析的循环类型。

C 风格的三因子形式现在使用 loop 关键字,否则保持不变。括号仍然是必需的。

for  ( my $i = 1; $i <= 10; $i++ ) { ... } # Perl 5
loop ( my $i = 1; $i <= 10; $i++ ) { ... } # Raku

循环迭代器表单以Raku命名,foreach不再是关键字。 for循环具有以下规则:

  • 括号是可选的;

  • 迭代变量(如果有的话)已经从列表前面出现,再出现在列表和添加的箭头操作符之后;

  • 迭代变量现在总是词法的:my 既不需要也不允许;

  • 迭代变量是当前列表元素的只读别名(在 Perl 5 中它是一个读写别名!)。如果需要读写别名,请将迭代变量前面的 更改为 <→。从 Perl 5 进行翻译时,检查循环变量的使用以确定是否需要读写。

for my $car (@cars)  {...} # Perl 5; read-write
for @cars  -> $car   {...} # Raku; read-only
for @cars <-> $car   {...} # Raku; read-write

如果正在使用默认主题 $_,那么它也是读写的。

for (@cars)      {...} # Perl 5; $_ is read-write
for @cars        {...} # Raku; $_ is read-write
for @cars <-> $_ {...} # Raku; $_ is also read-write

在每次迭代中可以使用列表中多个元素,只需在箭头操作符后指定多个变量:

my @array = 1..10;
for @array -> $first, $second {
    say "First is $first, second is $second";
}

11.10.4. each

这是 Perl 5 的 while…each(%hash) or while…each(@array) 的等价物,(即迭代数据结构的键/索引和值)而 Raku 中:

while (my ($i, $v) = each(@array)) { ... } # Perl 5
for @array.kv -> $i, $v { ... } # Raku
while (my ($k, $v) = each(%hash)) { ... } # Perl 5
for %hash.kv -> $k, $v { ... } # Raku

11.10.5. 控制流语句

没发生变化的:

  • next

  • last

  • redo

continue

不再有 continue 块了。而是在循环体内使用 NEXT 块(phaser)。

# Perl 5
    my $str = '';
    for (1..5) {
        next if $_ % 2 == 1;
        $str .= $_;
    }
    continue {
        $str .= ':'
    }
# Raku
    my $str = '';
    for 1..5 {
        next if $_ % 2 == 1;
        $str ~= $_;
        NEXT {
            $str ~= ':'
        }
    }

请注意,phasers 并不需要块。当您不想要另一个作用域时,这非常方便:

# Raku
    my $str = '';
    for 1..5 {
        next if $_ % 2 == 1;
        $str ~= $_;
        NEXT $str ~= ':';
    }

11.11. 函数

11.11.1. 带有裸块的内置函数

之前接受裸块的内置函数,其后没有逗号,其余参数现在需要在块和参数之间使用逗号,例如 mapgrep 等。

my @results = grep { $_ eq "bars" } @foo; # Perl 5
my @results = grep { $_ eq "bars" }, @foo; # Raku

11.11.2. delete

变成了 {} 哈希下标[数组下标]运算符的副词。

my $deleted_value = delete $hash{$key};  # Perl 5
my $deleted_value = %hash{$key}:delete;  # Raku - use :delete adverb
my $deleted_value = delete $array[$i];  # Perl 5
my $deleted_value = @array[$i]:delete;  # Raku - use :delete adverb

11.11.3. exists

变成了 {} 哈希下标[数组下标]运算符的副词。

say "element exists" if exists $hash{$key};  # Perl 5
say "element exists" if %hash{$key}:exists;  # Raku - use :exists adverb
say "element exists" if exists $array[$i];  # Perl 5
say "element exists" if @array[$i]:exists;  # Raku - use :exists adverb

11.12. 正则表达式 (regex/regexp)

11.12.1. =~ 和 !~ 变成了 ~~ 和 !~~

在 Perl 5 中,使用 =~ 正则表达式绑定运算符对变量进行匹配和替换。

在 Raku 中,使用了 ~~ 智能匹配运算符。

next if $line  =~ /static/  ; # Perl 5
next if $line  ~~ /static/  ; # Raku
next if $line  !~ /dynamic/ ; # Perl 5
next if $line !~~ /dynamic/ ; # Raku
$line =~ s/abc/123/;          # Perl 5
$line ~~ s/abc/123/;          # Raku

或者,可以使用新的 .match.subst 方法。请注意,.subst是非可变的

11.12.2. 捕获从 0 开始而非从 1 开始

/(.+)/ and print $1; # Perl 5
/(.+)/ and print $0; # Raku

11.12.3. 移动修饰符

将任何修饰符从正则表达式的末尾移动到开头。这可能需要您在 /abc/ 这样的普通匹配上添加可选的 m

next if $line =~    /static/i ; # Perl 5
next if $line ~~ m:i/static/  ; # Raku

11.12.4. 添加 :P5 或 :Perl5 副词

如果实际的正则表达式很复杂,您可能希望通过添加 P5 修饰符来原样使用它。

next if $line =~    m/[aeiou]/   ; # Perl 5
next if $line ~~ m:P5/[aeiou]/   ; # Raku, using P5 modifier
next if $line ~~ m/  <[aeiou]> / ; # Raku, native new syntax

请注意,Perl 5 正则表达式语法可以追溯到很多年前,可能缺少自 Raku 项目开始以来添加的功能。

11.12.5. 特殊匹配器通常属于 <> 语法

Perl 5 正则表达式支持许多特殊匹配语法的情况。它们不会全部列在这里,但通常不是被 () 包围,断言将被 <> 包围着。

对于字符类,这意味着:

  • [abc] 变成了 <[abc]>

  • [^abc] 变成了 ←[abc]>

  • [a-zA-Z] 变成了 <[a..zA..Z]>

  • `变成了 `<:Upper>

  • [abc[:upper:]] 变成了 <[abc]+:Upper>

对于环视断言:

  • (?=[abc]) 变成了 <?[abc]>

  • (?=ar?bitrary* pattern) 变成了 <before ar?bitrary* pattern>

  • (?!=[abc]) 变成了 <![abc]>

  • (?!=ar?bitrary* pattern) 变成了 <!before ar?bitrary* pattern>

  • (?⇐ar?bitrary* pattern) 变成了 <after ar?bitrary* pattern>

  • (?<!ar?bitrary* pattern) 变成了 <!after ar?bitrary* pattern>

有关更多信息,请参阅向前查看断言

(和 <> 语法无关, “环视” /foo\Kbar/ 变成了 /foo <( bar )> /

  • (?(?{condition))yes-pattern|no-pattern) 变成了 [ <?{condition}> yes-pattern | no-pattern ]

11.12.6. 最长 token 匹配(LTM) 取代了备选分支

在 Raku 正则表达式中,| 遵循 LTM,它根据一组规则决定哪个备选分支赢得了一个模糊匹配,而不是先写出哪个。

解决这个问题最简单的方法就是在你的 Perl 5 正则表达式中把任何 | 更改为 ||

但是,如果正则表达式用 || 写的是继承或组成使用 | 的语法无论是设计还是拼写错误,结果可能无法按预期工作。因此,当匹配过程变得复杂时,您最终需要对两者都有所了解,尤其是 LTM 策略的工作原理。此外,`| 可能是语法重用的更好选择。

11.12.7. 命名捕获

这些工作方式略有不同;他们也只使用最新版本的 Perl 5。

use v5.22;
"þor is mighty" =~ /is (?<iswhat>\w+)/n;
say $+{iswhat};

非捕获组中的内容用于实现捕获后面的内容,直到组的末尾(the)。捕获转到带有捕获名称的键下的 %+ 哈希。在 Raku 中,命名捕获以这种方式工作

"þor is mighty" ~~ /is \s+ $<iswhat>=(\w+)/;
say $<iswhat>;

在正则表达式中进行实际赋值;这与用于外部变量的语法相同。

11.12.8. 注释

与 Perl 5 一样,注释在正则表达式中照常工作。

/ word #`(match lexical "word") /

11.13. BEGIN, UNITCHECK, CHECK, INIT 和 END

除了 UNITCHECK 之外,所有这些特殊块也存在于 Raku 中。在 Raku 中,这些被称为 Phasers。但是有一些差异!

11.13.1. UNITCHECK 变为 CHECK

Raku 中目前没有直接等效的 CHECK 块。Raku 中的 CHECK phaser 与 Perl 5 中的 UNITCHECK 块具有相同的语义:只要它出现的编译单元完成解析,它就会运行。这被认为是比 Perl 5 中 CHECK 块的当前语义更加理智的语义。但出于兼容性原因,不可能在 Perl 5 中更改 CHECK 块的语义,因此在 5.10 中引入了 UNITCHECK 块。因此决定 Raku CHECK phaser 将遵循更健全的 Perl 5 UNITCHECK 语义。

11.13.2. 不再需要块

在 Perl 5 中,这些特殊块必须具有花括号,这意味着单独的范围。在 Raku 中,这不是必需的,允许这些特殊块与周围的词法范围共享它们的范围。

my $foo;             # Perl 5
BEGIN { $foo = 42 }
BEGIN my $foo = 42;  # Raku

11.13.3. 关于预编译改变了语义

如果将其放在正在预编译的模块中,则这些步骤将仅在预编译期间执行,而不是在加载预编译模块时执行。 因此,当从 Perl 5 移植模块代码时,您可能需要更改 BEGINCHECK

11.14. 编译指令

11.14.1. strict

严格模式现在默认启用。

11.14.2. warnings

警告现在默认开启。

目前 no warnings 还未实现 ,但是把东西放在一个安静的 {} 中会让其沉默。

11.14.3. autodie

autodie 更改以在异常时抛出异常的函数现在通常默认返回 Failures。您可以毫无问题地测试失败的定义/真实性。如果以任何其他方式使用 Failure,则将抛出由 Failure 封装的 Exception

# Perl 5
open my $i_fh, '<', $input_path;  # Fails silently on error
use autodie;
open my $o_fh, '>', $output_path; # Throws exception on error
# Raku
my $i_fh = open $input_path,  :r; # Returns Failure on error
my $o_fh = open $output_path, :w; # Returns Failure on error

因为您可以毫无问题地检查真实性,所以您可以在 if 语句中使用 open 的结果:

# Raku
if open($input_path,:r) -> $handle {
    .say for $handle.lines;
}
else {
    # gracefully handle the fact that the open() failed
}

11.14.4. base, parent

在类声明中,is 关键字在 Raku 中替换了 use baseuse parent

# Perl 5
package Cat;
use base qw(Animal);
# Raku
class Cat is Animal {}

请注意,必须在编译时知道 Animal 类才能继承它。

11.14.5. bigint bignum bigrat

不再相关。

Int 现在是任意精度,因为 Rat 的分子(分母限制为 2**64,之后它将自动升级到 Num 以保持性能)。如果你想要一个具有任意精度分母的 Rat,可以使用 FatRat

11.14.6. constant

在 Raku 中,constant 是变量的声明符,就像 my 一样,除了变量永久锁定到其初始化表达式的结果(在编译时计算)。

所以,将 更改为 =

use constant DEBUG => 0; # Perl 5
constant DEBUG = 0;      # Raku
use constant pi => 4 * atan2(1, 1); # Perl 5
tau, pi, e, i; # built-in constants in Raku
τ, π, 𝑒        # and their unicode equivalents

11.14.7. 编码

允许您以非 ascii 或非 utf8 编写脚本。 Raku 目前仅使用 utf8 作为其脚本。

11.14.8. 整数

Perl pragma 使用整数运算而不是浮点运算。在 Raku 中没有这样的等价物。如果你在计算中使用原生整数,那么这将是最接近的事情。

my int $foo = 42;
my int $bar = 666;
say $foo * $bar;    # uses native integer multiplication

11.14.9. lib

处理在编译时查找模块的位置。底层逻辑与 Perl 5 非常不同,但在使用等效语法的情况下,在 Raku 中 use lib 与 Perl 5 中的相同。

11.14.10. mro

不再相关。

在 Raku 中,方法调用现在始终使用 C3 方法解析顺序。如果需要查找给定类的父类,可以这样调用 mro 元方法:

say Animal.^mro;    # .^ indicates calling a meta-method on the object

11.14.11. uft8

不再相关:在 Raku 中,源代码应该采用 utf8 编码。

11.14.12. vars

在 Perl 5 中不鼓励使用。 参阅 https://perldoc.perl.org/vars.html

在转换为 Raku 之前,您应该重构 Perl 5 代码以消除 use vars 的需要。

11.15. 命令行标记

不变的:

-c -e -h -I -n -p -v -V

  • -a

更改您的代码以手动使用 .split

  • -F

更改您的代码以手动使用 .split

  • -l

现在这是默认行为。

  • -M -m

只有 -M 仍然存在。而且,由于您不能再使用“no Module”语法,因此不再使用带有 --M 来 “no” 模块。

  • -E

由于已启用所有功能,因此只需使用小写 -e

  • -d, -dt, -d:foo, -D, etc.

替换为 ++BUG metasyntactic 选项。

  • -s

切换解析现在由 MAIN 子例程的参数列表完成。

# Perl 5
    #!/usr/bin/perl -s
    if ($xyz) { print "$xyz\n" }
./example.pl -xyz=5
5
# Raku
    sub MAIN( Int :$xyz ) {
        say $xyz if $xyz.defined;
    }
raku example.p6 --xyz=5
5
raku example.p6 -xyz=5
5
  • it

被移除了

  • -P -u -U -W -X

被移除了,参阅 S19#Removed Syntactic Features.

  • -w

现在是默认行为。

  • -s, -T

11.16. 文件相关的运算符

11.16.1. 将文本文件的行读入数组

在 Perl 5 中,读取文本文件行的常用习惯用法如下:

open my $fh, "<", "file" or die "$!";
my @lines = <$fh>;                # lines are NOT chomped
close $fh;

在 Raku 中,这已经简化为

my @lines = "file".IO.lines;  # auto-chomped

不要试图尝试在文件中进行 slurping 并将结果字符串拆分为换行符,因为这会给出一个带有尾随空元素的数组,这比你预期的要多一些(它也更复杂),例如:

# initialize the file to read
spurt "test-file", q:to/END/;
first line
second line
third line
END
# read the file
my @lines = "test-file".IO.slurp.split(/\n/);
say @lines.elems;    #-> 4

如果由于某种原因你想要首先 slurp 文件,那么你可以在 slurp 的结果上调用 lines 方法:

my @lines = "test-file".IO.slurp.lines;  # also auto-chomps

另外,请注意 $! 与 Raku 中的文件操作失败无关。一个 IO 操作无法返回失败而不是抛出异常。 如果要返回失败消息,则它本身就是失败,而不是 $!. 要做同样的事情,我需要检查并报告 Perl 5:

my $fh = open('./bad/path/to/file', :w) or die $fh;

注意:$fh 而不是 $!. 现在,您可以将 $ 设置为失败并使用 $ 来消亡:

my $fh = open('./bad/path/to/file', :w) orelse .die;

尝试使用失败的任何操作都将导致程序出错并终止。即使只是调用 .self 方法也足够了。

my $fh = open('./bad/path/to/file', :w).self;

11.16.2. 捕获可执行文件的标准输出。

而在 Perl 5 中,你会这样做

my $arg = 'Hello';
my $captured = `echo \Q$arg\E`;
my $captured = qx(echo \Q$arg\E);

或者使用 String::ShellQuote(因为 \Q…​\E 不完全正确):

my $arg = shell_quote 'Hello';
my $captured = `echo $arg`;
my $captured = qx(echo $arg);

在 Raku 中,您可能希望在不使用 shell 的情况下运行命令:

my $arg = 'Hello';
my $captured = run('echo', $arg, :out).out.slurp;
my $captured = run(«echo "$arg"», :out).out.slurp;

如果你真的想要,你也可以使用 shell:

my $arg = 'Hello';
my $captured = shell("echo $arg", :out).out.slurp;
my $captured = qqx{echo $arg};

但请注意,在这种情况下根本没有保护!run 不使用 shell,因此不需要转义参数(参数直接传递)。如果你使用 shell 或 qqx,那么一切都会变成一个长字符串,然后传递给 shell。除非您非常仔细地验证您的参数,否则很有可能使用此类代码引入 shell 注入漏洞。

11.17. 环境变量

11.17.1. Perl 模块库路径

在 Perl 5 中,为 Perl 模块指定额外搜索路径的环境变量之一是 PERL5LIB

$ PERL5LIB="/some/module/lib" perl program.pl

在 Raku 中,这是类似的,只需要改变一个数字!您可能已经猜到了,您只需要使用 PERL6LIB

$ PERL6LIB="/some/module/lib" raku program.p6

在 Perl 5 中,使用 ':'(冒号)作为 PERL5LIB 的目录分隔符,但在 Raku 中使用 ','(逗号)。例如:

$ export PERL5LIB=/module/dir1:/module/dir2;

但是

$ export PERL6LIB=/module/dir1,/module/dir2;

(Raku 无法识别 PERL5LIB 或旧的 Perl 环境变量 PERLLIB。)

与 Perl 5 一样,如果未指定 PERL6LIB,则需要通过 use lib pragma 指定程序中的库路径:

use lib '/some/module/lib'

请注意,PERL6LIB 在 Raku 中更具开发人员便利性(与 Perl5 中 PERL5LIB 的等效用法相反),模块消费者不应使用它,因为将来可能会将其删除。这是因为 Raku 的模块加载与操作系统路径不直接兼容。

11.18. Misc

11.18.1. '0' 为真

与 Perl 5 不同,只包含零('0')的字符串为 True。由于 Raku 具有核心类型,因此更有意义。这也意味着常见的模式:

... if defined $x and length $x; # or just length() in modern perls

在 Raku 中变的简单

... if $x;

11.18.2. dump

不见了。

Raku 设计允许自动透明地保存和加载编译的字节码。

到目前为止,Rakudo 仅支持模块。

11.19. AUTOLOAD

FALLBACK 方法提供类似的功能。

11.19.1. 从模块导入特定函数

在 Perl 5 中,可以选择性地从给定模块导入函数,如下所示:

use ModuleName qw{foo bar baz};

在 Raku 中,通过使用相关子节点上的 is export 角色来指定要导出的函数;然后导出具有此角色的所有 subs。因此,以下模块 Bar 导出 subs foobar 但不导出 baz

unit module Bar;

sub foo($a) is export { say "foo $a" }
sub bar($b) is export { say "bar $b" }
sub baz($z) { say "baz $z" }

要使用此模块,只需 use Bar,即可使用 foobar 函数

use Bar;
foo(1);    #=> "foo 1"
bar(2);    #=> "bar 2"

如果尝试使用 baz,则会在编译时引发“未声明的例程”错误。

那么,如何重新创建能够有选择地导入函数的 Perl 5 行为呢?通过在模块内定义一个 EXPORT sub,它指定要导出的函数并删除 module Bar 语句。

以前的模块 Bar 现在只是一个名为 Bar.pm6 的文件,其中包含以下内容:

sub EXPORT(*@import-list) {
    my %exportable-subs =
        '&foo' => &foo,
        '&bar' => &bar,
        ;
    my %subs-to-export;
    for @import-list -> $import {
        if grep $sub-name, %exportable-subs.keys {
            %subs-to-export{$sub-name} = %exportable-subs{$sub-name};
        }
    }
    return %subs-to-export;
}

sub foo($a, $b, $c) { say "foo, $a, $b, $c" }
sub bar($a) { say "bar, $a" }
sub baz($z) { say "baz, $z" }

注意,不再通过 is export 角色显式地导出 subs,而是通过 EXPORT sub 指定我们想要导出的模块中的 subs,然后我们填充一个包含实际将被导出的 subs 的哈希。 @import-list 由调用代码中的 use 语句设置,因此允许我们有选择地导入模块可用的 subs。

因此,要仅导入 foo 例程,我们在调用代码中执行以下操作:

use Bar <foo>;
foo(1);       #=> "foo 1"

在这里我们看到即使 bar 是可导出的,如果我们没有明确地导入它,它也无法使用。因此,这会在编译时导致“未声明的例程”错误:

use Bar <foo>;
foo(1);
bar(5);       #!> "Undeclared routine: bar used at line 3"

但是,这将有效

use Bar <foo bar>;
foo(1);       #=> "foo 1"
bar(5);       #=> "bar 5"

另请注意,即使在 use 语句中指定,baz 仍然不可导致:

use Bar <foo bar baz>;
baz(3);       #!> "Undeclared routine: baz used at line 2"

为了使这个工作,显然必须跳过许多箍。在标准用例中,通过 is export 角色指定要导出的函数,Raku 会以正确的方式为您自动创建 EXPORT sub,因此应该非常仔细地考虑是否值得编写自己的 EXPORT 例程。

11.19.2. 从模块导入特定函数组

如果要从模块中导出函数组,只需要为组分配名称,其余的将自动运行。如果在 sub 声明中指定 is export,则实际上是将此子例程添加到 :DEFAULT 导出组。但是您可以将子例程添加到另一个组或多个组:

unit module Bar;
sub foo() is export { }                   # added by default to :DEFAULT
sub bar() is export(:FNORBL) { }          # added to the FNORBL export group
sub baz() is export(:DEFAULT:FNORBL) { }  # added to both

所以现在你可以像这样使用 Bar 模块:

use Bar;                     # imports foo / baz
use Bar :FNORBL;             # imports bar / baz
use Bar :ALL;                # imports foo / bar / baz

请注意 :ALL 是一个自动生成的组,它包含具有 is export trait 的所有子例程。

11.20. 核心模块

11.20.1. Data::Dumper

在 Perl 5 中,Data::Dumper 模块用于序列化,以及程序员调试程序数据结构的视图。

在 Raku 中,这些任务是使用 .perl 方法完成的,每个对象都有 .perl 方法。

# Given:
    my @array_of_hashes = (
        { NAME => 'apple',   type => 'fruit' },
        { NAME => 'cabbage', type => 'no, please no' },
    );
# Perl 5
    use Data::Dumper;
    $Data::Dumper::Useqq = 1;
    print Dumper \@array_of_hashes; # Note the backslash.
# Raku
say @array_of_hashes.perl; # .perl on the array, not on its reference.

在 Perl 5 中,Data::Dumper 具有更复杂的可选调用约定,允许命名 VAR。

在 Raku 中,在变量的 sigil 前面放置一个冒号,将其转换为一个 Pair,其中包含 var 名称的键和 var 值的值。

# Given:
    my ( $foo, $bar ) = ( 42, 44 );
    my @baz = ( 16, 32, 64, 'Hike!' );
# Perl 5
    use Data::Dumper;
    print Data::Dumper->Dump(
        [     $foo, $bar, \@baz   ],
        [ qw(  foo   bar   *baz ) ],
    );
# Output
#    $foo = 42;
#    $bar = 44;
#    @baz = (
#             16,
#             32,
#             64,
#             'Hike!'
#           );
# Raku
say [ :$foo, :$bar, :@baz ].perl;
# OUTPUT: «["foo" => 42, "bar" => 44, "baz" => [16, 32, 64, "Hike!"]]»

对于开发人员来说,还有一个特定于 Rakudo 的调试辅助工具,称为 dd(Tiny Data Dumper,它很小,它失去了“t”)。这将打印 STDERR 上给定变量的 .perl 表示和一些可以反省的额外信息:

# Raku
dd $foo, $bar, @baz;
# OUTPUT: «Int $foo = 42
# Int $bar = 44
# Array @baz = [16, 32, 64, "Hike!"]
# »

11.20.2. Getopt::Long

切换解析现在由 MAIN 子例程的参数列表完成。

# Perl 5
    use 5.010;
    use Getopt::Long;
    GetOptions(
        'length=i' => \( my $length = 24       ), # numeric
        'file=s'   => \( my $data = 'file.dat' ), # string
        'verbose'  => \( my $verbose           ), # flag
    ) or die;
    say $length;
    say $data;
    say 'Verbosity ', ($verbose ? 'on' : 'off') if defined $verbose;
perl example.pl
    24
    file.dat
perl example.pl --file=foo --length=42 --verbose
    42
    foo
    Verbosity on

perl example.pl --length=abc
    Value "abc" invalid for option length (number expected)
    Died at c.pl line 3.
# Raku
    sub MAIN( Int :$length = 24, :file($data) = 'file.dat', Bool :$verbose ) {
        say $length if $length.defined;
        say $data   if $data.defined;
        say 'Verbosity ', ($verbose ?? 'on' !! 'off');
    }
raku example.p6
    24
    file.dat
    Verbosity off
raku example.p6 --file=foo --length=42 --verbose
    42
    foo
    Verbosity on
raku example.p6 --length=abc
    Usage:
      c.p6 [--length=<Int>] [--file=<Any>] [--verbose]

请注意,Raku 会在命令行解析时自动生成错误的完整用法消息。

11.21. 自动翻译

查找 Raku 版本的 Perl 5 构造的快速方法是通过自动翻译器运行它。

注意:这些翻译人员尚未完成。

11.21.1. 蓝虎

该项目致力于 Perl 代码的自动化现代化。它(还)没有 Web 前端,因此必须在本地安装才有用。它还包含一个单独的程序,用于将 Perl 5 正则表达式转换为 Raku。

11.21.2. Perlito

在线翻译!

该项目是一套 Perl 交叉编译器,包括 Perl 5 到 6 的转换。它有一个 Web 前端,因此无需安装即可使用。到目前为止,它仅支持 Perl 5 语法的子集。

11.21.3. Perl-ToRaku

Jeff Goff 为 Perl 5 设计的 Perl::ToRaku 模块是围绕 Perl::Critic 的框架设计的。它旨在将 Perl5 转换为可编译(如果不一定运行)的 Raku 代码,只需进行最少的更改。代码转换器是可配置和可插拔的,因此您可以创建和贡献自己的转换,并根据自己的需要定制现有的转换。您可以从 CPAN 安装最新版本,也可以在 GitHub 上实时关注项目。在线转换器可能在某些时候可用。

12. Raku 语法

12.1. 描述

关于 Perl 5 和 Raku 之间差异的全面(希望)描述。

12.2. 注意

我*不会*详细解释 Raku 语法。本文档旨在指导你从 Perl 5 中的工作原理过渡到 Raku 中的等效工具。有关 Raku 语法的完整文档,请参阅 Raku 文档。

12.3. 自由形式

Raku 仍然*主要是*自由形式。但是,有一些情况下,空白的存在或缺失现在很重要。例如,在 Perl 5 中,你可以省略关键字后面的空格(例如 while($x < 5)my($x, $y))。在 Raku 中,这个空白是必需的,因此 while ($x < 5)my ($x, $y)。但是,在 Raku 中,你可以完全省略括号:while $x < 5 `。这适用于 `iffor 等等。

奇怪的是,在 Perl 5 中,你可以在数组或散列与其下标之间以及后缀运算符之间留出空格。所以 $seen {$_} ` 是有效的。Raku 再不这样了。Raku 中现在必须是 `%seen{$_}

如果能让你感觉更好,你可以使用反斜杠来 “unspace” 空格,这样你就可以使用空格,否则它将被禁止。

有关详细信息,请参阅空白

12.3.1. 声明

正如函数 指南中所述,Raku 中没有 undef 。声明但未初始化的标量变量将计算其类型。换句话说,my $x;say $x; 会给你"(Any)"。my Int $y;say $y; 会给你"(Int)"。

12.3.2. 注释

# 在 Perl 5 中开始一个运行到行尾的注释。

嵌入式注释以井号字符和反引号开头,后跟开口括号字符,并持续到匹配的闭合括号字符。像这样:

if #`( why would I ever write an inline comment here? ) True {
    say "something stupid";
}

与 Perl 5 一样,你可以使用 pod 指令在注释 =begin comment 之前和 =end comment 之后创建多行注释。

12.3.3. 真和假

Perl 5 与 Raku 之间的一个真正区别在于,与 Perl 5 不同,Raku 将字符串`"0"` 视为真。数字 0 仍为 false,你可以使用前缀 + 将字符串 "0" 强制转换为数字以使其为 false。Raku 还具有实际的布尔类型,因此,在许多情况下,你可以使用 TrueFalse,而无需担心哪些值计为 true 和 false。

12.3.4. 语句修饰符

大多数情况下,语句修饰符仍然有效,但有一些例外。

首先,for 循环是 Perl 5 中已知的 foreach 循环,C 风格的 for`循环不用于 Raku。要获得该行为,你需要 `looploop 不能用作语句修饰符。

在 Raku 中,你无法使用 do {…​} while $x 形式。你将需要用 repeat 替换 do 形式。do {…​} until $x 类似。

12.3.5. 复合语句

Perl 5 的最大变化是 given 在 Raku 中默认不是实验性质的或禁用的了。有关 given 的详细信息,请参阅此页面

12.3.6. 循环控制

nextlastredo 在 Perl 5 到 Raku 中没有变化。

但是 continue,在 Raku 中不存在。你将在循环体中使用 NEXT 块。

# Perl 5
my $str = '';
for (1..5) {
    next if $_ % 2 == 1;
    $str .= $_;
}
continue {
    $str .= ':'
}

# Raku
my $str = '';
for 1..5 {
    next if $_ % 2 == 1;
    $str ~= $_;
    NEXT {
        $str ~= ':'
    }
}

12.3.7. For 循环

如上所述,C 风格的 for 循环在 Raku 中不称为 for 循环。它们只是 loop 循环。要编写无限循环,你不需要使用 C 语言风格的 loop (;;) {…​},只是完全省略规范:loop {…​}

12.3.8. Foreach 循环

在 Perl 5 中,for 除了用于 C 风格的 for 循环之外,它还是`foreach` 的同义词。在 Raku 中,for 仅用于 foreach 样式的循环。

12.3.9. Switch 语句

Raku 具有真实的 switch 语句,通过提供 given 与由处理的单个情况的 whendefault。基本语法是:

given EXPR {
    when EXPR { ... }
    when EXPR { ... }
    default { ... }
}

完整的细节可以在这里找到。

12.3.10. Goto

goto 目前尚未实施(尚未)。标签实现,可用作 nextlastredo 的目标:

FOO:                         # Labels end with colons, like in Perl 5
for ^10 {
    say "outer for before";
    for ^10 {
        say "inner for";
        last FOO;
    }
    say "outer for after";   # Will not show because of the "last"
}
=== outer for before
=== inner for

有关 goto 的计划,请参阅 <https://design.raku.org/S04.html#The_goto_statement>。

12.3.11. 省略语句

…​(以及 !!!???)用于创建存根(stub)声明。这比 Perl 5 中使用的 …​ 要复杂得多,所以你可能想要查看 https://design.raku.org/S06.html#Stub_declarations 以了解详细信息。尽管如此,尽管它在 Raku 中的作用得到了扩展,但它似乎还没有*明显的*理由说明它为什么不能完成它在 Perl 5 中所扮演的角色。

12.3.12. PODs: 嵌入式文档

Pod 已经在 Perl 5 和 Raku 之间发生了变化。可能最大的区别在于你需要将你的 pod 放在 =begin pod=end pod 指令之间。这里和那里也有一些调整。例如,正如我在编写这些文档时发现的那样,垂直条(|)在 X<> 代码中很重要,并且不清楚如何将字面 | 插入他们。你最好的选择可能是使用 Raku 解释器检查你的 pod。你可以使 --doc 开关执行此操作。例如 raku --doc Whatever.pod。这将输出任何问题到标准错误。(根据你安装 raku 的方式/位置,你可能需要指定 Pod::To::Text 的位置。)有关 Raku 样式 pod 的详细信息,请访问 <https://design.raku.org/S26.html>。

13. Perl 到 Raku 的指南 - 概览

这些文档不应该被误认为是初学者教程或 Raku 的宣传概述; 它旨在作为具有很强 Perl 5 背景的人学习 Raku 的技术参考,以及任何将 Perl 5 代码移植到 Raku 的人。

13.1. 果壳中的 Raku

果壳中的 Raku提供了语法,运算符,复合语句,正则表达式,命令行标志以及各种其他零碎内容的快速概述。

13.2. 句法差异

语法章节提供的 Perl 5 和 Raku 之间的语法区别的一个概述:它是如何保持大部分形式自由的,写注释的其他方法,以及 switch 如何是一个很 Raku 的东西。

13.3. Raku 中的运算符

运算符章节将引导您从 Perl 5 的 perlop运算符在 Raku 中的等价物。

13.4. Raku 中的函数

函数章节描述了所有的 Perl 5 函数和它们的 Raku 等价物和任何行为差异。它还提供了对提供 Perl 5 函数行为的生态系统模块的引用,这些函数存在于 Raku 中,具有稍微不同的语义(例如 shift),或者在 Raku 中不存在(例如 tie)。

13.5. Raku 中的特殊变量

特殊变量章节描述很多 Perl 5 中的特殊(标点符号)变量是否以及如何在 Raku 中的支持。

14. 并发

与大多数现代编程语言一样,Raku 被设计为支持并行、异步和并发。并行是指同时做多件事件。异步编程, 有时也被称为事件驱动或反应式编程, 是关于支持由程序中其他地方触发的事件引起的程序流的变化。最后,并发是关于协调对一些共享资源的访问和修改。

Raku 并发设计的目的是提供一个高层级的、可组合的和一致的接口,而不管如下所述的虚拟机通过工具层怎样为特定操作的系统来实现它。

此外,某些 Raku 的特性可以隐式地以异步的方式操作,所以为了确保这些特性的可预测的互操作,用户代码应尽可能避免使用较低层级的并发 API(如线程调度器),而使用较高层级的接口。

14.1. 高级 API

14.1.1. Promise

Promise(在其他编程环境中也被称为 future)封装了在获得 promise 时可能尚未完成或甚至未开始的计算结果。PromisePlanned 状态开始, 结果要么是 Kept 状态, 这意味着该 promise 已成功完成, 要么是 Broken 状态, 意味着该 promise 已失败。 通常这就是用户代码需要以并行或异步方式操作的使用最多的功能。

my $p1 = Promise.new;
say $p1.status;       # OUTPUT: «Planned␤»
$p1.keep('Result');
say $p1.status;       # OUTPUT: «Kept␤»
say $p1.result;       # OUTPUT: «Result␤»
                      # (since it has been kept, a result is available!)

my $p2 = Promise.new;
$p2.break('oh no');
say $p2.status;       # Broken
say $p2.result;       # dies, because the promise has been broken

CATCH { default { say .^name, ': ', .Str } };
# OUTPUT: «X::AdHoc+{X::Promise::Broken}: oh no␤»

Promise 通过组合, 例如通过链接(chaining), 通常通过 then 方法获取更多力量:

my $promise1 = Promise.new();
my $promise2 = $promise1.then(
    -> $v { say $v.result; "Second Result"}
);
$promise1.keep("First Result");
say $promise2.result;   # First Result \n Second Result

在这里 then 方法安排代码(即圆括号中的闭包)在第一个 Promise 为 kept 或 broken 时执行, 它自身返回一个新的 Promise, 这个新的 Promise 会在执行代码时与结果一块保存。 (如果代码执行失败则 broken ) keep 更改 promise 的状态为 Kept, 并设置结果为位置参数。result 阻塞当前执行的线程直到那个 promise 变为 kept 或 broken, 如果它是 kept, 那么它会返回那个结果(即传递给 keep 的值, ) 否则它会根据传递给 break 的值抛出异常。后者的行为如下所示:

my $promise1 = Promise.new();
my $promise2 = $promise1.then(-> $v { say "Handled but : "; say $v.result});
$promise1.break("First Result");
try $promise2.result;
say $promise2.cause;        # Handled but : \n First Result

当它在原来的作为参数传递的 promise 上调用 result 方法时, 这里的 break 会导致 then 代码块抛出异常, 这随后会导致第二个 promise 变为 broken, 在它的结果被接收时反过来引发一个异常。然后能从 cause 中访问那个实际的 Exception 对象。如果那个 promise 还没有变为 broken, 那么 cause 会引发 X::Promise::CauseOnlyValidOnBroken 异常。

Promise 也可以安排在未来自动保存(kept):

my $promise1 = Promise.in(5);
my $promise2 = $promise1.then(-> $v { say $v.status; 'Second Result' });
say $promise2.result; # 5 秒后打印出: Kept\n Second Result

in 方法创建了一个新的 promise 并安排一个新的任务在不早于所提供的秒数内在它身上调用 keep, 返回一个新的 Promise 对象。

promises 的一个非常频繁的用法是运行一段代码, 并且一旦它成功地返回就 keep 那个 promise, 或者当那块代码死掉时中断(break)那个 promise。start 方法为此提供了一种快捷方式:

my $promise = Promise.start(
    { my $i = 0; for 1 .. 10 { $i += $_ }; $i}
);
say $promise.status;    # Kept
say $promise.result;    # 55

这里返回的 promise 的结果(result)是从代码返回的值。类似地, 如果那段代码失败了(那个 promise 也因此被中断), 那么 cause 会成为抛出的那个 Exception 对象:

my $promise = Promise.start({ die "Broken Promise" });
try $promise.result; # Nil
say $promise.cause;  # Broken Promise
                     #  in block <unit> at <unknown file> line 1

这个模式太常见了以至于它还提供了子例程形式:

my $promise = start {
    my $i = 0;
    for 1 .. 10 {
        $i += $_
    }
    $i
}
my $result = await $promise;
say $result;

await 几乎等价于在由 start 返回的 promise 对象身上调用 result 但是它也会接受一组 promises 并返回每个 promise 的结果:

my $p1 = start {
    my $i = 0;
    for 1 .. 10 {
        $i += $_
    }
    $i
};
my $p2 = start {
    my $i = 0;
    for 1 .. 10 {
        $i -= $_
    }
    $i
};
my @result = await $p1, $p2;
say @result;            # [55 -55]

除了 await 之外, 两个类方法把几个 Promise 对象合并到一个新的 promise 对象中: 当所有原来的 promises 是 kept 或 broken 时, allof 返回一个 kept 状态的 promise:

my $promise = Promise.allof(
    Promise.in(2),
    Promise.in(3)
);

await $promise;
say "All done"; # Should be not much more than three seconds later

并且当原 promises 中的任何一个的状态变为 kept 或 broken 时, anyof 返回将为 kept 的新 promise:

my $promise = Promise.anyof(
    Promise.in(3),
    Promise.in(8600)
);

await $promise;
say "All done"; # Should be about 3 seconds later

不同于 await,然而如果不引用原来的 promise, 那么就访问不了原来状态为 kept 的 promise 的结果,因此当任务的完成或其他方面对于消费者来说比实际结果更重要时,或者当通过其它方式收集结果时。 你可能,例如,您可以创建一个依赖的 Promise,它会检查每个原始的 promise:

my @promises;
for 1..5 -> $t {
    push @promises, start {
        sleep $t;
        Bool.pick;
    };
}
say await Promise.allof(@promises).then({ so all(@promises>>.result) });

如果所有的 promise 都保持为 True, 那么它会打印 True, 否则会打印 False。

如果你正在创建一个 promise,你打算保持或中断自己,那么在你做之前, 你可能不想要任何可能会收到 promise 以无意(或否则)保持或中断该 promise 的代码。 为了这个目的,就有了方法 vow,它返回一个 Vow 对象,它成为 promise 能被保留或中断的唯一机制。 如果试图直接保持或断开这个 Promise ,则会抛出 X::Promise::Vowed 异常,只要 vow 对象保持私有,那么 promise 的状态就是安全的:

sub get_promise {
    my $promise = Promise.new;
    my $vow = $promise.vow;
    Promise.in(10).then({$vow.keep});
    $promise;
}

my $promise = get_promise();

# Will throw an exception
# "Access denied to keep/break this Promise; already vowed"
$promise.keep;

返回一个将被自动保存或断开的 promise 的方法,如 instart 将会做到这一点,所以没有必要这样做。

14.1.2. Supplies

Supply 是异步数据流传输机制,其可以以类似于其他编程语言中的"事件"的方式同时由一个或多个消费者消费,并且可以被视为开启"事件驱动"或反应式设计。

最简单的是,Supply 是一个消息流,可以有多个通过方法 tap 创建的订阅者,其数据项可以使用 emit 放置。

Supply 可以是现场的(live)或按需的(on-demand)。 现场(live)供应就像电视广播:那些调入(收听/收看)的人不会得到先前发出的值。 点播(on-demand)广播就像 Netflix:每个开始流式传输电影(点击电源)的人,总是从头开始(获取所有的值),不管有多少人正在观看它。 请注意,没有为`按需`供应保留历史记录,而是为供应的每次点击运行 supply 块。

Netflix: 在线观看电影的网站

实时供应(live Supply)由 Supplier 工厂创建,每个发出的值在添加时传递给所有活动的 tappers:

my $supplier = Supplier.new;
my $supply   = $supplier.Supply;

$supply.tap( -> $v { say $v });

for 1 .. 10 {
    $supplier.emit($_); # 1\n2\n3\n4\n5\n6\n7\n8\n9\n10
}

请注意,tapSupplier创建的 Supply 对象上调用,并且新值在 Supplier上发出。

或者作为由 supply 关键字创建的按需供应 Supply

my $supply = supply {
    for 1 .. 10 {
        emit($_);
    }
}
$supply.tap( -> $v { say $v });
# 1\n2\n3\n4\n5\n6\n7\n8\n9\n10

在这种情况下,供应块中的代码在每次供应返回的供应被窃取时执行,如下所示:

my $supply = supply {
    for 1 .. 10 {
        emit($_);
    }
}
$supply.tap( -> $v { say "First : $v" });
$supply.tap( -> $v { say "Second : $v" });

tap 方法返回一个 Tap 对象,它可以用来获取关于 tap 的信息,并且当我们不再对事件感兴趣时关闭它:

my $supplier = Supplier.new;
my $supply   = $supplier.Supply;

my $tap = $supply.tap( -> $v { say $v });

$supplier.emit("OK");
$tap.close;
$supplier.emit("Won't trigger the tap");

在供应对象(supply object)上调用 done 调用可以为任何 tap 指定的 done 回调,但不会阻止任何其他事件被发送到流,或者接收它们。

方法 interval 返回一个新的按需供应,它会以指定的间隔定期发出一个新事件。 发出的数据是从 0 开始的整数,对于每个事件递增。 以下代码输出 0 .. 5:

my $supply = Supply.interval(2);
$supply.tap(-> $v { say $v });
sleep 10;

这也可以使用 react 关键字书写(输出 0..4):

react {
    whenever Supply.interval(2) -> $v {
        say $v;
        done() if $v == 4;
    }
}

这里,whenever 关键字使用 .act 从提供的块在 Supply 上创建一个 tap。 当在其中一个 tap 中调用 done() 时,退出 react 块。

第二个参数可以提供给 interval,它指定第一个事件触发之前的延迟(以秒为单位)。 通过 interval 创建的 supply 的每个 tap 都有自 0 开始的自身序列,如下所示:

my $supply = Supply.interval(2);
$supply.tap(-> $v { say "First $v" });
sleep 6;
$supply.tap(-> $v { say "Second $v"});
sleep 10;

也可以从将要依次发出的值的列表中按需创建 Supply(供给),因此第一个按需示例(打印 1 到 10)可以写作:

react {
    whenever Supply.from-list(1..10) -> $v {
        say $v;
    }
}

可以使用方法 grepmap 分别过滤或转换现有的供应对象(supply object),以类似具名列表方法的方式创建新供应(supply):grep 返回这样一个供应(supply),以至于只有在源流上发出的那些事件的 grep 条件为真时才在第二个 supply 上发出:

my $supplier = Supplier.new;
my $supply = $supplier.Supply;
$supply.tap(-> $v { say "Original : $v" });
my $odd_supply = $supply.grep({ $_ % 2 });
$odd_supply.tap(-> $v { say "Odd : $v" });
my $even_supply = $supply.grep({ not $_ % 2 });
$even_supply.tap(-> $v { say "Even : $v" });
for 0 .. 10 {
    $supplier.emit($_);
}

map 返回一个新的 supply(供应),使得对于发送到原始供应的每个项目,发出作为传递给 map 表达式的结果的新项目:

my $supplier = Supplier.new;
my $supply = $supplier.Supply;
$supply.tap(-> $v { say "Original : $v" });
my $half_supply = $supply.map({ $_ / 2 });
$half_supply.tap(-> $v { say "Half : $v" });
for 0 .. 10 {
    $supplier.emit($_);
}

如果您需要在 supply(供应)完成时运行一个操作,您可以通过在对 tap 的调用中设置 donequit 选项来完成:

$supply.tap: { ... },
    done => { say 'Job is done.' },
    quit => {
        when X::MyApp::Error { say "App Error: ", $_.message }
    };

quit 块的工作方式非常类似于 CATCH。 如果异常被标记为由 whendefault 块看到,那么异常会被捕获并处理。 否则,异常继续沿调用树向上(即,与没有设置 quit 时行为相同)。

如果你伴随着 whenever 使用 react 或者 supply block 语法,你可以在你的 whenever 块中添加 phasers 来处理来自 tapped supply 的 donequit 消息:

react {
    whenever $supply {
        ...; # your usual supply tap code here
        LAST { say 'Job is done.' }
        QUIT { when X::MyApp::Error { say "App Error: ", $_.message } }
    }
}

这里的行为与在 tap 上设置 donequit 相同。

14.1.3. Channels

通道(Channel)是线程安全的队列,可以具有多个读取器和写入器,可以被认为在操作上与“fifo”(先进先出)或命名管道相似,除了它不启用进程间通信之外。 应该注意的是,作为真正的队列,发送到通道的每个值将仅在先读,先服务的基础上对于单个读取器可用:如果想要多个读取器能够接收可能想要发送的每个项目那么请考虑 Supply

项目(item)通过方法 send 排队到通道上,方法 receive 从队列中删除一个项目并返回,如果队列为空,则阻塞它直到发送新项目:

my $channel = Channel.new;
$channel.send('Channel One');
say $channel.receive;  # 'Channel One'

如果使用 close 方法关闭了通道,那么任何发送(send)都将导致抛出异常 X::Channel::SendOnClosed,并且如果队列中没有更多的项目,接收(receive) 将抛出一个 X::Channel::ReceiveOnClosed 异常。

方法 list 返回 Channel 上的所有项目,并将阻塞,直到其他项目被排队,除非通道关闭:

my $channel = Channel.new;
await (^10).map: -> $r {
    start {
        sleep $r;
        $channel.send($r);
    }
}
$channel.close;
for $channel.list -> $r {
    say $r;
}

还有从通道返回可用项目的非阻塞方法 poll, 或者, 如果没有项目或通道被关闭则返回 Nil,这当然意味着必须检查通道以确定其是否关闭:

my $c = Channel.new;

# Start three Promises that sleep for 1..3 seconds, and then
# send a value to our Channel
^3 .map: -> $v {
    start {
        sleep 3 - $v;
        $c.send: "$v from thread {$*THREAD.id}";
    }
}

# Wait 3 seconds before closing the channel
Promise.in(3).then: { $c.close }

# Continuously loop and poll the channel, until it's closed
my $is-closed = $c.closed;
loop {
    if $c.poll -> $item {
        say "$item received after {now - INIT now} seconds";
    }
    elsif $is-closed {
        last;
    }

    say 'Doing some unrelated things...';
    sleep .6;
}

# Doing some unrelated things...
# Doing some unrelated things...
# 2 from thread 5 received after 1.2063182 seconds
# Doing some unrelated things...
# Doing some unrelated things...
# 1 from thread 4 received after 2.41117376 seconds
# Doing some unrelated things...
# 0 from thread 3 received after 3.01364461 seconds
# Doing some unrelated things...

方法 closed 返回一个 Promise,当通道关闭时,它将被保存(kept)(因此在布尔上下文中将被计算为 True)。

.poll 方法可以与 .receive 方法结合使用,作为一种缓存机制,其中 .poll 返回的值不足是需要获取更多值并加载到通道的信号:

sub get-value {
    return $c.poll // do { start replenish-cache; $c.receive };
}

sub replenish-cache {
    for ^20 {
        $c.send: $_ for slowly-fetch-a-thing();
    }
}

可以使用通道代替前面描述的 wheneverreact 块中的 Supply

my $channel = Channel.new;
my $p = start {
    react {
        whenever $channel {
            say $_;
        }
    }
}

await (^10).map: -> $r {
    start {
        sleep $r;
        $channel.send($r);
    }
}

$channel.close;
await $p;

还可以使用 Channel 方法Supply 获得 Channel,该通道方法返回通过 Supply 上的 tap 馈送的通道:

my $supplier = Supplier.new;
my $supply   = $supplier.Supply;
my $channel = $supply.Channel;

my $p = start {
    react  {
        whenever $channel -> $item {
            say "via Channel: $item";
        }
    }
}

await (^10).map: -> $r {
    start {
        sleep $r;
        $supplier.emit($r);
    }
}

$supplier.done;
await $p;

Channel 将返回一个不同的通道,每次调用时都会使用相同的数据。 这可以用于例如将 Supply 输出到一个或多个通道以在程序中提供的不同接口。

14.1.4. Proc::Async

Proc::Async 构建在所描述的设施上以异步方式运行并与外部程序交互:

my $proc = Proc::Async.new('echo', 'foo', 'bar');

$proc.stdout.tap(-> $v { print "Output: $v" });
$proc.stderr.tap(-> $v { print "Error:  $v" });

say "Starting...";
my $promise = $proc.start;

await $promise;
say "Done.";

# Output:
# Starting...
# Output: foo bar
# Done.

命令的路径以及命令的任何参数都提供给该构造函数。 该命令将不被执行,直到调用 start,它将返回一个 Promise,当程序退出时该 Promise 变为 kept 状态。 程序的标准输出和标准错误分别从 stdoutstderr 方法中作为 Supply 对象提供,可以根据需要进行分接。

如果要写入程序的标准输入,您可以给构造函数提供 :w 副词,并使用方法 writeprintsay 在程序启动后写入打开的管道:

my $proc = Proc::Async.new(:w, 'grep', 'foo');

$proc.stdout.tap(-> $v { print "Output: $v" });

say "Starting...";
my $promise = $proc.start;

$proc.say("this line has foo");
$proc.say("this one doesn't");

$proc.close-stdin;
await $promise;
say "Done.";

# Output:
# Starting...
# Output: this line has foo
# Done.

一些程序(例如本例中没有文件参数的 grep)在关闭标准输入之前不会退出,因此在完成写入后可以调用 close-stdin,以允许由 start 返回的 Promise 的状态变为 kept。

14.2. Low-level APIs

14.2.1. Threads

最低级别的并发接口由 Thread 提供。 线程可以被认为是可以最终在处理器上运行的一段代码,其布置几乎完全由虚拟机和/或操作系统完成。 线程应该被考虑,对于所有意图,很大程度上是不受管理的,应避免在用户代码中直接使用它们。

线程可以被创建,然后随后实际运行:

my $thread = Thread.new(code => { for  1 .. 10  -> $v { say $v }});
# ...
$thread.run;

或者可以在单个调用中创建和运行:

my $thread = Thread.start({ for  1 .. 10  -> $v { say $v }});

在这两种情况下,由 Thread 对象封装的代码的完成可以用 finish 方法来等待,该方法将阻塞直到线程完成:

$thread.finish;

除此之外,没有用于同步或资源共享的其他设施,这主要是为什么应当强调线程不可能直接用于用户代码。

14.2.2. Schedulers

并发 API 的下一级由实现角色 Scheduler 定义的接口的类提供。 调度程序接口的目的是提供一种机制来确定使用哪些资源来运行特定任务以及何时运行它。 大多数较高级别的并发 API 是基于调度器构建的,并且用户代码根本不需要使用它们,尽管一些方法,例如在 Proc::AsyncPromiseSupply 中找到的方法允许您明确地提供调度器。

当前缺省全局调度程序在变量 $*SCHEDULER 中可用。

调度程序的主接口(确实是 Scheduler 接口所需的唯一方法)是 cue 方法:

method cue(:&code, Instant :$at, :$in, :$every, :$times = 1; :&catch)

这将按照由副词(如在 Scheduler 中记录的)所确定的方式使用由调度器实现的执行方案来调度 &code 中的 Callable 以执行。 例如:

my $i = 0;
my $cancellation = $*SCHEDULER.cue({ say $i++}, every => 2 );
sleep 20;

假设 $*SCHEDULER 没有从默认值改变,将以大约每两秒打印数字 0 到 10(即使用操作系统调度容差)。 在这种情况下,代码将被调度运行,直到程序正常结束,但是该方法返回一个 Cancellation 对象,它可以用来在正常完成之前取消调度执行:

my $i = 0;
my $cancellation = $*SCHEDULER.cue({ say $i++}, every => 2 );
sleep 10;
$cancellation.cancel;
sleep 10;

应该只输出 0 到 5,

尽管 Scheduler 接口提供的所有功能明显优于 Thread 提供的,但是通过更高级别的接口可以获得所有的功能,并且不应该有必要直接使用调度器,除非在上述情况下,调度器可以被明确地提供给某些方法。

如果库具有特殊要求,例如 UI 库可能希望所有代码在单个 UI 线程中运行,或者可能需要一些定制的优先级机制,则库可能希望提供备选的调度器实现,然而,被作为标准的实现和下面的描述应该足以满足大多数用户代码。

14.2.3. ThreadPoolScheduler

ThreadPoolScheduler 是默认调度程序,它维护一个根据需要分配的线程池,根据需要创建新的线程,直到创建调度程序对象时作为参数给出的最大数目(默认值为 16)。如果超过最大值 那么 cue 可以对代码进行排队,直到线程变得可用为止。

Rakudo 允许在程序启动时由环境变量 RAKUDO_MAX_THREADS 在默认调度程序中设置允许的最大线程数。

14.2.4. CurrentThreadScheduler

CurrentThreadScheduler 是一个非常简单的调度程序,它将始终调度代码在当前线程上立即运行。 暗示这个调度器的 cue 将阻塞,直到代码完成执行,把它的效用限制在某些特殊情况,如测试。

14.2.5. Locks

Lock 在并发环境中提供了保护共享数据的低级机制,并因此是高级 API 中支持线程安全性的关键,这在其他编程语言中有时称为 “Mutex”。 因为较高级别的类(PromiseSupplyChannel)在需要时使用 Lock,所以用户代码不可能直接使用 Lock。

Lock 的主接口是方法 protect,它确保一个代码块(通常称为“临界区”)只能在一个线程中同时执行:

my $lock = Lock.new;

my $a = 0;

await (^10).map: {
    start {
            $lock.protect({
                my $r = rand;
                sleep $r;
                $a++;
            });
    }
}

say $a; # 10

protect 返回代码块返回任何东西。

因为 protect 将阻止任何等着要执行临界区的线程,所以代码应该尽可能快。

14.3. Safety Concerns

一些共享数据并发问题相比其他问题并不明显。 关于这个问题的好文章请看这个博客

要注意的一个特别的问题是当容器自动更新或发生扩展时。 当数组哈希条目被赋初始值时,底层结构被更改,并且那个操作不是异步安全的。 例如,在这段代码中:

my @array;
my $slot := @array[20];
$slot = 'foo';

第三行是临界区,因为那就是数组被扩展之时。 最简单的解决方法是使用 <Lock> 来保护临界区。 一个可能更好的解决方案是重构代码,以使共享容器不是必需的。

15. Grammar 指南

15.1. 开始之前

15.1.1. 为什么是 grammar?

Grammars 解析字符串并从这些字符串返回数据结构。Grammars 可用于编写执行程序以确定程序是否可以运行(如果它是一个有效的程序),将网页分解成组成部分,或在其它的东西中识别句子的不同部分。

15.1.2. 我什么时候该使用 grammar?

如果你有驯服或解释的字符串,grammar 提供工具来完成这项工作。

该字符串可能是一个文件, 您想把它拆分成多个章节; 也许是一个协议,比如 SMTP,你需要指定哪些"命令"来自用户提供的数据;也许你正在设计自己的领域特定语言。Grammars 可以提供帮助。

15.1.3. grammar 的广义概念

正则表达式(Regexes)适用于查找字符串中的模式。然而,对于一些任务来说,如同时查找多个模式,或者组合模式,或者单独测试可能围绕字符串正则表达式的模式是不够的。

在使用 HTML 时,您可以定义一个 grammar 来识别 HTML 标记,包括开始和结束元素以及它们之间的文本。然后,您可以将这些元素组织到数据结构中,例如数组或散列。

15.2. 更多 Grammar 技术

你总是会遇到令人头疼的字符串解析。举个例子, 据说 HTML 不能被有效地分解和解析,只需使用正则表达式来排序元素。另一个例子是定义单词和符号可能构成语言并提供含义的顺序。这正 和 Perl 的 Gramamr 系统完美契合。

Grammar 非常适合接受字符串,试图理解它们,然后将它们保存到一个你实际可以使用的数据结构中。如果你有某种带顺序或解释类型的字符串,Grammar 给你一些很强大的工具,使解析字符串更容易。

你的字符串可能是整个文件,你需要分成几个部分。也或许是一行一行的。也许你有一个正在使用的 SMTP 那样的协议,想要一个方便有条理的方式来定义哪些"命令"需要在用户数据的后面,使协议工作。也许你想创建自己的基于字符串的协议。也许你正在设计自己的语言。

正则表达式(regex)很好地在字符串中查找模式并操作它们。然而,当你需要同时找到多个模式,或者需要组合模式,或者测试可能围绕字符串的模式或其他模式 - 单单用正则表达式是不够的。

Grammar 提供了一种方式来定义如何使用正则表达式来检查字符串,并且可以将这些正则表达式组合在一起以提供更多的意义。

例如,在 HTML 的情况下,您可以定义一个语法,它可以识别 HTML 标记(开始和结束元素以及它们之间的文本),并通过将这些元素填充到数据结构中来对这些元素进行操作,例如数组或散列,然后可以轻松使用。实质上,Grammar 提供了一种定义可用于解析任意大小和复杂度的字符串的完整语言或规范的手段。

15.2.1. 概念描述

Gramamr 被定义为对象, 就像 Perl 中的其它东西。从技术上讲, Gramamr 是普通的类加上一点额外的魔法, 我们稍后就说到它 — 还有一点限制。你像类那样命名和定义一个 Grammar, 除了使用「grammar」关键字代替「class」。

grammar My::Gram { ..methods 'n stuff... }

Grammar 包含像方法那样的元素, 这些方法叫做 regex, tokenrule。这些方法是有名字的, 就像方法有名字一样。它们每一个都定义一个 regex, token 或 rule(它们几乎是同样的东西(并不真的一样))。

一旦你定义了你的 Grammar, 在你的程序中通过 Grammar 的名字调用它并传递你想解析的字符串。该字符串将通过你的 regex, token 和 rule "方法"定义的规则运行。 完成后,将返回一个 Match 对象,该对象已填充了用于定义方法的名称所结构化并存储的数据。

my $matchObject = My::Gram.parse($what-a-big-string-you-have);

现在,你可能想知道,如果我让所有这些定义的正则表达式只返回他们的结果,那么这该如何帮助在字符串中向前或向后解析东西呢,或需要从多个那样的正则表达式组合的东西。 ..这就是 grammar action 发挥作用的地方。

对于你的 grammar 中匹配的每个"方法",你会得到一个可调用的动作,用那个匹配你可以做一些有趣或聪明的事情。 你还可以得到一个最重要的 action,你可以使用这个 action 把它们捆绑在一起,并自定义构建一个你可能想要返回的数据结构,其中所有疯狂的字符串解析在你很好的排序和定义的数据结构是有意义的。 默认情况下,此 over-arching 方法称为 TOP。 我们也会得到更多的。

15.2.2. 技术概览

Grammars 就像类那样定义, 除了使用 grammar 关键字代替 class. grammars 中的「methods」叫做 regex, token, 或 rule。虽然 Regex 方法慢但是彻底 — 它们会在字符串中向后查看并真的尝试匹配。Token 方法更快一点并且它们忽略空白。Rule 方法和 token 方法一样, 但是它们在你的"regex" 定义中消费空白。

当方法(regex, token 或 rule)在 grammar 中匹配后, 匹配到的字符串被放入最终将返回的 Match 对象中, 并且它将使用与您选择命名的方法相同的名称。

grammar My::Gram {
  token TOP { <thingy> .* }
  token thingy { 'clever_text_keyword' }
}

所以在这里,如果你写 my $match = My::Gram.parse($string) - 并且你的字符串以 'clever_text_keyword' 开头, 那么你会得到一个匹配对象,在你的匹配对象中包含用「thingy」 标记的 'clever_text_keyword' 字符串。 这些可以变得越来越复杂,根据你的需要,如你所想。

现在, 我们说说 TOP。 TOP 方法(regex, token or rule)是必须匹配一切的(默认)的包罗万象的 regex。 如果传递进来解析的字符串与 TOP regex 不匹配,则返回的匹配对象将为空(Any)。

正如你可以看到的,在 TOP 中,提到了 <thingy> 标记。 <thingy> 被定义在下一行,token thingy …​。 这意味着 'clever_text_keyword' 必须是传入的字符串中的第一个东西,否则 grammar 解析将失败,而我们将得到一个空匹配。 这对于识别有人可能给你应该被丢弃的畸形的东西是极好的。

15.3. 通过一个例子学习 Grammar - REST 设计

让我们假设我们要将一个 URL 解析成组成 RESTful 请求的组件部分。假设我们希望网址的工作方式如下:

  • URI 的第一部分,我们称之为"主体",如零件,产品或人。

  • URI 的第二部分,我们称之为"命令",就像标准的 CRUD 东西(创建,检索,更新或删除)。

  • URI 的第三部分将是任意数据。也许我们将使用的具体 ID,或者一个由 "/" 分隔的长列表数据。

  • 当我们得到一个 URL 时,我们需要把上面的 1-3 放在一个很好的我们可以使用的数据结构中,而不必做各种分割,并且可以很容易地在未来改变或扩展(或扩展) 。

因此,如果我们在服务器上有一个 "/product/update/7/notify" 的 URI,我们希望我们的 Grammar 给我们一个很好的 $match 对象,它有一个 "product" 的 "subject",一个 "update" 的 "command" 和 "7/notify" 的"数据"(现在)。

我们做的第一件事是定义 grammar 类。我们将需要定义我们的主题,命令和数据。我想我们将为他们使用 token,因为我们不关心正则表达式中的空格。

grammar REST {
  token subject { \w+ }
  token command { \w+ }
  token data    { .*  }
}

到目前为止,这个 REST Grammar 说,我们想要一个只是单词字符的主题,一个只是单词字符的命令和剩余全部是字符串的数据(在这种情况下为 URI)。

但是在我们的大字符串中,我们不知道这些正则表达式匹配将会进入什么顺序。我们需要能够将这些匹配的 token 放在我们将作为该字符串传递的 URI 的更大的上下文中。 这就是 TOP 方法要做的。 因此,我们添加 TOP,并在其中放置我们的 token 名称,以及其它应该出现的有效字符串。

grammar REST {
  token TOP { '/' <subject> '/' <command> '/' <data> }
  token subject { \w+ }
  token command { \w+ }
  token data    { .*  }
}

实际上,您可以用它从基本的 CRUD 的 URI 中提取您的数据,其中包含所有 3 个参数:

my $match = REST.parse('/product/update/7/notify');
say $match;

输出:

«「/product/update/7/notify」␤
 subject => 「product」
 command => 「update」
 data => 「7/notify」»

当然,可以使用 $match<subject>$match<command>$match<data> 直接访问数据以返回解析的值。 它们每个都包含可以进一步工作的匹配对象,或强制转换为字符串($match<command>.Str

15.3.1. 添加一点灵活性

到目前为止,REST 语法将处理检索,删除和更新。 但是,create 命令没有第三部分(数据部分)。 这意味着如果我们尝试解析 creat URL,我们的 Grammar 将无法匹配。 为了避免这种情况,我们需要使最后一个数据位置匹配可选,以及它前面的 '/'。 这很容易通过为分组的 '/' 和 TOP token 的数据组件添加一个问号来表示它们的可选性质,就像一个普通的正则表达式那样。 所以现在我们有:

grammar REST {
    token TOP     { '/' <subject> '/' <command> [ '/' <data> ]? }
    token subject { \w+ }
    token command { \w+ }
    token data    { .* }
}

my $m = REST.parse('/product/create');
say $m<subject>, $m<command>;

# OUTPUT: «「product」「create」␤»

让我们想象,为了演示的目的,我们可能想允许用户从终端输入这些相同的 URI。 在这种情况下,他们可能在 '/' 之间放置空格,因为用户容易破坏事物。 如果我们想要适应这种可能性,我们可以用另一个 token 替换 TOP 中的 '/',以允许在它的任何一边的空格。

grammar REST {
    token TOP     { <slash><subject><slash><command>[<slash><data>]? }
    token subject { \w+ }
    token command { \w+ }
    token data    { .* }

    token slash   { \s* '/' \s* }
}

my $m = REST.parse('/ product / update /7 /notify');
say $m;

# OUTPUT: «「/ product / update /7 /notify」␤
#          slash => 「/ 」
#          subject => 「product」
#          slash => 「 / 」
#          command => 「update」
#          slash => 「 /」
#          data => 「7 /notify」»

现在我们在我们的匹配对象中得到一些额外的垃圾,即那些斜线,但有一些非常好的方法,使我们得到一个整洁的返回值。

15.3.2. 添加一些约束

我们希望我们的 RESTful Grammar 只允许 CRUD 操作。 还有我们想要解析的东西。 这意味着我们上面的"命令"应该有四个值之一:create, retrieve, update 或 delete。

有几种方法来完成这个。 例如,您可以更改 command 方法:

token command { \w+ }

# ...becomes...

token command { 'create'|'retrieve'|'update'|'delete' }

要成功解析 URI,/ 之间的字符串的第二部分必须是那些 CRUD 值之一,否则解析失败。这正是我们想要的。

还有另一种技术可以在选项膨胀时提供更大的灵活性并提高可读性:原型正则表达式(proto-regexes)。

为了利用这些原型正则表达式(实际上是 multi methods)将我们限制为有效的 CRUD 选项,我们将用以下代替 token command:

proto token command {*}
token command:sym<create>   { <sym> }
token command:sym<retrieve> { <sym> }
token command:sym<update>   { <sym> }
token command:sym<delete>   { <sym> }

sym 关键字用于创建各种原型正则表达式(proto-regex)选项。每个选项都被命名(例如, sym<update>), 并且为了使用该选项,会使用相同的名字自动生成一个特殊的 <sym> token。

可以在原型正则表达式选项块中使用 <sym> token 以及其他用户定义的 tokens 来定义特定的"匹配条件"。正则表达式 tokens 是编译过的形式,一旦定义,随后就不能被副词动作(例如: i)修改。因此,由于它是自动生成的,所以特殊的 <sym> token 仅在需要与选项名称完全匹配时才有用。

如果对于其中一个原型正则表达式选项,出现匹配条件,则整个原型的搜索终止。匹配数据以匹配对象的形式分配给父原型 token。如果使用特殊 <sym> token,并形成全部或部分实际匹配,则将其保留为匹配对象中的子级别,否则它将不存在。

使用这样的原型正则表达式给了我们很大的灵活性。例如,不是返回 <sym>,在这种情况下是匹配的整个字符串,我们可以输入自己的字符串,或做其他有趣的事情。我们可以用"token subject"方法做同样的事,并将其限制为仅对有效主题(如’part’或’people’等)进行正确解析。

15.3.3. 把我们的 RESTful Grammar 组合在一块

目前为止我们的 RESTful URIs 的处理如下:

grammar REST
{
    token TOP { <slash><subject><slash><command>[<slash><data>]? }

    proto token command {*}
    token command:sym<create>   { <sym> }
    token command:sym<retrieve> { <sym> }
    token command:sym<update>   { <sym> }
    token command:sym<delete>   { <sym> }

    token subject { \w+ }
    token data    { .* }
    token slash   { \s* '/' \s* }
}

让我们看看各种 URI,以及它们在通过我们的 Grammar 时是如何表现的。

my @uris = ['/product/update/7/notify',
            '/product/create',
            '/item/delete/4'];

for @uris -> $uri {
    my $m = REST.parse($uri);
    say "Sub: $m<subject> Cmd: $m<command> Dat: $m<data>";
}

# OUTPUT: «Sub: product Cmd: update Dat: 7/notify␤
#          Sub: product Cmd: create Dat:
#          Sub: item Cmd: delete Dat: 4»

请注意,由于 <data> 与第二个字符串没有匹配,因此 $m<data> 将为 Nil,然后在 say 函数的字符串上下文中使用它会发出警告。

只用 grammar 的这一部分,我们就能获得几乎所有我们正在寻找的东西。 URI 被解析,我们得到一个数据结构。

data token 将 URI 的整个末尾作为一个字符串返回。 4 很好。但是从 '7/notify' 中我们只需要那个 7。为了得到 7,我们将使用 grammar 类的另一个特性: actions。

15.4. Grammar Actions

在 Grammar 类中使用 Grammar actions 来处理匹配。Actions 在它们自己的类中定义,与 grammar 类不同。

您可以将 grammar action 看作 grammar 插件扩展模块的一种。很多时候你都会很开心的使用 grammars。但是当你需要进一步处理其中的一些字符串时,你可以插入 Actions 扩展模块。

要使用 action,可以使用名为 actions 的命名参数,它应该包含 action 类的一个实例。通过上面的代码,如果我们的 action 类调用了 REST-actions,我们会像这样解析 URI 字符串:

my $matchObject = REST.parse($uri, actions => REST-actions.new);

#   …or if you prefer…

my $matchObject = REST.parse($uri, :actions(REST-actions.new));

如果你将你的 action 方法命名为与你的 grammar 方法(tokens,regexes,rules)相同的名称,那么当您的 grammar 方法匹配时,具有相同名称的 action 方法将自动调用。该方法还将传递相应的匹配对象(由 $/ 变量表示)。

我们来看一个例子。

我们回到我们离开的地方:

grammar REST
{
    token TOP { <slash><subject><slash><command>[<slash><data>]? }

    proto token command {*}
    token command:sym<create>   { <sym> }
    token command:sym<retrieve> { <sym> }
    token command:sym<update>   { <sym> }
    token command:sym<delete>   { <sym> }

    token subject { \w+ }
    token data    { .* }
    token slash   { \s* '/' \s* }
}

回想一下,我们想要进一步处理 data token "7/notify", 以获得 7. 为此,我们将创建一个与具名 token 名称相同的方法的 action 类。在这种情况下,我们的 token 被命名为 data,因此我们的方法也被命名为 data

class REST-actions
{
    method data($/) { $/.split('/') }
}

现在,当我们通过 Grammar 传递 URI 字符串时,data token 匹配将传递给 REST-actions 的 data 方法。action 方法会按照 / 字符拆分字符串,返回列表的第一个元素将是 ID 号 (即 "7/notify" 中的 7)。

但你高兴的太早了。

15.4.1. 用 "make" 和 "made" 使 grammars 保持整洁

如果 grammar 在 data 上调用上面的 action,那么 data 方法将被调用,但是返回到程序的大的 TOP grammar 匹配结果中不会显示任何内容。 为了使 action 的结果显示出来,我们需要在这个结果上调用 make,这个结果可以是很多东西,包括字符串,数组或散列结构。

你可以想象,make 把该结果存到 grammar 中一个特殊的容器化区域中。 我们所制作(make)的所有东西,稍后都可以通过 made 来访问。

因此,代替我们的上面的 REST-actions 类,我们应该写:

class REST-actions
{
    method data($/) { make $/.split('/') }
}

当我们为 match split(它返回一个列表)中添加 make 时,这个 action 将返回一个数据结构给我们的 grammar,它将与原 grammar 的 data token 分开存储。 这样,如果我们需要,我们可以操作两者。

如果我们想从这个长的 URI 中访问 7 这个 ID, 那么我们访问从我们所制成的(made)的 data action 返回的列表的第一个元素:

my $uri = '/product/update/7/notify';

my $match = REST.parse($uri, actions => REST-actions.new);

say $match<data>.made[0];  # OUTPUT: «7␤»
say $match<command>.Str;   # OUTPUT: «update␤»

在这里,我们在 data 上调用 made,因为我们想要我们所制成的(made)(使用 make)action 的结果以得到分割后的数组。这好极了!但是,如果我们能够构造(make)一个包含我们想要的所有东西的更友好的数据结构,而不是强转类型和牢记数组,是不是更好?

就像 Grammar 中匹配整个字符串的 TOP, actions 也有一个 TOP 方法。我们可以构造(make)所有单独的匹配组件,如 datasubjectcommand,然后我们可以将它们放置在我们将在 TOP 中构造(make)的数据结构中。当我们返回最终的匹配对象时,之后就可以访问该数据结构了。

要做到这一点,我们要做的是将方法 TOP 添加到 action 类中,在该方法中,从组件片段中构造(make)出我们喜欢的任何数据结构。

所以,我们的 action 类现在变成:

class REST-actions
{
    method TOP ($/) {
        make { subject => $<subject>.Str,
               command => $<command>.Str,
               data    => $<data>.made }
    }

    method data($/) { make $/.split('/') }
}

在我们的 TOP 方法中,subject 与我们在 grammar 中匹配的 subject 保持相同。 此外, command 返回匹配到的(create, update, retrieve, 或 delete)的有效 <sym>。 我们把每个匹配都强转为 .Str,因为我们不需要整个匹配对象。

但是我们想要确定的是,在 $<data> 对象上使用 made 方法,因为我们想要访问那个我们在 action 中使用 make 制成的(made)的分割,而不是正确的 $<data> 对象。

我们在 grammar action 的 TOP 方法中构造(make)一些东西之后,我们可以通过在 grammar 结果对象上通过调用 made 方法来访问所有的自定义值。 代码现在变成:

my $uri = '/product/update/7/notify';

my $match = REST.parse($uri, actions => REST-actions.new);

my $rest = $match.made;
say $rest<data>[0];   # OUTPUT: «7␤»
say $rest<command>;   # OUTPUT: «update␤»
say $rest<subject>;   # OUTPUT: «product␤»

如果你不需要完整的返回匹配对象,你可以从你的 actions 的 TOP 方法中只返回 made 后的数据。

my $uri = '/product/update/7/notify';

my $rest = REST.parse($uri, actions => REST-actions.new).made;

say $rest<data>[0];   # OUTPUT: «7␤»
say $rest<command>;   # OUTPUT: «update␤»
say $rest<subject>;   # OUTPUT: «product␤»

哦,我们忘了摆脱那个丑陋的数组元素编号了吗? 嗯。 让我们在 TOP grammar 的自定义返回中构造(make) 一个新东西 - 我们称之为 subject-id,并将它设置为 <data> 的第 0 个元素。

class REST-actions
{
    method TOP ($/) {
        make { subject    => $<subject>.Str,
               command    => $<command>.Str,
               data       => $<data>.made,
               subject-id => $<data>.made[0] }
    }

    method data($/) { make $/.split('/') }
}

现在我们可以这样做:

my $uri = '/product/update/7/notify';

my $rest = REST.parse($uri, actions => REST-actions.new).made;

say $rest<command>;    # OUTPUT: «update␤»
say $rest<subject>;    # OUTPUT: «product␤»
say $rest<subject-id>; # OUTPUT: «7␤»

下面是完整的代码:

grammar REST
{
    token TOP { <slash><subject><slash><command>[<slash><data>]? }

    proto token command {*}
    token command:sym<create>   { <sym> }
    token command:sym<retrieve> { <sym> }
    token command:sym<update>   { <sym> }
    token command:sym<delete>   { <sym> }

    token subject { \w+ }
    token data    { .* }
    token slash   { \s* '/' \s* }
}


class REST-actions
{
    method TOP ($/) {
        make { subject    => $<subject>.Str,
               command    => $<command>.Str,
               data       => $<data>.made,
               subject-id => $<data>.made[0] }
    }

    method data($/) { make $/.split('/') }
}

15.4.2. 直接添加 actions

上面我们看到如何将 grammars 与 actions 对象相关联,并在匹配对象上执行 actions。但是,当我们想要处理匹配对象时,这不是唯一的方法。看下面的例子:

grammar G {
  rule TOP { <function-define> }
  rule function-define {
    'sub' <identifier>
    {
      say "func " ~ $<identifier>.made;
      make $<identifier>.made;
    }
    '(' <parameter> ')' '{' '}'
    { say "end " ~ $/.made; }
  }
  token identifier { \w+ { make ~$/; } }
  token parameter { \w+ { say "param " ~ $/; } }
}

G.parse('sub f ( a ) { }');
# OUTPUT: «func f␤param a␤end f␤»

这个例子是解析器的缩版。让我们更专注于它显示的功能。

首先,我们可以在 grammar 本身中添加 action,一旦正则表达式的控制流到达它们,就会执行这些 action。请注意,action 对象的方法将始终在整个正则表达式项匹配后执行。其次,它展示了 make 真正做了什么,它不过是 $/.made = …​ 的语法糖。这个技巧引入了一种从正则表达式 item 中传递消息的方法。

希望这有助于向您介绍 Raku 中的 Grammar,并向您展示 grammar 和 grammar action 类是如何协同工作的。有关更多信息,请查看更高级的 Perl Grammar 指南

对于更多的 Grammar 调试,请参见 Grammar::Debugger。它为每个 grammar tokens 提供了断点调试和颜色高亮的匹配(MATCH)和匹配失败(FAIL)的输出。

16. 创建操作符

通过使用 sub 关键字后跟 prefix, infix, postfix, circumfix, 或 postcircumfix; 声明运算符; 然后是冒号结构中的冒号和运算符名称。对于(后)环缀操作符,用空格分隔这两部分。

sub hello {
    say "Hello, world!";
}

say &hello.^name;   # OUTPUT: «Sub␤»
hello;              # OUTPUT: «Hello, world!␤»

my $s = sub ($a, $b) { $a + $b };
say $s.^name;       # OUTPUT: «Sub␤»
say $s(2, 5);       # OUTPUT: «7␤»

# Alternatively we could create a more
# general operator to sum n numbers
sub prefix:<Σ>( *@number-list ) {
    [+] @number-list
}

say Σ (13, 16, 1); # OUTPUT: «30␤»

sub infix:<:=:>( $a is rw, $b is rw ) {
    ($a, $b) = ($b, $a)
}

my ($num, $letter) = ('A', 3);
say $num;          # OUTPUT: «A␤»
say $letter;       # OUTPUT: «3␤»

# Swap two variables' values
$num :=: $letter;

say $num;          # OUTPUT: «3␤»
say $letter;       # OUTPUT: «A␤»

sub postfix:<!>( Int $num where * >= 0 ) { [*] 1..$num }
say 0!;            # OUTPUT: «1␤»
say 5!;            # OUTPUT: «120␤»

sub postfix:<♥>( $a ) { say „I love $a!“ }
42♥;               # OUTPUT: «I love 42!␤»

sub postcircumfix:<⸨ ⸩>( Positional $a, Whatever ) {
    say $a[0], '…', $a[*-1]
}

[1,2,3,4]⸨*⸩;      # OUTPUT: «1…4␤»

constant term:<♥> = "♥"; # We don't want to quote "love", do we?
sub circumfix:<α ω>( $a ) {
    say „$a is the beginning and the end.“
};

α♥ω;               # OUTPUT: «♥ is the beginning and the end.␤»

17. 模块包

注意 “模块”是 Raku 中的重载术语; 本文档重点介绍 module 声明符的使用。

17.1. 什么是模块?

模块,如类和 grammars,是一种。模块对象是 ModuleHOW 元类的实例; 这提供了某些功能,可用于创建命名空间,版本控制,代理和数据封装(另请参见角色)。

要创建模块,请使用 module 声明符:

module M {}
say M.HOW;   # OUTPUT: «Raku::Metamodel::ModuleHOW.new»

这里我们定义一个名为 M 的新模块; 内省 HOW 确认了底层的元类 MRaku::Metamodel::ModuleHOW

17.1.1. 何时使用模块

模块主要用于封装不属于类或角色定义的代码和数据。模块内容(类,子程序,变量等)可以从具有 is export trait 的模块中导出; 一旦`import` 或 use 了模块,这些内容在调用者的命名空间中就可用了。模块还可以选择性地在其命名空间中通过 our 暴露符号以进行限定引用。

17.1.2. 使用模块

为了说明模块作用域和导出规则,我们首先定义一个简单的模块 M

module M {
  sub greeting ($name = 'Camelia') { "Greetings, $name!" }
  our sub loud-greeting (--> Str)  { greeting().uc       }
  sub friendly-greeting is export  { greeting('friend')  }
}

回想一下,子例程是词法作用域的,除非另有说明(声明符 sub 等效于 my sub),因此`greeting` 在上面的示例中,词法作用域为模块并且在其外部不可访问。我们还使用 our 声明符定义了 loud-greeting,这意味着除了在词法作用域内,它还在模块的符号表中起了别名。最后,friendly-greeting 标记为导出; 导入模块时,它将在*调用者的*符号表中注册:

import M;               # import the module
say M::loud-greeting;   # OUTPUT: «GREETINGS, CAMELIA!»
say friendly-greeting;  # OUTPUT: «Greetings, friend!»

17.2. 磁盘上的模块

虽然 .pm.pm6 文件(以下简称: .pm6) 有时被称为“模块”,但它们实际上只是在您写了 needuse 或者 require 时加载和编译的普通文件。

对于我们一直使用的意义上提供模块的 .pm6 文件,它需要如上所述用的用 module 声明一个模块。例如,通过将模块 M 放入 Foo.pm6 内部,我们可以按如下方式加载和使用模块:

use Foo;                # find Foo.pm6, run need followed by import
say M::loud-greeting;   # OUTPUT: «GREETINGS, CAMELIA!»
say friendly-greeting;  # OUTPUT: «Greetings, friend!»

注意文件名和模块名之间的解耦 - .pm6 文件可以声明零个或多个具有任意标识符的模块。

17.2.1. 文件和模块命名

我们通常希望 .pm6 文件提供*单个*模块,仅此而已。这里的常见约定是文件 basename 与模块名称匹配。回到 Foo.pm6,显而易见的是,它仅提供单个模块,M; 在这种情况下,我们可能想要重命名 MFoo。修改后的文件将为:

module Foo {
  sub greeting ($name = 'Camelia') { "Greetings, $name!" }
  our sub loud-greeting (--> Str)  { greeting().uc       }
  sub friendly-greeting is export  { greeting('friend')  }
}

可被调用者更一致地使用(注意 use FooFoo:: 之间的关系):

use Foo;
say Foo::loud-greeting;  # OUTPUT: «GREETINGS, CAMELIA!»
say friendly-greeting;   # OUTPUT: «Greetings, friend!»

如果 Foo.pm6 在源树中放置得更深,例如在 lib/Utils/Foo.pm6 中,我们可以选择命名模块 Utils::Foo 以保持一致性。

unit 关键字

只提供单个模块的文件可以用 unit 关键字更简洁地编写; unit module 指定编译单元的其余部分是声明的模块的一部分。这里 Foo.pm6 使用 unit 重写:

unit module Foo;

sub greeting ($name = 'Camelia') { "Greetings, $name!" }
our sub loud-greeting (--> Str)  { greeting().uc       }
sub friendly-greeting is export  { greeting('friend')  }

单元声明后的所有内容都是 Foo 模块规范的一部分。

(请注意,unit 也可以用于 classgrammarrole)。

17.2.2. 如果我省略了`module`会发生什么?

为了更好地理解在 Foo.pm6module 声明符在做什么,让我们将它与变体文件 Bar.pm6 进行对比,它省略了声明。下面的子程序定义几乎相同(唯一的区别在于 greeting 的正文,为了清晰起见而修改):

sub greeting ($name = 'Camelia') { "Greetings from Bar, $name!" }
our sub loud-greeting (--> Str)  { greeting().uc                }
sub friendly-greeting is export  { greeting('friend')           }

提醒一下,这是我们以前使用 Foo.pm6 的方式,

use Foo;
say Foo::loud-greeting;  # OUTPUT: «GREETINGS, CAMELIA!»
say friendly-greeting;   # OUTPUT: «Greetings, friend!»

这是我们使用 Bar.pm6 的方式,

use Bar;
say loud-greeting;       # OUTPUT: «GREETINGS FROM BAR, CAMELIA!»
say friendly-greeting;   # OUTPUT: «Greetings from Bar, friend!»

注意 loud-greeting 的使用,而不是 Bar::loud-greeting 因为 Bar 不是已知符号(我们没有在 Bar.pm6 中创建一个以那个名字命名的 module)。但是为什么 loud-greeting 是可调用的, 即使我们没有将其标记为导出。答案很简单,Bar.pm6 不创建一个新的包命名空间 - $?PACKAGE 仍设置为 GLOBAL 当我们将 loud-greeting 声明为 our 时,它被注册到 GLOBAL 符号表中。

词法别名和安全

值得庆幸的是,Raku 保护我们免受意外调用地点定义的痛击(例如内置函数)。除了 Bar.pm6 考虑以下内容:

our sub say ($ignored) { print "oh dear\n" }

这会创建一个词法别名,将内置 say 隐藏在 Bar.pm6 内部 但保持调用者 say 不变。因此,以下 say 调用仍然按预期工作:

use Bar;
say 'Carry on, carry on...';  # OUTPUT: «Carry on, carry on...»

18. 类和对象

Raku 有一个丰富的内置语法来定义和使用类。

默认构造函数允许为创建的对象设置属性:

class Point {
    has Int $.x;
    has Int $.y;
}

class Rectangle {
    has Point $.lower;
    has Point $.upper;

    method area() returns Int {
        ($!upper.x - $!lower.x) * ( $!upper.y - $!lower.y);
    }
}

# Create a new Rectangle from two Points
my $r = Rectangle.new(lower => Point.new(x => 0, y => 0), upper => Point.new(x => 10, y => 10));

say $r.area(); # OUTPUT: «100␤»

您也可以提供自己的构建和构建实现。下面更详细的例子展示了 Raku 中依赖处理器的外观。它展示了自定义构造函数,私有属性和公共属性,方法以及签名的各个方面。它代码不多,但结果是有趣和有用的。

class Task {
    has      &!callback;
    has Task @!dependencies;
    has Bool $.done;

    # Normally doesn't need to be written
    # BUILD is the equivalent of a constructor in other languages
    method new(&callback, *@dependencies) {
        return self.bless(:&callback, :@dependencies);
    }

    submethod BUILD(:&!callback, :@!dependencies) { }

    method add-dependency(Task $dependency) {
        push @!dependencies, $dependency;
    }

    method perform() {
        unless $!done {
            .perform() for @!dependencies;
            &!callback();
            $!done = True;
        }
    }
}

my $eat =
    Task.new({ say 'eating dinner. NOM!' },
        Task.new({ say 'making dinner' },
            Task.new({ say 'buying food' },
                Task.new({ say 'making some money' }),
                Task.new({ say 'going to the store' })
            ),
            Task.new({ say 'cleaning kitchen' })
        )
    );

$eat.perform();

18.1. 从类开始

和许多其他语言一样,Raku 使用 class 关键字来定义一个类。接下来的块可能包含任意代码,就像其他块一样,但类通常包含状态和行为声明。示例代码包括通过 has 关键字引入的属性(状态)以及通过 method 关键字引入的行为。

声明一个类会创建一个新的*类型对象*,默认情况下,它将被安装到当前包中(就像使用 our 作用域声明的变量一样)。此类型对象是类的“空实例”。例如,IntStr 等类型引用 Raku 内置类之一的类型对象。上面的示例使用类名称 Task,以便其他代码稍后可以引用它,例如通过调用 new 方法来创建类实例。

您可以使用 .DEFINITE 方法来确定你拥有的是实例还是类型对象:

say Int.DEFINITE; # OUTPUT: «False␤» (type object)
say 426.DEFINITE; # OUTPUT: «True␤»  (instance)

class Foo {};
say Foo.DEFINITE;     # OUTPUT: «False␤» (type object)
say Foo.new.DEFINITE; # OUTPUT: «True␤»  (instance)

你还可以使用类型表情符号来仅接受实例或类型对象:

multi foo (Int:U) { "It's a type object!" }
multi foo (Int:D) { "It's an instance!"   }
say foo Int; # OUTPUT: «It's a type object!␤»
say foo 42;  # OUTPUT: «It's an instance!␤»

18.2. 状态

类块中的前三行声明所有属性(在其他语言中称为字段或实例存储)。就像 my 变量不能从其声明的作用域之外访问一样,属性不能在类的外面访问。这种封装是面向对象设计的关键原则之一。

第一个声明指定回调的实例存储 - 为执行对象表示的任务而调用的一些代码:

has &!callback;

& sigil 表示该属性代表可调用的内容。 ! 字符是一个 twigil,或 secondary sigil。twigil 组成变量名称的一部分。在这种情况下,! twigil 强调,这个属性对类是私有的。

第二个声明也使用私有 twigil:

has Task @!dependencies;

然而,这个属性表示一个项目的数组,所以它需要 @ sigil。这些项目分别指定一个任务,在完成之前必须先完成这些任务。而且,这个属性的类型声明表明该数组只能包含 Task 类的实例(或者它的某个子类)。

第三个属性表示任务完成的状态:

has Bool $.done;

这个标量属性(带有 $ sigil)有一个 Bool 类型。而不是 ! twigil,使用 . twigil。尽管 Raku 确实对属性进行了封装,但它也可以避免编写访问器方法。替换!与。都声明属性 $!done 和一个名为 done 的访问器方法。就好像你写了:

has Bool $!done;
method done() { return $!done }

请注意,这不像某些语言允许的那样声明公共属性;你真的得到了一个私有属性和一个方法,而无需手动编写该方法。你可以自由地编写自己的访问器方法,如果你将来需要做一些比返回值更复杂的事情。

请注意,使用。 twigil 创建了一个方法,将提供对该属性的只读访问权限。如果该对象的用户应该能够重置任务的完成状态(也许再次执行),则可以更改属性声明:

has Bool $.done is rw;

rw 特征会导致生成的访问器方法返回一些外部代码可以修改的内容以更改该属性的值。

您还可以为属性提供默认值(对于有和没有访问者的情况,这些默认值同样适用):

has Bool $.done = False;

分配是在对象构建时进行的。此时评估右侧,甚至可以引用早期的属性:

has Task @!dependencies;
has $.ready = not @!dependencies;

18.3. 静态字段?

Raku 没有静态关键字。尽管如此,任何类都可以声明模块可以做的任何事情,所以使范围变量听起来像是个好主意。

class Singleton {
    my Singleton $instance;
    method new {!!!}
    submethod instance {
        $instance = Singleton.bless unless $instance;
        $instance;
    }
}

由我或我们定义的类属性也可以在声明时初始化,但是我们在这里实现 Singleton 模式,并且必须在第一次使用时创建对象。预测执行属性初始化的时刻不是 100%,因为它可以在编译,运行时或两者期间发生,尤其是在使用 use 关键字导入类时。

class HaveStaticAttr {
      my Foo $.foo = some_complicated_subroutine;
}

类属性也可以用辅助 sigil 声明 - 以类似于对象属性的方式 - 如果属性将被公开,将生成只读访问器。

18.4. 方法

虽然属性赋予对象状态,但方法赋予对象行为。我们暂时忽略新方法;这是一种特殊的方法。考虑第二种方法 add-dependency,它将一项新任务添加到任务的依赖列表中。

method add-dependency(Task $dependency) {
    push @!dependencies, $dependency;
}

在许多方面,这看起来很像一个子声明。但是,有两个重要的区别。首先,将此例程声明为方法将其添加到当前类的方法列表中,因此 Task 类的任何实例都可以使用它调用它。方法调用操作符。其次,一种方法将其调用者放入特殊变量 self 中。

该方法本身将传入的参数(它必须是 Task 类的一个实例)并将其推送到 invocant 的@!dependencies 属性上。

执行方法包含依赖性处理程序的主要逻辑:

method perform() {
    unless $!done {
        .perform() for @!dependencies;
        &!callback();
        $!done = True;
    }
}

它不需要参数,而是使用对象的属性。首先,通过检查$!done 属性来检查任务是否已经完成。如果是这样,那就没有什么可做的了。

否则,该方法执行所有任务的依赖关系,使用 for 构造遍历 @!dependencies 属性中的所有项。此迭代将每个项目(每个项目都放置一个 Task 对象)放入主题变量 $_ 中。使用 。方法调用操作符而不指定明确的调用者将当前主题用作调用者。因此,迭代构造对当前调用者的 @!dependencies 属性中的每个 Task 对象调用 .perform() 方法。

在所有的依赖关系完成之后,通过直接调用 &! 回调属性来执行当前任务的任务。这是括号的目的。最后,该方法将 $!done 属性设置为 True,以便后续对该对象执行的调用(例如,如果此 Task 是另一个 Task 的依赖项)将不会重复该任务。

18.5. 私有方法

就像属性一样,方法也可以是私有的。私有方法声明带有前缀感叹号。他们被称为 self!, 随后是方法的名称。要调用另一个类的私有方法,调用类必须被调用类信任。信任关系是用信任声明的,而且要信任的类必须已经声明。调用另一个类的私有方法需要该类的实例和该方法的全限定名称。信任也允许访问私有属性

class B {...}

class C {
    trusts B;
    has $!hidden = 'invisible';
    method !not-yours () { say 'hidden' }
    method yours-to-use () {
        say $!hidden;
        self!not-yours();
    }
}

class B {
    method i-am-trusted () {
        my C $c.=new;
        $c!C::not-yours();
    }
}

C.new.yours-to-use(); # the context of this call is GLOBAL, and not trusted by C
B.new.i-am-trusted();

信任关系不受继承。要信任全局名称空间,可以使用伪包 GLOBAL。

18.6. 构造函数

Raku 比构造函数领域的许多语言更自由。构造函数是任何返回类实例的东西。而且,构造函数是普通的方法。您从基类 Mu 继承了一个名为 new 的默认构造函数,但您可以自由覆盖 new,如下例所示:

method new(&callback, *@dependencies) {
    return self.bless(:&callback, :@dependencies);
}

Raku 中的构造函数和 C#Java 等语言中的构造函数最大的不同之处在于,它不是以某种方式为已经神奇创建的对象设置状态,而是由 Raku 构造函数自己创建对象。最简单的方法是调用也是从 Mu 继承的祝福方法。 bless 方法期望一组命名参数为每个属性提供初始值。

该示例的构造函数将位置参数转换为命名参数,以便该类可以为其用户提供一个很好的构造函数。第一个参数是回调(将执行任务的东西)。其余参数是相关的 Task 实例。构造函数将这些捕获到 @dependencies slurpy 数组中,并将它们作为命名参数传递给 bless(注意: &callback 使用变量的名称 - 减去 sigil - 作为参数的名称)。

私有属性确实是私有的。这意味着 bless 不允许直接将事物绑定到 &!callback@! 依赖关系。为了做到这一点,我们重写 BUILD 子方法,这是通过 bless 在全新对象上调用的:

submethod BUILD(:&!callback, :@!dependencies) { }

由于 BUILD 在新创建的 Task 对象的上下文中运行,因此可以操作这些私有属性。这里的技巧是使用私有属性( &!callback@! 依赖项)作为 BUILD 参数的绑定目标。零样板初始化!查看对象获取更多信息。

BUILD 方法负责初始化所有属性,还必须处理默认值:

has &!callback;
has @!dependencies;
has Bool ($.done, $.ready);
submethod BUILD(
        :&!callback,
        :@!dependencies,
        :$!done = False,
        :$!ready = not @!dependencies
    ) { }

请参阅对象构造以获取更多影响对象构造和属性初始化的选项。

18.7. 消费我们的类

创建一个类后,您可以创建该类的实例。声明一个自定义构造函数提供了一种简单的方式来声明任务及其依赖关系。要创建没有依赖关系的单个任务,请写下:

my $eat = Task.new({ say 'eating dinner. NOM!' });

前面的章节解释说,声明类 Task 在命名空间中安装了一个类型对象。这个类型对象是类的一个“空实例”,特别是没有任何状态的实例。您可以调用该实例的方法,只要它们不尝试访问任何状态;新是一个例子,因为它创建了一个新对象,而不是修改或访问现有对象。

不幸的是,晚餐从未奇迹般地发生。它有依赖任务:

my $eat =
    Task.new({ say 'eating dinner. NOM!' },
        Task.new({ say 'making dinner' },
            Task.new({ say 'buying food' },
                Task.new({ say 'making some money' }),
                Task.new({ say 'going to the store' })
            ),
            Task.new({ say 'cleaning kitchen' })
        )
    );

注意自定义构造函数和明智的空白使用如何清除任务依赖关系。

最后,perform 方法调用按顺序递归调用各种其他依赖项上的 perform 方法,并给出以下输出:

making some money
going to the store
buying food
cleaning kitchen
making dinner
eating dinner. NOM!

18.8. 继承

面向对象编程提供了继承的概念,作为代码重用的机制之一。 Raku 支持一个类从一个或多个类继承的能力。当一个类从另一个类继承时,它会通知方法调度器遵循继承链寻找一个派发方法。对于通过方法关键字定义的标准方法以及通过其他方式(如属性访问器)生成的方法,都会发生这种情况。

class Employee {
    has $.salary;
}

class Programmer is Employee {
    has @.known_languages is rw;
    has $.favorite_editor;

    method code_to_solve( $problem ) {
        return "Solving $problem using $.favorite_editor in "
        ~ $.known_languages[0];
    }
}

现在,Programmer 类型的任何对象都可以使用 Employee 类中定义的方法和访问器,就像它们来自 Programmer 类一样。

my $programmer = Programmer.new(
    salary => 100_000,
    known_languages => <Perl5 Raku Erlang C++>,
    favorite_editor => 'vim'
);

say $programmer.code_to_solve('halting problem'), " will get ", $programmer.salary(), "\$";
#OUTPUT: «Solving halting problem using vim in Perl5 will get 100000$␤»

18.8.1. 重写继承到的方法

当然,类可以通过定义它们自己来覆盖由父类定义的方法和属性。下面的例子演示了 Baker 类覆盖 Cook 的 cook 方法。

class Cook is Employee {
    has @.utensils  is rw;
    has @.cookbooks is rw;

    method cook( $food ) {
        say "Cooking $food";
    }

    method clean_utensils {
        say "Cleaning $_" for @.utensils;
    }
}

class Baker is Cook {
    method cook( $confection ) {
        say "Baking a tasty $confection";
    }
}

my $cook = Cook.new(
    utensils => <spoon ladle knife pan>,
    cookbooks => 'The Joy of Cooking',
    salary => 40000);

$cook.cook( 'pizza' );       # OUTPUT: «Cooking pizza␤»
say $cook.utensils.perl;     # OUTPUT: «["spoon", "ladle", "knife", "pan"]␤»
say $cook.cookbooks.perl;    # OUTPUT: «["The Joy of Cooking"]␤»
say $cook.salary;            # OUTPUT: «40000␤»

my $baker = Baker.new(
    utensils => 'self cleaning oven',
    cookbooks => "The Baker's Apprentice",
    salary => 50000);

$baker.cook('brioche');      # OUTPUT: «Baking a tasty brioche␤»
say $baker.utensils.perl;    # OUTPUT: «["self cleaning oven"]␤»
say $baker.cookbooks.perl;   # OUTPUT: «["The Baker's Apprentice"]␤»
say $baker.salary;           # OUTPUT: «50000␤»

因为调度员会在 Baker 上移到父级之前看到 Cook 的 cook 方法,所以调用 Baker 的 cook 方法。

要访问继承链中的方法,请使用重新分派或 MOP

18.9. 多重继承

如前所述,一个类可以从多个类继承。当一个类从多个类继承时,调度员知道在查找方法时要查看这两个类。 Raku 使用 C3 算法对多个继承层次进行线性化,这比深度优先搜索更好地处理多重继承。

class GeekCook is Programmer is Cook {
    method new( *%params ) {
        push( %params<cookbooks>, "Cooking for Geeks" );
        return self.bless(|%params);
    }
}

my $geek = GeekCook.new(
    books           => 'Learning Raku',
    utensils        => ('stainless steel pot', 'knife', 'calibrated oven'),
    favorite_editor => 'MacVim',
    known_languages => <Raku>
);

$geek.cook('pizza');
$geek.code_to_solve('P =? NP');

现在所有可用于 Programmer 和 Cook 类的方法都可以从 GeekCook 类中获得。

虽然多重继承是知道和偶尔使用的有用概念,但重要的是要了解有更多有用的 OOP 概念。当达到多重继承时,最好考虑是否通过使用角色来更好地实现设计,这通常更安全,因为它们强制类作者明确地解决冲突的方法名称。有关角色的更多信息,请参阅角色。

18.10. also 声明符

通过在特征前加上也可以在类声明主体中列出要继承的类。这也适用于角色组合特质。

class GeekCook {
    also is Programmer;
    also is Cook;
    # ...
}

role A {};
role B {};
class C { also does A; also does B }

18.11. 自省

自省是在程序中收集有关某些对象的信息的过程,而不是通过阅读源代码,而是通过查询对象(或控制对象)来获取某些属性,例如其类型。

给定一个对象 $o 和前面几节的类定义,我们可以问一些问题:

if $o ~~ Employee { say "It's an employee" };
if $o ~~ GeekCook { say "It's a geeky cook" };
say $o.WHAT;
say $o.perl;
say $o.^methods(:local)».name.join(', ');
say $o.^name;

输出可能如下所示:

It's an employee
(Programmer)
Programmer.new(known_languages => ["Perl", "Python", "Pascal"],
        favorite_editor => "gvim", salary => "too small")
code_to_solve, known_languages, favorite_editor
Programmer

前两个测试每个智能匹配类名称。如果对象是该类或继承类,则返回 true。因此,所讨论的对象是 Employee 类,或者是继承它的类,但不是 GeekCook

.WHAT 方法返回与对象 $o 关联的类型对象,它告诉我们 $o 的确切类型:在这种情况下是 Programmer

$o.perl 返回一个可以作为 Perl 代码执行的字符串,并且再现原始对象 $o。虽然这在所有情况下都不能很好地工作,但它对调试简单对象非常有用。 $o.^methods(:local) 产生一个可以在 $o 上调用的方法列表。 :local 命名参数将返回的方法限制为在 Programmer 类中定义的方法,并排除继承的方法。

使用 .^ 而不是单个点调用方法的语法意味着它实际上是对其元类的一个方法调用,该类是管理 Programmer 类的属性的类 - 或者您感兴趣的任何其他类。班级也启用了其他反省方式:

say $o.^attributes.join(', ');
say $o.^parents.map({ $_.^name }).join(', ');

最后,$o.^name 调用元对象的名称方法,这毫不意外地返回类名称。

自省对于调试和学习语言和新库非常有用。当一个函数或方法返回一个你不知道的对象时,用 .WHAT 查找它的类型,用 .perl 等等来查看它的构造方法,你会很清楚它的返回值是什么。使用 .^ 方法,您可以了解您可以对课程做些什么。

但也有其他应用程序:将对象序列化为一串字节的例程需要知道该对象的属性,可以通过内省查找该对象的属性。

18.12. 重写默认的 gist 方法

有些类可能需要它自己的版本,它会覆盖当被调用以提供类的默认表示时被打印的简洁方式。例如,异常可能只想写入有效负载而不是完整对象,以便更清楚发生了什么。但是,每个班级你都可以这样做:

class Cook {
    has @.utensils  is rw;
    has @.cookbooks is rw;

    method cook( $food ) {
        return "Cooking $food";
    }

    method clean_utensils {
        return "Cleaning $_" for @.utensils;
    }

    multi method gist(Cook:U:) { '⚗' ~ self.^name ~ '⚗' }
    multi method gist(Cook:D:) { '⚗ Cooks with ' ~ @.utensils.join( " ‣ ") ~ ' using ' ~ @.cookbooks.map( "«" ~ * ~ "»").join( " and ") }
}

my $cook = Cook.new(
    utensils => <spoon ladle knife pan>,
    cookbooks => ['Cooking for geeks','The French Chef Cookbook']);

say Cook.gist; # OUTPUT: «⚗Cook⚗»
say $cook.gist; # OUTPUT: «⚗ Cooks with spoon ‣ ladle ‣ knife ‣ pan using «Cooking for geeks» and «The French Chef Cookbook»␤»

通常你会想定义两个方法,一个用于类,另一个用于实例;在这种情况下,类方法使用 alambic 符号,下面定义的实例方法聚合了我们在厨师上的数据以叙述方式显示。

  1. 例如,封闭不容易以这种方式复制;如果你不知道封闭是什么,不要担心。此外,当前的实现方式在倾倒循环数据结构方面存在问题,但预期它们可以在某些时候由 .perl 正确处理。

19. 模块

19.1. 导出和选择性导出

19.1.1. is export

packages(包), subroutines(子例程), variables(变量), constants(常量) 和 enums(枚举) , 通过在它们的名字后面添加 is export 特性来导出。

unit module MyModule;
our $var is export = 3;
sub foo is export { ... };
constant $FOO is export = "foobar";
enum FooBar is export <one two three>;

# Packages like classes can be exported too
class MyClass is export {};

# If a subpackage is in the namespace of the current package
# it doesn't need to be explicitly exported
class MyModule::MyClass {};

就像所有的 traits 一样, 如果应用到子例程(routine)上, "is export" 应该出现在参数列表的后面:

sub foo (Str $string) is export {...}

你可以给 is export 传递命名参数以组织要导出的符号, 然后导入程序 (importer) 可以剔除和选择导入哪一个。有 3 个预先定义好的标签: ALL, DEFAULT, MANDATORY(强制的)。

# lib/MyModule.pm
unit module MyModule;
sub bag        is export              { ... }
sub pants      is export(:MANDATORY)  { ... }
sub sunglasses is export(:day)        { ... }
sub torch      is export(:night)      { ... }
sub underpants is export(:ALL)        { ... }
# main.pl
use lib 'lib';
use MyModule;           #bag, pants
use MyModule :DEFAULT;  #the same
use MyModule :day;      #pants, sunglasses
use MyModule :night;    #pants, torch
use MyModule :ALL;      #bag, pants, sunglasses, torch, underpants

19.1.2. UNIT::EXPORT::*

表象之下, 其实 is export 是把符号添加到 EXPORT 命名空间中的 UNIT 作用域包中。例如, is export(:FOO) 会把目标添加到 UNIT::EXPORT::FOO 包中。这正是 Raku 决定导入什么所做的。

unit module MyModule;

sub foo is export         { ... }
sub bar is export(:other) { ... }

等价于:

unit module MyModule;

my package EXPORT::DEFAULT {
  our sub foo { ... }
}

my package EXPORT::other {
  our sub bar { ... }
}

多数时候, is export 足够用了, 但是当你想动态生成要导出的符号时, EXPORT 包就很有用了。例如:

# lib/MyModule.pm
unit module MuModule;

my package EXPORT::DEFAULT {
  for <zero one two three four>.kv -> $number, $name {
      for <sqrt log> -> $func {
          OUR::{'&' ~ $func ~ '-of-' ~ $name } := sub { $number."$func()" };
      }
  }
}
# main.pl
use MyModule;
say sqrt-of-four; #-> 2
say log-of-zero;  #-> -Inf

19.1.3. EXPORT

你可以用一个 EXPORT 子例程导出任意符号。 EXPORT 必须返回一个 Map, 在 map 里面键是符号名, 键值是想要的值。符号名应该包含(如果有的话)关联类型。

class MyModule::Class { ... }

sub EXPORT {
  {
      '$var'      => 'one',
      '@array'    => <one two three>,
      '%hash'     => { one => 'two', three => 'four'},
      '&doit'     => sub { ... },
      'ShortName' => MyModule::class
  }
}
# main.pl
use lib 'lib';
use MyModule;
say $var;
say @array;
say %hash;
doit();
say ShortName.new;  #-> MyModule::Class.new

注意, EXPORT 不能声明在包内, 因为目前的 rakudo(2015.09) 好像把 EXPORT 当作 compunit 的一部分而非包的一部分。

虽然 UNIT::EXPORT 包处理传递给 use 的命名参数, 而 EXPORT sub 处理位置参数。如果你把位置参数传递给 use, 那么这些参数会被传递给 EXPORT. 如果传递了位置参数, 那么 module 就不再需要导出默认符号了。你仍然可以伴随着你的位置参数, 通过显式地给 use 传递 :DEFAULT 参数来导入它们。

# lib/MyModule

class MyModule::Class {}

sub EXPORT($short_name?) {
    {
      do $short_name => MyModule::Class if $short_name
    }
}

sub always is export(:MANDATORY) { say "works" }

#import with :ALL or :DEFAULT to get
sub shy is export { say "you found me!" }
# main.pl
use lib 'lib';
use MyModule 'foo';
say foo.new(); #MyModule::Class.new
always();      #OK   - is imported
shy();         #FAIL - won't be imported

19.2. 发布模块

如果你已经写了一个 Raku模块, 你想把它分享到社区, 我们会很高兴地把它放到 Raku 模块文件夹清单中。Raku modules directory

现在, 你需要使用 git 对你的模块进行版本控制。

这需要你有一个 Github 帐号, 以使你的模块能被从它的 Github 仓库中分享出去。

要分享你的模块, 按照下面说的做:

  • 创建一个以你的模块命名的工程文件夹。 例如, 如果你的模块是 Vortex::TotalPerspective , 那么就创建一个叫做 Vortex::TotalPerspective 的工程文件夹。这个工程目录的名字也会被用作 Github 仓库的名字。

  • 让你的工程目录看起来像这样:

Vortex-TotalPerspective/
|-- lib
|   `-- Vortex
|       `-- TotalPerspective.pm
|-- LICENSE
|-- META.info
|-- README.md
`-- t
    `-- basic.t

如果你的工程包含能帮助主模块完成工作的其它模块, 它们应该被放到你的 lib 目录中像这样组织:

lib
`-- Vortex
    |-- TotalPerspective.pm
    `-- TotalPerspective
        |-- FairyCake.pm
        `-- Gargravarr.pm
  • README.md 文件是一个 markdown 格式的文件, 它稍后会被 Github 自动渲染成 HTML

  • 关于 LICENSE 文件, 如果你没有其它选择, 就是用和 Rakudo Raku 一样的 LICENSE 把。仅仅把它的原始 license 复制/粘贴进你自己的 LICENSE 文件中。

  • 如果你还没有任何测试, 现在你可以忽略 t 目录 和 basic.t 文件。关于如何写测试, 你可以看看其它模块是怎么使用 Test 的。它和 Perl'5 的 Test::More 很类似。

  • 如果要文档化你的模块, 在你的模块中使用 Raku Pod 标记。欢迎给模块写文档, 并且为了浏览的方便, 一旦 Raku module directory(或其它网站) 开始把 Pod 文档渲染成 HTML, 写文档尤为重要。

  • 让你的 META.info 文件看起来像这样:

{
        "name"        : "Vortex::TotalPerspective",
        "version"     : "0.1.0",
        "description" : "Wonderful simulation to get some perspective.",
        "author"      : "Your Name",
        "provides"    : {
            "Vortex::TotalPerspective" : "lib/Vortex/TotalPerspective.pm"
        },
        "depends"     : [ ],
        "source-url"  : "git://github.com/you/Vortex-TotalPerspective.git"
    }

关于选择版本号的方案, 或许使用 "major.minor.patch" (查看 the spec on versioning 获取详细信息 )。如果版本号现在对你或你的用户来说不重要, 你可以给版本那儿放上一颗星(*)。

provides 一节, 包含进你的发布中提供的所有命名空间。

  • 把你的工程放在 git 版本控制之下, 如果你还未这样做。

  • 一旦你对你的工程满意了, 在 Github 上为它创建一个仓库。必要的话, 查看 Github’s help docs。 你的 Github 仓库的名字应该和你工程目录的名字一样。创建完 Githhub 仓库后, Github 会为你展示怎么配置你的本地仓库以获悉你的 Github 仓库。

  • 把你的工程推送到 Github。

  • 在 IRC 频道找个人帮你展示怎么把你的模块添加到ecosystem, 或者让他们是否能替你添加。

  • pull 请求被接收之后, 等个把小时。如果你的模块没有出现在 http://modules.raku.org/ , 请到 http://modules.raku.org/log/update.log 翻看log 日志文件, 以查找是否有错误。

就是这样啦! 感谢为 Raku 社区做贡献!

如果你想尝试安装你的模块, 使用熊猫 panda 安装工具, 这已经包含在 Rakudo Raku 中了:

zef install Vortex::TotalPerspective

这会下载你的模块到它自己的工作目录(~/.panda), 在那儿创建 build, 并把模块安装到 ~/.raku == CompUnits 以及在哪里找到它们

19.3. 概览

作为 Perl 语言家族的一员,Raku 中的程序在顶层更倾向于被解释的程序。在本教程中,"被解释的程序"是指源代码,即人类可读的文本,如 "hello world",立即被 Raku 程序处理成可以被计算机执行的代码,中间的任何阶段都存储在内存中。

相比之下,编译后的程序,是将人类可读的源码首先处理成机器可执行的代码,并将这些代码的某些形式存储在光盘上。为了执行程序,机器可读版本被加载到内存中,然后由计算机运行。

编译和解释形式都有其优点。简而言之,解释型程序可以快速地被"编写",并且可以快速地改变源码。编译后的程序可能很复杂,需要大量的时间来预处理成机器可读的代码,但运行这些程序对用户来说要快得多,因为用户只看到加载和运行时间,而不是编译时间。

Raku 有这两种范式。在最上层,Raku 程序是被解释的,但如果被分离出来的代码会被编译成模块,然后在必要时加载预处理的版本。在实践中,由社区编写的 Module 只需要在用户"安装"时进行一次预编译,例如通过一个像 zef 这样的模块管理器。然后它们就可以被开发者在自己的程序中使用。这样做的效果是让 Raku 顶级程序快速运行。

Perl 系列语言的最大优势之一就是能够将整个生态系统中的模块由有能力的程序员编写的模块集成到一个小程序中。这一优势被广泛地复制,现在已经成为所有语言的标准。Raku 的整合能力更进一步,使得 Raku 程序可以相对容易地将其他语言编写的系统库整合到 Raku 程序中,参见 Native Call

从 Perl 和其他语言的经验来看,模块的分布式特性会产生一些实际困难。

  • 随着 API 的改进,一个流行的模块可能会经历多次迭代,而不能保证向后兼容。所以,如果一个程序依赖于某些特定的函数或返回,那么必须有一种方法来指定版本。

  • 一个模块可能是由 Bob 写的,他是一个很有能力的程序员,但他在生活中转行了,这个模块无人维护,于是 Alice 接手了。这意味着,同一个模块,用同样的名字,同样的通用 API,在野外可能有两个版本。或者,两个开发者(例如,Alice 和 Bob)最初在一个模块上合作,然后就分道扬镳了。因此,有时需要有一种方法来定义模块的Auth认证。

  • 一个模块可能会随着时间的推移而增强,维护者可能会保持两个版本的更新,但是有不同的 API。所以可能有必要定义所需的 API。

  • 当开发一个新程序时,开发者可能希望 Alice 和 Bob 编写的模块都安装在本地。因此,不可能只安装一个单一名称的模块的一个版本。

Raku 实现了所有这些可能性,允许多个版本、多个权限和多个 API 在本地存在、安装和可用。类和模块可以用特定属性访问的方式在其他地方解释。本教程是关于 Raku 如何处理这些可能性。

19.4. 介绍

在考虑 Raku 框架之前,我们先来看看 Perl 或 Python 等语言是如何处理模块安装和加载的。

ACME::Foo::Bar -> ACME/Foo/Bar.pm
os.path -> os/path.py

在这些语言中,模块名与文件系统路径有 1:1 的关系。我们只需用斜线代替双冒号或句号,然后加上一个 .pm.py

注意,这些都是相对路径。Python 和 Perl 都使用了一个 include 路径列表,来完成这些路径。在 Perl 中,它们可以在全局的 @INC 数组中使用。

@INC

/usr/lib/perl5/site_perl/5.22.1/x86_64-linux-thread-multi
/usr/lib/perl5/site_perl/5.22.1/
/usr/lib/perl5/vendor_perl/5.22.1/x86_64-linux-thread-multi
/usr/lib/perl5/vendor_perl/5.22.1/
/usr/lib/perl5/5.22.1/x86_64-linux-thread-multi
/usr/lib/perl5/5.22.1/

每个 include 目录都要检查它是否包含一个由模块名确定的相对路径。如果符合,则加载该文件。

当然这是一个有点简化的版本。这两种语言都支持缓存模块的编译版本。所以 Perl 首先查找的是 .pmc 文件,而不是只查找 .pm 文件。而 Python 首先寻找 .pyc 文件。

在这两种情况下,模块的安装主要意味着将文件复制到由相同的简单映射决定的位置。这个系统解释起来很简单,容易理解,简单而稳健。

为什么改变呢?

为什么 Raku 需要另一个框架?原因是那些语言所缺乏的功能,即。

  • Unicode 模块名称

  • 不同作者以相同名称出版的模块

  • 安装了多个版本的模块

26 个拉丁文字符的集合对于几乎所有真正的现代语言来说,包括英语在内,这套26个拉丁文字符的限制性太强了,很多常用的单词都有变音符号。

模块名和文件系统路径之间的关系是 1:1,一旦你试图在多个平台和文件系统上支持 Unicode,你就会进入一个痛苦的世界。

然后是在多个作者之间共享模块名。这个问题在实践中可能会很好地解决,也可能不会很好地解决。我可以想象用它来发布一个带有一些修正的模块,直到原作者在"官方"版本中加入修正。

最后是多版本。通常情况下,需要特定版本的模块的人都会去找 local::lib 或者容器或者一些自家的变通方法。它们都有自己的缺点。如果应用程序可以直接说,嘿,我需要一个好的、可靠的 2.9 版本,或者说,也许是那个分支的 bug 修复版本,那么这些都没有必要。

如果你有任何希望继续使用简单的名称映射解决方案,你可能会在版本要求时就放弃了。因为,当你在寻找 2.9 或更高版本的模块时,你如何找到 3.2 版本的模块?

流行的想法包括在 JSON 文件中收集已安装模块的信息,但当那些被证明是趾高气扬的时候,文本文件就被放在 SQLite 数据库中的元数据取代了。然而,这些想法通过引入另一个要求很容易被击倒:发行版包。

Linux 发行版的软件包大多只是包含一些文件加上一些元数据的存档。理想情况下,安装这样的软件包的过程意味着只需解压这些文件并更新中央软件包数据库。卸载意味着删除安装的文件,然后再次更新包数据库。在安装和卸载时改变现有的文件,会给打包者的生活带来很大的困难,所以我们真的要避免这种情况。另外,安装的文件的名称可能与之前安装的文件不一样。我们必须在打包的时候就知道这些文件的名称是什么。

长名称

Foo::Bar:auth<cpan:nine>:ver<0.3>:api(1)

让我们走出困境的第 0 步是定义一个长的名称。在 Raku 中,一个完整的模块名称包括短名、AUTH、版本和 API。

同时,你安装的东西通常不是单个模块,而是一个可能包含一个或多个模块的发行版。发行版名称的工作方式和模块名称一样。事实上,发行版通常只是以其主模块的名字命名。发行版的一个重要属性是它们是不可更改的。Foo:auth<cpan:nine>:ver<0.3>:api<1> 将永远是完全相同的代码名称。

$*REPO

在 Perl 和 Python 中,你会处理指向文件系统目录的 include 路径。在 Raku 中,我们把这些目录称为"存储库",每个存储库都是由一个对象来管理,这个对象的作用是 CompUnit::Repository。与其说是 B<@INC> 数组,不如说是 $*REPO 变量。它包含了一个单一的存储库对象。这个对象有一个 next-repo 属性,可能包含另一个存储库。换句话说:版本库是作为一个链接列表来管理的。与传统数组的重要区别在于,当通过列表时,每个对象都有发言权,决定是否将请求传递给下一个repo。Raku 设置了一个标准的 repositories,即 "perl"、"vendor" 和 "site" repositories,就像你从 Perl 中知道的那样。此外,我们还为当前用户设置了一个 "home" 存储库。

存储库必须实现需求方法。Raku 代码中的 userequire 语句基本上被转化为调用 B<$*REPO>need 方法。这个方法可以反过来将请求委托给下一个 repo。

role CompUnit::Repository {
    has CompUnit::Repository $.next is rw;

    method need(CompUnit::DependencySpecification $spec,
                CompUnit::PrecompilationRepository $precomp,
                CompUnit::Store :@precomp-stores
                --> CompUnit:D
                )
        { ... }
    method loaded(
                --> Iterable
                )
        { ... }

    method id( --> Str )
        { ... }
}

仓库

Rakudo 自带了几个可以用于仓库的类。最重要的是 CompUnit:::Repository:::FileSystemCompUnit::Repository:::Installation。FileSystem repo 是为了在模块开发过程中使用,实际上在寻找模块时,它的工作原理就像 Perl 一样。它不支持版本或 auths,只是将短名映射到文件系统路径。

安装库才是真正聪明的地方。当你请求一个模块时,你通常会通过它的确切的长名,或者说"给我一个符合这个过滤器的模块"。这样的过滤器是通过 CompUnit:::DependencySpecification 对象来提供的,它的字段为

  • 简称:

  • auth-matcher。

  • 版本匹配器和

  • api-matcher。

在查找候选模块时,安装库会根据这个 DependencySpecification 或者说是单个字段与单个匹配器进行智能匹配。因此,匹配器可以是一些具体的值、版本范围,甚至是一个 regex(虽然一个任意的 regex,如 .*,不会产生有用的结果,但像 3.20.1+ 这样的东西只会找到高于 3.20.1 的候选模块)。

加载所有已安装的发行版的元数据将是非常缓慢的。目前 Raku 框架的实现使用文件系统作为一种数据库。然而,另一个实现可能使用另一种策略。下面的描述展示了一个实现是如何工作的,这里包含在这里来说明所发生的事情。

我们不仅存储一个发行版的文件,而且还创建了索引来加快查找速度。这些索引之一是以目录的形式出现,以安装模块的简称命名。但是现在大多数常用的文件系统都不能处理 Unicode名 称,所以我们不能直接使用模块名。这就是现在臭名昭著的 SHA-1 哈希值进入游戏的地方。目录名是 UTF-8 编码的模块短名的 ASCII 编码的 SHA-1 散列。

在这些目录中,我们发现每个发行版都会有一个文件,其中包含一个模块的短名与之匹配。这些文件再次包含了 dist 的 ID 和其他构成长名的字段: auth、版本 和 api。因此,通过读取这些文件,我们通常会得到一个简短的 auth-version-api 三重奏的列表,我们可以将其与 DependencySpecification 匹配。我们最终得到了获胜的发行版的 ID,我们用它来查询元数据,这个元数据存储在一个 JSON 编码的文件中。这个元数据包含了 source/目录中的文件名,其中包含了所请求模块的代码。这就是我们可以加载的内容。

寻找源文件的名字又是一个比较棘手的问题,因为还有一个 Unicode 的问题,此外,相同的相对文件名可能会被不同的发行版所使用(想想看版本)。所以至少现在,我们使用 SHA-1 长名的哈希值。

资源

%?RESOURCES
%?RESOURCES<libraries/p5helper>
%?RESOURCES<icons/foo.png>
%?RESOURCES<schema.sql>

Foo
|___ lib
|     |____ Foo.rakumod
|
|___ resources
      |___ schema.sql
      |
      |___ libraries
            |____ p5helper
            |        |___
            |___ icons
                     |___ foo.png

不仅仅是源文件被存储和发现的方式是这样的。发行版还可能包含任意的资源文件。这些资源文件可能是图像、语言文件或安装时编译的共享库。它们可以通过 %?RESOURCES 散列从模块内部访问。

只要你坚持使用标准的发行版的布局惯例,这甚至可以在开发过程中不安装任何东西就能正常工作。

这种架构的一个很好的结果是,创建特殊用途的存储库相当容易。

依赖

幸运的是,至少在大多数情况下,预编译的效果相当好。然而,它也有自己的挑战。加载单个模块是很容易的。当一个模块有依赖关系,而这些依赖关系又有自己的依赖关系时,乐趣就开始了。

在 Raku 中加载一个预编译的文件时,我们还需要加载所有依赖关系的预编译文件。而这些依赖关系必须是预编译的,我们不能从源文件中加载它们。更糟糕的是,这些依赖项的预编译文件必须与我们在预编译模块时使用的文件完全相同。

更糟糕的是,预编译的文件只能与编译时使用的 Raku 二进制文件一起工作。

如果不是因为一个额外的要求,所有这些都还算好办:作为用户,你希望一个刚安装的模块的新版本能够被实际使用,不是吗?

换句话说:如果你升级了一个预编译的模块的依赖关系,我们必须检测到这一点,然后用新的依赖关系重新预编译该模块。

预编译存储

现在请记住,虽然我们有一个标准的版本库链,但用户可以通过命令行中的 -I 或代码中的 "use lib" 来预置额外的版本库。

这些存储库可能包含了预编译模块的依赖关系。

我们对这个谜题的第一个解决方案是,每个仓库都有自己的precomp存储库,预编译的文件就存放在这里。我们只从链中第一个版本库的precomp存储库中加载precomp文件,因为这是唯一一个可以直接或至少间接访问所有候选模块的版本库。

如果这个存储库是一个FileSystem存储库,我们会在.precomp目录下创建一个precomp存储库。

虽然是安全的选择,但这样做的后果是,每当你使用一个新的存储库时,我们将在开始时无法访问预编译的文件。

相反,我们将在第一次加载模块时对其进行预编译。

归功于

本教程是根据 niner演讲内容编写的。

20. 模块开发工具

以下是您可以在 Raku 生态系统中找到的模块列表,旨在使开发 Raku 模块的体验更加有趣。

20.1. 模块构建器和创作工具

一些模块和工具可帮助您生成属于模块分发的文件。

20.2. 测试

一些模块质量测试。

  • Test::META 测试您的 META6.json 文件

  • Test::Output 测试程序生成的 STDOUT 和 STDERR 的输出

  • Test::Screen 使用GNU screen测试全屏VT应用程序

  • Test::When 控制测试运行时间(作者测试,在线测试等)

20.3. NativeCall

这里有一些模块可以帮助您使用 NativeCall。

20.4. Sample modules

仅作为极简主义示例,安装程序测试或骨架的模块。

  • Foo 具有两个不同版本分布的模块

21. 核心模块

Rakudo 实现包含一些您可能想要使用的模块。以下是它们的列表,以及它们的源代码的链接。

21.1. CompUnit::* 模块和角色

这些模块主要由分发构建工具使用,并不打算由最终用户使用(至少在版本6.c之前)。

21.2. NativeCall 模块

21.3. 其它模块

22. 进程间通信

22.1. 运行程序

许多程序需要能够运行其他程序,我们需要将信息传递给它们并接收它们的输出和退出状态。在 Raku 中运行程序非常简单:

run 'git', 'status';

这一行运行名为 “git” 的程序,并将 “git” 和 “status” 传递给它的命令行。它将使用 %*ENV<PATH> 设置找到该 git 程序。

如果您想通过向 shell 发送命令行来运行程序,那么也有一个工具。所有 shell 元字符都由 shell 解释,包括管道,重定向,环境变量替换等。

shell 'ls -lR | gzip -9 > ls-lR.gz';

使用 shell 用户输入时应小心。

22.2. Proc 对象

runshell 都返回一个PROC对象,它可以使用具有更详细的进程进行通信。请注意,除非您关闭所有输出管道,否则程序通常不会终止。

my $git = run 'git', 'log', '--oneline', :out;
for $git.out.lines -> $line {
    my ($sha, $subject) = $line.split: ' ', 2;
    say "$subject [$sha]";
}
$git.out.close();

如果程序失败(以非零退出码退出),它将在返回的Proc对象沉没时抛出异常。您可以将其保存为变量,甚至是匿名变量,以防止下沉:

$ = run '/bin/false'; # does not sink the Proc and so does not throw

您可以通过传递 :out:err 标志来告诉 Proc 对象将输出捕获为文件句柄。您也可以通过 :in 标记传递输入。

my $echo = run 'echo', 'Hello, world', :out;
my $cat  = run 'cat', '-n', :in($echo.out), :out;
say $cat.out.get;
$cat.out.close();

您还可以使用 Proc 捕获PID,将信号发送到应用程序,并检查 exitcode。

my $crontab = run 'crontab', '-l';
if $crontab.exitcode == 0 {
    say 'crontab -l ran ok';
}
else {
    say 'something went wrong';
}

22.3. Proc::Async 对象

当您需要更多地控制与另一个进程的通信时,您将需要使用Proc::Async。该类提供对与程序进行异步通信的支持,以及向该程序发送信号的能力。

# Get ready to run the program
my $log = Proc::Async.new('tail', '-f',  '/var/log/system.log');
$log.stdout.tap(-> $buf { print $buf });
$log.stderr.tap(-> $buf { $*ERR.print($buf) });

# Start the program
my $done = $log.start;
sleep 10;

# Tell the program to stop
$log.kill('QUIT');

# Wait for the program to finish
await $done;

上面的小程序使用“tail”程序每 10 秒打印出名 system.log 的日志内容,然后通过 QUIT 信号告诉程序停止。

虽然 Proc 使用 IO::Handle 提供对输出的访问,但 Proc::Async 使用异步 supplies 提供访问(请参阅Supply)。

如果要在等待原始程序完成时运行程序并执行某些工作,则 start 例程将返回Promise,该程序在程序退出时保留(kept)。

使用 write 方法将数据传递到程序中。

23. 正则表达式最佳实践和陷阱

为了提供强大的正则表达式和 grammar,这里有一些代码布局和可读性的最佳实践,实际匹配的内容,以及避免常见的陷阱。

23.1. 代码布局

如果没有 :sigspace 副词,在 Raku 正则表达式中空格并不重要。 使用它自己的优势,并插入空格,增加可读性。 此外,必要时插入注释。

比较非常紧凑的写法

my regex float { <[+-]>?\d*'.'\d+[e<[+-]>?\d+]? }

和这种可读性更好的写法:

my regex float {
     <[+-]>?        # optional sign
     \d*            # leading digits, optional
     '.'
     \d+
     [              # optional exponent
        e <[+-]>?  \d+
     ]?
}

根据经验, 在原子周围和组的内部使用空白; 将量词直接放在原子之后; 并垂直对齐开口和闭合关方括号和括号。

在括号或方括号内使用替换列表时,请对齐竖线:

my regex example {
    <preamble>
    [
    || <choice_1>
    || <choice_2>
    || <choice_3>
    ]+
    <postamble>
}

23.2. 保持短小

正则代码通常比常规代码更紧凑。 因为他们用这么少的字符就做得那么多,所以保持了正则表达式的简短。

当你可以给正则表达式的一部分命名时,通常最好将它放入一个单独的,命名的正则表达式中。

例如,您可以以前面获取浮点正则表达式为例:

my regex float {
     <[+-]>?        # optional sign
     \d*            # leading digits, optional
     '.'
     \d+
     [              # optional exponent
        e <[+-]>?  \d+
     ]?
}

并将其分解为部件:

my token sign { <[+-]> }
my token decimal { \d+ }
my token exponent { 'e' <sign>? <decimal> }
my regex float {
    <sign>?
    <decimal>?
    '.'
    <decimal>
    <exponent>?
}

这有助于,特别是当正则表达式变得更加复杂时。 例如,您可能希望在存在指数的情况下使小数点可选。

my regex float {
    <sign>?
    [
    || <decimal>?  '.' <decimal> <exponent>?
    || <decimal> <exponent>
    ]
}

23.3. 要匹配什么

输入数据格式通常没有明确的规范,或者程序员不知道规范。 然后,按照你的期望自由是好的,但只要没有可能的含糊之处。

例如,在 ini 文件中:

[section]
key=value

section 标题内可以有什么内容? 只允许一个词可能限制性太强。 有人可能会写`[two words]`,或使用破折号等。而不是问内部允许什么,可能值得问一下:什么是不允许的?

显然,不允许闭合方括号,因为 [a]b] 是不明确的。 根据同一论点,应禁止开口方括号。 这让我们失望了

token header { '[' <-[ \[\] ]>+ ']' }

如果你只处理一行就没问题。 但是,如果您正在处理整个文件,那么正则表达式会解析

[with a
newline in between]

这可能不是一个好主意。妥协是

token header { '[' <-[ \[\] \n ]>+ ']' }

然后,在后处理中,从 section 标题中删除前导和尾随空格和制表符。

23.4. 匹配空白

:sigspace 副词(或使用 rule 声明符而不是 tokenregex)非常便于隐式解析可能出现在许多地方的空格。

回到解析 ini 文件的例子,我们有

my regex kvpair { \s* <key=identifier> '=' <value=identifier> \n+ }

这可能不像我们想要的那样的文字,因为用户可能在等号周围放置空格。 那么,我们可以试试这个:

my regex kvpair { \s* <key=identifier> \s* '=' \s* <value=identifier> \n+ }

但那看起来很笨重,所以我们尝试别的东西:

my rule kvpair { <key=identifier> '=' <value=identifier> \n+ }

可是等等! 值之后的隐式空格匹配会占用所有空格,包括换行符,因此 \n+ 没有任何东西可以匹配(并且 rule 也禁用了回溯,因此没有运气)。

因此,将隐式空格的定义重新定义为输入格式中不重要的空白非常重要。

这通过重新定义 token ws 来工作; 但是,它只适用于 grammars

grammar IniFormat {
    token ws { <!ww> \h* }
    rule header { \s* '[' (\w+) ']' \n+ }
    token identifier  { \w+ }
    rule kvpair { \s* <key=identifier> '=' <value=identifier> \n+ }
    token section {
        <header>
        <kvpair>*
    }

    token TOP {
        <section>*
    }
}

my $contents = q:to/EOI/;
    [passwords]
        jack = password1
        joy = muchmoresecure123
    [quotas]
        jack = 123
        joy = 42
EOI
say so IniFormat.parse($contents);

除了将所有正则表达式都放入 grammar 并将其转换为 tokens(因为它们无论如何都不需要回溯),有趣的一点是

token ws { <!ww> \h* }

在进行隐式空格分析的时候会调用该 token。 当它不在两个单词字符之间( <!ww>,"在单词中"的否定断言)和零个或多个水平空格字符之间匹配。 对水平空格的限制很重要,因为换行符(垂直空格)会分隔记录,不应被隐式匹配。

不过,潜伏着一些与空白相关的麻烦。 正则表达式 \n+\n \n 之类的字符串不匹配,因为两个换行符之间有空白。 要允许此类输入字符串,请将 \n+ 替换为 \n\s*

24. 命令行接口 - 概述

Raku 脚本的默认命令行界面由三部分组成:

24.1. 将命令行参数解析为捕获

这将查看 @*ARGS 中的值,根据某些策略解释这些值,并创建一个 Capture 对象。解析器的替代方式可以由开发者提供或使用模块安装。

24.2. 使用该捕获调用提供的MAIN子例程

标准多分重分派用于使用生成的 Capture 对象调用 MAIN 子例程。这意味着您的 MAIN子 例程可能是一个 multi sub,其中每个候选程序负责处理给定命令行参数的某些部分。

24.3. 如果调用 MAIN 失败,则创建/显示使用信息

如果多重分派失败,则应尽可能通知脚本的用户失败的原因。默认情况下,这是通过检查每个 MAIN 候选 sub 的签名以及任何关联的 pod 信息来完成的。然后在 STDERR 上向用户显示结果(如果指定了 --help,则在 STDOUT 上显示)。生成使用信息的替代方式可以由开发者提供或使用模块安装。

24.4. sub MAIN

在运行所有相关的输入phasers(BEGINCHECKINITPREENTER)并执行脚本的主线之后,将执行具有特殊名称 MAIN 的子程序。如果没有 MAIN sub,则不会发生错误:您的脚本只需要在脚本的主线中执行工作,例如参数解析。

从 MAIN sub 的任何正常退出将导致退出代码为 0,表示成功。 MAIN 子的任何返回值都将被忽略。如果抛出未在 MAIN 子内部处理的异常,则退出代码将为 1。如果调度到 MAIN 失败,则在 STDERR 上将显示一条用法消息,退出代码将为 2。

命令行参数存在于 @*ARGS 动态变量中,并且可以在调用 MAIN 单元之前在脚本的主线中进行更改。

(多个子 MAIN 的候选者)的签名确定使用标准多重分派语义实际调用哪个候选者。

一个简单的例子:

# inside file 'hello.p6'
sub MAIN($name) {
    say "Hello $name, how are you?"
}

如果您调用该脚本没有任何参数:

$ raku hello.p6
Usage:
  hello.p6 <name>

但是,如果为参数指定默认值,则无论是否指定名称,运行脚本始终有效:

# inside file 'hello.p6'
sub MAIN($name = 'bashful') {
    say "Hello $name, how are you?"
}
$ raku hello.p6
Hello bashful, how are you?
$ raku hello.p6 Liz
Hello Liz, how are you?

另一种方法是使 sub MAIN 成为一个 multi sub

# inside file 'hello.p6'
multi sub MAIN()      { say "Hello bashful, how are you?" }
multi sub MAIN($name) { say "Hello $name, how are you?"   }

这将提供与上述示例相同的输出。您是否应该使用任何一种方法来实现预期目标完全取决于您。

使用单个位置和多个命名参数的更复杂的示例:

# inside "frobnicate.p6"
sub MAIN(
  Str   $file where *.IO.f = 'file.dat',
  Int  :$length = 24,
  Bool :$verbose
) {
    say $length if $length.defined;
    say $file   if $file.defined;
    say 'Verbosity ', ($verbose ?? 'on' !! 'off');
}

有了 file.dat,这将以这种方式工作:

$ raku frobnicate.p6
24
file.dat
Verbosity off

或者这样 --verbose

$ raku frobnicate.p6 --verbose
24
file.dat
Verbosity on

如果文件 file.dat 不存在,或者您指定了另一个不存在的文件名,您将获得从 MAIN 子的内省创建的标准用法消息:

$ raku frobnicate.p6 doesntexist.dat
Usage:
  frobnicate.p6 [--length=<Int>] [--verbose] [<file>]

虽然您不必在代码中执行任何操作,但它仍然可能被视为有点简洁。但是通过使用 pod 功能提供提示,有一种简单的方法可以更好地使用该消息:

# inside "frobnicate.p6"
sub MAIN(
  Str   $file where *.IO.f = 'file.dat',  #= an existing file to frobnicate
  Int  :$length = 24,                     #= length needed for frobnication
  Bool :$verbose,                         #= required verbosity
) {
    say $length if $length.defined;
    say $file   if $file.defined;
    say 'Verbosity ', ($verbose ?? 'on' !! 'off');
}

哪个会改善这样的用法消息:

$ raku frobnicate.p6 doesntexist.dat
Usage:
  frobnicate.p6 [--length=<Int>] [--verbose] [<file>]

    [<file>]          an existing file to frobnicate
    --length=<Int>    length needed for frobnication
    --verbose         required verbosity

24.5. %*SUB-MAIN-OPTS

通过设置 %*SUB-MAIN-OPTS 哈希中的选项,可以在将参数传递给 sub MAIN {} 之前更改参数的处理方式。由于动态变量的性质,需要设置 %*SUB-MAIN-OPTS 哈希并使用适当的设置填充它。例如:

my %*SUB-MAIN-OPTS =
  :named-anywhere,    # allow named variables at any location
  # other possible future options / custom options
;
sub MAIN ($a, $b, :$c, :$d) {
    say "Accepted!"
}

可用选项包括:

24.6. named-anywhere

默认情况下,传递给程序的命名参数(即 MAIN)在任何位置参数后都不会出现。但是,如果将 %*SUB-MAIN-OPTS<named-anywhere> 设置为 true 值,则可以在任何位置指定命名参数,即使在位置参数之后也是如此。例如,可以使用以下命令调用上述程序:

$ raku example.p6 1 --c=2 3 --d=4

24.7. is hidden-from-USAGE

有时您希望排除MAIN候选者显示在任何自动生成的使用消息中。这可以通过向您不想显示的 MAIN 候选者的规范添加 hidden-from-USAGE 特征来实现。扩展前面的例子:

# inside file 'hello.p6'
multi sub MAIN() is hidden-from-USAGE {
    say "Hello bashful, how are you?"
}
multi sub MAIN($name) {  #= the name by which you would like to be called
    say "Hello $name, how are you?"
}

因此,如果您只使用命名变量调用此脚本,您将获得以下用法:

$ raku hello.p6 --verbose
Usage:
  hello.p6 <name> -- the name by which you would like to be called

没有第一个候选者 hidden-from-USAGE 特征,它看起来像这样:

$ raku hello.p6 --verbose
Usage:
  hello.p6
  hello.p6 <name> -- the name by which you would like to be called

虽然技术上是正确的,但也不能读。

24.8. MAIN 的单位作用域定义

如果整个程序体驻留在 MAIN 中,则可以使用单位声明符,如下所示(调整前面的示例):

unit sub MAIN(
  Str   $file where *.IO.f = 'file.dat',
  Int  :$length = 24,
  Bool :$verbose,
);  # <- note semicolon here

say $length if $length.defined;
say $file   if $file.defined;
say 'Verbosity ', ($verbose ?? 'on' !! 'off');
# rest of script is part of MAIN

请注意,这只适用于只有一个(仅)sub MAIN 的情况。

24.9. sub USAGE

如果找不到给定命令行参数的 MAIN 的多候选者,则调用 sub USAGE。如果未找到此类方法,编译器将输出默认用法消息。

#|(is it the answer)
multi MAIN(Int $i) { say $i == 42 ?? 'answer' !! 'dunno' }
#|(divide two numbers)
multi MAIN($a, $b){ say $a/$b }

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

    Prints the answer or 'dunno'.
EOH
}

通过只读 $*USAGE 变量,sub USAGE 内的默认用法消息可用。它将基于可用的 sub MAIN 候选者及其参数生成。如前所示,您可以使用 #|(…​) Pod 块为每个候选项指定其他扩展描述以设置 WHY

24.10. 拦截 CLI 参数解析(2018.10, v6.d and later)

您可以通过自己提供 ARGS-TO-CAPTURE 子例程,或者从生态系统中可用的任何 Getopt 模块中导入一个子例程来替换或扩充参数解析的默认方式。

24.11. sub ARGS-TO-CAPTURE

ARGS-TO-CAPTURE 子程序应该接受两个参数:一个 Callable 表示要执行的 MAIN 单元(因此可以在必要时进行内省)和一个带有来自命令行的参数的数组。它应该返回一个将用于调度 MAIN 单元的 Capture 对象。一个非常人为的例子,它将根据输入的某个关键字创建一个 Capture(在测试脚本的命令行界面时可以很方便):

sub ARGS-TO-CAPTURE(&main, @args --> Capture) {
    # if we only specified "frobnicate" as an argument
    @args == 1 && @args[0] eq 'frobnicate'
      # then dispatch as MAIN("foo","bar",verbose => 2)
      ?? Capture.new( list => <foo bar>, hash => { verbose => 2 } )
      # otherwise, use default processing of args
      !! &*ARGS-TO-CAPTURE(&main, @args)
}

请注意,动态变量 &*ARGS-TO-CAPTURE 可用于执行捕获处理的默认命令行参数,因此如果您不想,则不必重新发明整个轮子。

24.12. 拦截使用消息生成(2018.10,v6.d及更高版本)

您可以通过自己提供 GENERATE-USAGE 子例程,或者从生态系统中可用的任何 Getopt 模块导入一个子例程来替换或扩充默认的使用方式消息生成方式(在向 MAIN 发送失败之后)。

24.12.1. sub RUN-MAIN

定义为:

sub RUN-MAIN(&main, $mainline, :$in-as-argsfiles)
该程序允许完全控制 MAIN 的处理。它得到一个 Callable,它是应该执行的 MAIN,主线执行的返回值和其他命名变量

in-as-argsfiles 如果 STDIN 应该被视为 $*ARGFILES,它将为 True

如果未提供 RUN-MAIN,将运行默认的 RUN-MAIN 以查找旧接口的子例程,例如 MAIN_HELPERUSAGE。如果找到,将执行“旧”语义。

class Hero {
    has @!inventory;
    has Str $.name;
    submethod BUILD( :$name, :@inventory ) {
        $!name = $name;
        @!inventory = @inventory
    }
}

sub new-main($name, *@stuff ) {
    Hero.new(:name($name), :inventory(@stuff) ).perl.say
}

RUN-MAIN( &new-main, Nil );

这将打印生成的对象的名称(第一个参数)。

24.13. sub GENERATE-USAGE

GENERATE-USAGE 子例程应该接受一个 Callable,表示由于调度失败而未执行的 MAIN 子例程。这可以用于内省。所有其他参数都是设置为发送到MAIN的参数。它应该返回您想要显示给用户的使用信息的字符串。这个例子只是重新创建从处理参数创建的 Capture

sub GENERATE-USAGE(&main, |capture) {
    capture<foo>:exists
      ?? "You're not allowed to specify a --foo"
      !! &*GENERATE-USAGE(&main, |capture)
}

您还可以使用 multi 子例程来创建相同的效果:

multi sub GENERATE-USAGE(&main, :$foo!) {
    "You're not allowed to specify a --foo"
}
multi sub GENERATE-USAGE(&main, |capture) {
    &*GENERATE-USAGE(&main, |capture)
}

请注意,动态变量 &*GENERATE-USAGE 可用于执行默认使用消息生成,因此您不必重新发明整个轮子。

24.14. 拦截 MAIN 调用(2018.10之前,v6.e)

较旧的接口使得一个接口完全拦截对 MAIN 的调用。这取决于是否存在 MAIN_HELPER 子程序,如果在程序的主线中找到 MAIN 子程序,则该子程序将被调用。

此接口从未记录过。但是,使用此未记录的界面的任何程序将继续运行,直到 v6.e。从 v6.d 开始,使用未记录的 API 将导致 DEPRECATED 消息。

生态系统模块可以提供新旧接口,以便与旧版本的 Raku 兼容:如果较新的 Raku 识别出新的(记录的)接口,它将使用它。如果没有可用的新接口子例程,但旧的 MAIN_HELPER 接口是,那么它将使用旧接口。

如果模块开发人员决定仅为 v6.d 或更高版本提供模块,则可以从模块中删除对旧接口的支持。

25. 输入 Unicode 字符

Raku 允许把 unicode 字符用作变量名. 很多操作符使用 unicode 符号(特别是在 set/bag 操作符中)还有一些引号结构. 因此, 知道如何把这些符号输入编辑器, Raku shell 和 命令行中是极好的, 特别是现实键盘中不存在那个符号的时候.

在各种操作系统和环境下关于输入 unicode 字符的通用信息可以在 Wikipedia unicode 输入页 中找到.

25.1. XCompose (Linux)

25.2. WinCompose(Windows)

25.2.1. 终端, shell和编辑器

25.2.2. Vim

在 Vim 中, unicode 字符是通过先按 Ctrl-V(也表示为 ^V), 然后按下 u 和 要输入的 unicode 字符的十六进制值来输入的(在插入模式). 例如, 希腊字母 λ (lambda) 是通过组合键来输入的:

^Vu03BB

更多关于在 Vim 中输入特殊字符的信息可以在 Vim Wikia 页 键入特殊字符 中找到.

25.2.3. Emacs

在 Emacs 中, unicode 字符的输入是首先输入和弦 Ctrl-x 8 Enter , 然后再输入 unicode 代码点的十六进制数字, 然后回车. 因此, 要输入希腊字母 λ (lambda) 使用下面的组合键(命令之间添加了空格以使清晰):

Ctrl-x 8 Enter 3bb Enter

更多关于在 Emacs 中输入 unicode 字符的信息可以在 Unicode 编码 Emacs wiki 页面 中找到.

25.2.4. Unix shell

在 bash shell 中, 要输入 unicode 字符先键入 Ctrl-Shift-u, 然后键入 unicode 代码点的值后回车. 例如, 要键入属于操作符()这个 unicode 字符, 使用下面的组合键(添加的空白是为了清晰):

Ctrl-Shift-u 2208 Enter

如果在 Unix shell 中开启了 REPL, 这也是一种在 perl 6 的 REPL 中输入 unicode 字符的方式之一.

25.3. Raku 中有用的 Unicode 字符

25.3.1. 小引号

这些字符在法语和德语中是当作引号使用的. 在 Raku 中, 它们仍然用作引号(在 POD 中是单引号, 在普通代码中是双引号), 还可以标示超运算符. 下面是这些符号和它们的 unicode 十六进制值:

符号 unicode代码点 ascii equivalent
«	U+00AB	      <<
»	U+00BB	      >>

因此, 下面这些结构是可用的:

C« fixed-width POD text »
say (1, 2) »+« (3, 4);     # 4 6 ; element-wise add
@array »+=» 42;            # add 42 to each element of @array
say «moo»;                 # moo
my $baa = 123; say «$baa»; # 123

25.3.2. Set/bag 操作符

下面列出的 set/bag 操作符 都有与集合理论相关的符号, unicode 代码点, 和它们的 ascii 等价物. 要构成这样的一个字符, 只需键入字符组合键(例如 Vim中的 Ctrl-V u, Bash 中的 Ctrl-Shift-u), 然后输入 unicode 代码点的十六进制数.

操作符	unicode代码点	ascii equivalent
∈	  U+2208	    (elem)
∉	  U+2209	    !(elem)
∋	  U+220B	    (cont)
∌	  U+220C	    !(cont)
⊆	  U+2286	    (<=)
⊈	  U+2288	    !(<=)
⊂	  U+2282	    (<)
⊄	  U+2284	    !(<)
⊇	  U+2287	    (>=)
⊉	  U+2289	    !(>=)
⊃	  U+2283	    (>)
⊅	  U+2285	    !(>)
≼	  U+227C	    (<+)
≽	  U+227D	    (>+)
∪	  U+222A	    (|)
∩	  U+2229	    (&)
∖	  U+2216	    (-)
⊖	  U+2296	    (^)
⊍	  U+228D	    (.)
⊎	  U+228E	    (+)

25.3.3. 数学符号

Wikipedia 包含了一个 unicode 中数学操作符和符号 的完整列表, 还有它们数学意义的链接.

25.3.4. 希腊字符

希腊字符可以用作变量名了. 查看 Greek in Unicode Wikipedia article 列表获取希腊和埃及字符还有它们的 unicode 代码点.

例如, 把数值3赋值给 π, 在 Vim 中输入(添加的空格是为了清晰):

my $Ctrl-V u 03C0 = 3;  # same as: my $π = 3;
say $Ctrl-V u 03C0;     # 3    same as: say $π;

25.3.5. 上标和下标

使用 U+207x, U+208x 和 (less often) U+209x 范围能直接创建一个有限的上标和下标的集合. 然而, 要生成一个值的平方或立方, 你需要使用 U+00B2U+00B3 , 因为这些被定义在 Latin1 supplement Unicode block) 中.

因此, 要书写泰勒级数展开, 你可以在 Vim 中输入:

exp(x) = 1 + x + xCtrl-V u 00B2/2! + xCtrl-V u 00B3/3! + ... + xCtrl-V u 207F/n!
# which would appear as
exp(x) = 1 + x + x²/2! + x³/3! + ... + xⁿ/n!

或者指定列表中从1到k 的元素:

ACtrl-V u 2081, ACtrl-V u 2082, ..., ACtrl-V u 2096
# which would appear as
A₁, A₂, ..., Aₖ

26. 迭代

26.1. Iterator 和 Iterable 角色

Raku 是一种函数式语言,但在处理复杂的数据结构时,函数需要一些东西来支撑。特别是,它们需要一个统一的接口,可以以同样的方式应用于所有数据结构。IteratorIterable 角色就提供了这样一种接口。

Iterable 角色比较简单。它为迭代器方法提供了一个存根,也就是 for 等语句实际使用的方法,for 会对它前面的变量调用 .iterator,然后每运行一项就运行一次块。其他方法, 如数组赋值, 将使 Iterable 类以同样的方式运行。

class DNA does Iterable {
    has $.chain;
    method new ($chain where {
                       $chain ~~ /^^ <[ACGT]>+ $$ / and
                       $chain.chars %% 3 } ) {
        self.bless( :$chain );
    }

    method iterator(DNA:D:){ $.chain.comb.rotor(3).iterator }
};

my @longer-chain =  DNA.new('ACGTACGTT');
say @longer-chain.raku;
# OUTPUT: «[("A", "C", "G"), ("T", "A", "C"), ("G", "T", "T")]␤»

say @longer-chain».join("").join("|"); #OUTPUT: «ACG|TAC|GTT␤»

在这个例子中,这是 Iterable 中示例的扩展,显示了如何调用 .iterator,只有当创建的对象被分配到一个Positional变量, @long-chain 时,迭代器方法才会在适当的上下文中被调用; 这个变量是一个数组,我们在上一个例子中对它进行操作。

Iterator(可能有点容易混淆) 角色比 Iterable 更复杂一些。首先,它提供了一个常量, IterationEnd。然后, 它还提供了一系列方法,如 .pull-one,它允许在几个上下文中对迭代进行更精细的操作:添加或删除项,或跳过它们以访问其他项。事实上,该角色为所有其他方法提供了一个默认的实现,所以唯一需要定义的方法就是 pull-one,角色只提供了其中的一个存根(stub)。Iterable 提供了循环将使用的高级接口,而 Iterator 则提供了循环的每次迭代都会调用的低级函数。我们用这个角色来扩展前面的例子。

class DNA does Iterable does Iterator {
    has $.chain;
    has Int $!index = 0;

    method new ($chain where {
                       $chain ~~ /^^ <[ACGT]>+ $$ / and
                       $chain.chars %% 3 } ) {
        self.bless( :$chain );
    }

    method iterator( ){ self }
    method pull-one( --> Mu){
        if $!index < $.chain.chars {
            my $codon = $.chain.comb.rotor(3)[$!index div 3];
            $!index += 3;
            return $codon;
        } else {
            return IterationEnd;
        }
    }
};

my $a := DNA.new('GAATCC');
.say for $a; # OUTPUT: «(G A A)␤(T C C)␤»

我们声明一个 DNA 类,它扮演两个角色,IteratorIterable; 这个类将包含一个字符串,这个字符串的长度将被约束为3的倍数, 并且只由 ACGT 组成。

我们先来看一下 pull-one 方法。这个方法将在每次发生新的迭代时被调用,所以它必须保持上一次的状态。$.index 属性将在各次调用中保持该状态; pull-one 将检查是否已经到达链的末端,并将返回角色提供的 IterationEnd 常量。实现这种低级接口, 实际上简化了 Iterable 接口的实现。现在, 迭代器将是对象本身,因为我们可以对它调用 pull-one 来依次访问每一个成员; .iterator 将因此只返回 self; 这是可能的,因为对象将同时是 IterableIterator

这并非总是如此,在大多数情况下 .iterator 将不得不构建一个要返回的迭代器类型(例如,它将跟踪迭代状态,我们现在正在主类中这样做),就像我们在前面的例子中所做的那样;然而,这个例子显示了构建一个满足迭代器和可迭代角色的类所需的最小代码。

26.2. 如何迭代:上下文化和主题变量

for 和其他循环将每次迭代中产生的项放入主题变量 $_+ 中,或者将它们捕获到随块声明的变量中。这些变量可以直接在循环内部使用,而不需要使用 ^twigil 来声明它们。

使用 序列运算符时会发生隐式迭代。

say 1,1,1, { $^a²+2*$^b+$^c } … * > 300; # OUTPUT: «(1 1 1 4 7 16 46 127 475)

生成块正在运行一次,而完成序列的条件(在这种情况下,项大于 300)没有得到满足。这样做的副作用是运行了一个循环,但也创建了一个输出的列表。

这可以通过使用 gather/take 块来更系统地完成,gather/take 块是一种不同的迭代构造,它不是在 sink 上下文中运行,而是每迭代一次就返回一项。这篇降临日历教程解释了这种循环的用例,其实,gather 并不是一种循环结构,而是一个语句前缀,它收集了 take 产生的项,并从这些项中创建一个列表。

26.3. 经典循环以及为什么我们不喜欢它们

经典的 for 循环,循环变量递增,可以在 Raku 中通过 loop 关键字来完成。其他的 repeatwhile 循环也是可能的。

然而,在一般情况下,它们是不被鼓励的。Raku 是一种函数式和并发式的语言;当用 Raku 编码时,你应该用函数式的方式来看待循环:逐个处理迭代器产生的项,也就是把一个项喂给一个块,而不产生任何形式的二次效应。这种函数式的观点还可以通过 hyperrace 自动线程的方法轻松实现操作的并行化。

如果你觉得你的老式循环更舒服,语言允许你使用它们。然而,在 Raku 中,只要有可能,尽量使用函数式和并发迭代结构被认为是更好的做法。

注意:自从 6.d 版本以来,循环可以从最后语句的值中产生一个值的列表。

27. 使用 Raku 做数学计算

27.1. Sets

Raku 包括 Set 数据类型,以及对大多数 set 操作的支持。并集和交集不仅是原生操作,它们使用自然符号 。例如,此代码将检查有限数量集的集算术的基本定律:

my @arbitrary-numbers = ^100;
my \U = @arbitrary-numbers.Set;

my @sets;

@sets.push: Set.new( @arbitrary-numbers.pick( @arbitrary-numbers.elems.rand)) for @arbitrary-numbers;

my (@union, @intersection);

for @sets -> $set {
    @union.push: $set ∩ $set === $set;
    @intersection.push: $set ∪ $set === $set;
}

say "Idempotent union is ", so @union.all;
# OUTPUT: «Idempotent union is True»
say "Idempotent intersection is ", so @intersection.all;
# OUTPUT: «Idempotent intersection is True»
my (@universe, @empty-set, @id-universe, @id-empty);

for @sets -> \A {
    @universe.push: A ∪ U === U;
    @id-universe.push: A ∩ U === A;
    @empty-set.push: A ∩ ∅ === ∅;
    @id-empty.push: A ∪ ∅ === A;
}

say "Universe dominates ", so @universe.all;    # OUTPUT: «Universe dominates True»
say "Empty set dominates ", so @empty-set.all;  # OUTPUT: «Empty set dominates True»

say "Identity with U ", so @id-universe.all;    # OUTPUT: «Identity with U True»
say "Identity with ∅ ", so @id-empty.all;       # OUTPUT: «Identity with ∅ True»

在这个使用 Raku 已经定义的空集的代码中,我们不仅检查集合代数中的等式是否成立,我们还通过无符号变量和集合运算符的 Unicode 形式使用表达式。尽可能接近原始形式; 例如,A ∪ U === U,除了使用值标识运算符 ⇐=⇒维基百科条目中的实际数学表达式非常接近。

我们甚至可以测试摩根定律,如下面的代码所示:

my @alphabet = 'a'..'z';
my \U = @alphabet.Set;
sub postfix:<⁻>(Set $a) { U ⊖ $a }
my @sets;
@sets.push: Set.new( @alphabet.pick( @alphabet.elems.rand)) for @alphabet;
my ($de-Morgan1,$de-Morgan2) = (True,True);
for @sets X @sets -> (\A, \B){
    $de-Morgan1 &&= (A ∪ B)⁻  === A⁻ ∩ B⁻;
    $de-Morgan2 &&= (A ∩ B)⁻  === A⁻ ∪ B⁻;
}
say "1st De Morgan is ", $de-Morgan1;
say "2nd De Morgan is ", $de-Morgan2;

我们声明 - 作为补语运算,它计算通用集U和我们集之间的对称差⊖。一旦宣布这一点,就可以比较容易地表达诸如A和B的并集 (A ∪ B)⁻ 的补集,其符号与原始数学符号非常接近。

27.2. 算术

Raku 可以使用不同的数据类型进行算术运算。 NumRatComplex 都可以在操作加法,减法,乘法和除法下作为场)运行。等效的数学领域是:

Raku class

Field

C<Rat>

C<Num>

C<Complex>

Int`s 虽然在技术上与 Z 相对应,但它并不是真正的数学领域,因为它们不是在四个算术运算下关闭的,并且整数不满足同一性公理。但是,如果使用整数除法 `div,它们的操作将始终产生其他整数;但是,如果使用 /,通常结果将是 Rat

此外,Int 可以进行无限精度算术(或者至少在存储器允许的情况下无限制;还可以发生数字溢出(Numeric overflow)),如果数字太大则不会回落到 Num

my @powers = 2, 2 ** * ... Inf; say @powers[4].chars; # OUTPUT: «19729␤»

同样严格地说,行为类似于数学领域的 Rational 类是 FatRat。出于效率原因,当数字足够大或者分子和分母之间存在很大差异时,操作 Rats 时会回落到 NumFatRat 可以使用任意精度,与默认的 Int 类相同。

生态系统中的某些模块可以使用数学方法处理其他数据类型:

数字会自动变为其实际表示的数字类型:

.^name.say for (4, ⅗, 1e-9, 3+.1i); # OUTPUT: «Int␤Rat␤Num␤Complex␤»

这也使算术运算最适合特定类型

say .33-.22-.11 == 0; # OUTPUT: «True␤»

在这儿,所有数字都被解释为 `Rat`s,这使得操作准确。通常,大多数语言会将它们解释为浮点数,

say .33.Num -.22.Num - .11.Num; # OUTPUT: «1.3877787807814457e-17␤»

对于这样的情况,Raku 还包括一个近似相等的运算符(` approximately equal `),

say .33.Num -.22.Num - .11.Num ≅ 0; # OUTPUT: «True␤»

27.3. 序列

序列是允许重复的对象的枚举集合,也是 Raku 中称为 Seq 的第一类数据类型。 Seq 能够表示无限序列,如自然数:

my \𝕟 = 1,2 … ∞;
say 𝕟[3];# OUTPUT: «4␤»

无限序列使用 ∞,Inf*(Whatever)作为终止符。 …​ 是列表生成器,只要插入第一个数字,它实际上可以理解算术和几何级数:

say 1,5,9 … * > 100;
# OUTPUT: «(1 5 9 13 17 21 25 29 33 37 41 45 49 53 57 61 65 69 73 77 81 85 89 93 97 101)␤»
say 1,3,9 … * > 337; # OUTPUT: «(1 3 9 27 81 243 729)␤»

当生成的数字大于 100 时,第一个序列将终止; 第二个序列,当它大于 337 时,是几何级数。

可以使用任意生成器的事实可以很容易地生成像 Fibonacci 这样的序列:

say 1,1, * + * … * > 50;#  OUTPUT: «(1 1 2 3 5 8 13 21 34 55)␤»

事实上,我们可以通过这种方式计算黄金比例的近似值:

my @phis = (2.FatRat, 1 + 1 / * ... *);
my @otherphi = (1 - @phis[200], 1 + 1 / * ... *);
say @otherphi[^10, |(20, 30 ... 100)];# OUTPUT:
# «((-0.61803398874989484820458683436563811772030918
# -0.61803398874989484820458683436563811772030918
# -0.61803398874989484820458683436563811772030918
# -0.61803398874989484820458683436563811772030918
# -0.61803398874989484820458683436563811772030918
# -0.618033…»

Math::Sequences 模块包含许多已为你定义的数学序列。它定义了百科全书中的许多序列,其中一些序列的原始名称,如 ℤ 或 ℝ。

一些集合运算符也对序列进行操作,它们可用于查明对象是否是其中的一部分:

say 876 ∈ (7,14 … * > 1000) ; # OUTPUT: «False␤»

在这种特殊情况下,我们可以看出 876 是否是 7 的直接倍数,但同样的原理适用于使用复杂发生器的其他序列。我们也可以使用集合包含运算符:

say (55,89).Set ⊂ (1,1, * + * … * > 200); # OUTPUT: «True␤»

虽然它没有考虑到它是否实际上是一个子序列,但这里只是存在两个元素;集合没有顺序,即使您没有将子序列显式地转换为Set或显式地将其转换为Seq,它也会被强制转换为包含运算符的应用程序。

27.4. 数学常数

Raku 已经包含了一组数学常量作为核心的一部分。

say π; # OUTPUT: «3.141592653589793»
say τ; # Equivalent to 2π; OUTPUT: «6.283185307179586»
say 𝑒; # OUTPUT: «2.718281828459045␤»

它们的拉丁名字 e, pitau 也是可得的, 具有相同的值 (尽管 𝑒 在 MoarVM 外面访问不了).

Math::Constants 模块包括一系列额外的物理和数学常数,例如前面提到的黄金比率 φ 或普朗克常数

由于 Raku 允许定义使用 Unicode 字形的变量,以及没有任何类型的 sigil 的变量和常量名称,因此使用概念的实际数学名称来尽可能地命名它们被认为是一种好的做法。

28. 输入和输出

在这里,我们简要概述了与文件相关的输入/输出操作。详细信息可以在 IO 角色的文档中找到,也可以在 IO::HandleIO::Path 类型中找到。

28.1. 读取文件

读取文件内容的一种方法是通过带有 :r(读取)文件模式选项的 open 函数打开文件,并吞噬内容:

my $fh = open "testfile", :r;
my $contents = $fh.slurp;
$fh.close;

这里我们使用 IO::Handle 对象上的 close 方法显式地关闭文件句柄。这是一种非常传统的读取文件内容的方法。但是,同样的事情可像这样更容易和更清楚地完成:

my $contents = "testfile".IO.slurp;
# or in procedural form:
$contents = slurp "testfile"

通过将 IO 角色添加到文件名字符串中,我们实际上能够将字符串作为文件对象本身引用,从而直接吞噬其内容中。请注意,slurp 负责为你打开和关闭文件。

28.1.1. 逐行读取

当然,我们也可以选择逐行读取文件。将排除新行分隔符(即 $*IN.nl-in)。

for 'huge-csv'.IO.lines -> $line {
    # Do something with $line
}

# or if you'll be processing later
my @lines = 'huge-csv'.IO.lines;

28.2. 写文件

要将数据写入文件,我们再次选择调用 open 函数的传统方法 - 这次使用 :w(write)选项 - 并将数据打印到文件中:

my $fh = open "testfile", :w;
$fh.print("data and stuff\n");
$fh.close;

或者使用等效的 say,因此不再需要显式的换行符了:

my $fh = open "testfile", :w;
$fh.say("data and stuff");
$fh.close;

我们可以通过使用 spurt 在写入模式下打开文件,将数据写入文件并再次为我们关闭来简化此操作:

spurt "testfile", "data and stuff\n";

默认情况下,所有(文本)文件都写为 UTF-8,但是如果需要,可以通过 :enc 选项指定显式编码:

spurt "testfile", "latin1 text: äöüß", enc => "latin1";

要将格式化的字符串写入文件, 请使用 IO::Handleprintf 函数。

my $fh = open "testfile", :w;
$fh.printf("formatted data %04d\n", 42);
$fh.close;

要追加到文件,请在显式地打开文件句柄时指定 :a 选项,

my $fh = open "testfile", :a;
$fh.print("more data\n");
$fh.close;

或者使用等效的 say,因此不再需要显式的换行符了,

my $fh = open "testfile", :a;
$fh.say("more data");
$fh.close;

或者甚至在调用 spurt 时加上 :append 选项:

spurt "testfile", "more data\n", :append;

要将二进制数据显式地写入文件,请使用 :bin 选项打开它。然后输入/输出操作将使用 Buf 类型而不是 Str 类型。

28.3. 复制和重命名文件

例程 copy, rename, 和 move 是可用的以避免低级别的系统命令。 在 copy, rename, 和 move 查看详情. 一些例子:

my $filea = 'foo';
my $fileb = 'foo.bak';
my $filec = '/disk1/foo';  # note 'diskN' is assumed to be a physical storage device

copy $filea, $fileb;              # overwrites $fileb if it exists
copy $filea, $fileb, :createonly; # fails if $fileb exists

rename $filea, 'new-foo';              # overwrites 'new-foo' if it exists
rename $filea, 'new-foo', :createonly; # fails if 'new-foo' exists

# use move when a system-level rename may not work
move $fileb, '/disk2/foo';              # overwrites '/disk2/foo' if it exists
move $fileb, '/disk2/foo', :createonly; # fails if '/disk2/foo' exists

28.4. 检查文件和目录

IO::Handle 对象上使用 e 方法来测试文件或目录是否存在。

if "nonexistent_file".IO.e {
    say "file exists";
}
else {
    say "file doesn't exist";
}

也可以使用冒号对语法来实现相同的功能:

if "path/to/file".IO ~~ :e {
    say 'file exists';
}
my $file = "path/to/file";
if $file.IO ~~ :e {
    say 'file exists';
}

与文件存在检查类似,也可以检查路径是否是目录。例如,假设文件 testfile 和目录 lib 存在,我们将从存在测试方法 e 获得相同的结果,即两者都存在:

say "testfile".IO.e;  # OUTPUT: «True␤»
say "lib".IO.e;       # OUTPUT: «True␤»

但是,由于它们中只有一个是目录,因此目录测试方法 d 将给出不同的结果:

say "testfile".IO.d;  # OUTPUT: «False␤»
say "lib".IO.d;       # OUTPUT: «True␤»

当我们通过文件测试方法 f 检查路径是否是文件时,结果自然会反过来:

say "testfile".IO.f;  # OUTPUT: «True␤»
say "lib".IO.f;       # OUTPUT: «False␤»

还有其他方法可用于查询文件或目录,一些有用的方法是:

my $f = "file";

say $f.IO.modified; # return time of last file (or directory) change
say $f.IO.accessed; # return last time file (or directory) was read
say $f.IO.s;        # return size of file (or directory inode) in bytes

更多方法和详细信息请查看 IO::Path.

28.5. 获取目录列表

要列出当前目录的内容,请使用 dir 函数。它返回 IO::Path 对象的列表。

say dir;          # OUTPUT: «"/path/to/testfile".IO "/path/to/lib".IO␤»

要列出给定目录中的文件和目录,只需将路径作为参数传递给 dir

say dir "/etc/";  # OUTPUT: «"/etc/ld.so.conf".IO "/etc/shadow".IO ....␤»

28.6. 创建和移除目录

要创建一个新目录,只需使用目录名作为参数调用函数 mkdir

mkdir "newdir";

该函数在成功时返回创建目录的名称,在失败时返回 Nil。因此,标准的 Perl 惯用法按预期工作:

mkdir "newdir" or die "$!";

使用 rmdir 来移除*空*目录:

rmdir "newdir" or die "$!";

29. 数据结构

29.1. 标量结构

某些类没有任何*内部*结构, 访问它们的一部分必须使用特定的方法。数字,字符串和其他一些整体类包含在该类中。他们使用 $ sigil,虽然复杂的数据结构也可以使用它。

my $just-a-number = 7;
my $just-a-string = "8";

有一个 Scalar 类,它在内部用于为使用 $ sigil 声明的变量赋值。

my $just-a-number = 333;
say $just-a-number.VAR.^name; # OUTPUT: «Scalar␤»

任何复杂数据结构都可以通过使用 $ 在项上下文中*标量化*。

(1, 2, 3, $(4, 5))[3].VAR.^name.say; # OUTPUT: «Scalar␤»

但是,这意味着它将在它们的上下文中被视为标量。你仍然可以访问其内部结构。

(1, 2, 3, $(4, 5))[3][0].say; # OUTPUT: «4␤»

有一个有趣的副作用,或者可能是故意的特性,是标量化保留了复杂结构的同一性。

for ^2 {
     my @list = (1, 1);
     say @list.WHICH;
} # OUTPUT: «Array|93947995146096␤Array|93947995700032␤»

每次 (1, 1) 被分配时,创建的变量在 === 上的意义上是不同的; 如它所示,打印了内部指针所表示的不同值。然而

for ^2 {
  my $list = (1, 1);
  say $list.WHICH
} # OUTPUT: «List|94674814008432␤List|94674814008432␤»

在这种情况下,$list 使用的是 Scalar sigil,因此将是一个 Scalar。任何具有相同值的标量都将完全相同,如打印指针时所显示的那样。

29.2. 复杂数据结构

根据你如何访问其第一级元素, 复杂的数据结构分为两大类: Positional, 或类列表结构 Associative, 或类键值对儿结构。 通常, 复杂数据结构, 包括对象, 会是两者的组合, 使对象属性变为键值对儿。而所有的对象都是 Mu 的子类, 通常复杂对象是 Any 子类的实例。 虽然理论上可以在不这样做的情况下混合使用 “Positional” 或 “Associative”,但是大多数适用于复杂数据结构的方法都是在 “Any” 中实现的。

操纵这些复杂的数据结构是一项挑战,但 Raku 提供了一些可用于它们身上的函数:deepmapduckmap。而前者会按顺序切换每个元素,无论块传递的是什么。

say [[1, 2, [3, 4]],[[5, 6, [7, 8]]]].deepmap( *.elems );
# OUTPUT: «[[1 1 [1 1]] [1 1 [1 1]]]␤»

这返回 1 因为它进入更深层次并将 elems 应用于元素,deepmap 可以执行更复杂的操作:

say [[1, 2, [3, 4]], [[5, 6, [7, 8]]]].duckmap:
   -> $array where .elems == 2 { $array.elems };
# OUTPUT: «[[1 2 2] [5 6 2]]␤»

在这种情况下,它深入到结构中,但如果它不满足块 (1, 2) 中的条件则返回元素本身,如果它满足则返回数组的元素数(每个子数组末尾的两个 2 )。

由于 deepmapduckmapAny 方法,它们也适用于关联数组:

say %( first => [1, 2], second => [3,4] ).deepmap( *.elems );
# OUTPUT: «{first => [1 1], second => [1 1]}␤»

仅在这种情况下,它们将应用于作为值的每个列表或数组,而仅保留键。

PositionalAssociative 可以相互转换。

say %( first => [1, 2], second => [3,4] ).list[0];
# OUTPUT: «second => [3 4]␤»

但是,在这种情况下,对于 Rakudo >= 2018.05,它每次运行时都会返回不同的值。哈希将被转换为键值对的列表,但保证它是无序的。你也可以从相反的方向进行操作,只要该列表具有偶数个元素(奇数将导致错误):

say <a b c d>.Hash # OUTPUT: «{a => b, c => d}␤»

但是

say <a b c d>.Hash.kv # OUTPUT: «(c d a b)␤»

每次运行时都会获得不同的值; kv 把每个 Pair 转换成列表。

复杂数据结构通常还是 Iterable 的。 从中生成 iterator 将允许程序逐个访问结构的第一级:

.say for 'א'..'ס'; # OUTPUT: «א␤ב␤ג␤ד␤ה␤ו␤ז␤ח␤ט␤י␤ך␤כ␤ל␤ם␤מ␤ן␤נ␤ס␤»

'א'..'ס' 是一个 Range, 一个复杂数据结构, 把 for 放在它前面会迭代直到列表元素耗尽。你可以通过重写 iterator 方法(来自角色 Iterable)以在你的复杂数据结构上使用 for:

class SortedArray is Array {
  method iterator() {
    self.sort.iterator
  }
};
my @thing := SortedArray.new([3,2,1,4]);
.say for @thing; # OUTPUT: «1␤2␤3␤4␤»

for 直接调用 @thing 上的 iterator 方法, 使其按顺序返回数组元素。更多信息请参阅 专门讨论迭代的页面.

29.3. 函数式结构

Raku 是一种函数式语言,因此,函数是一等*数据*结构。函数遵循 Callable 角色,这是基础角色四重奏中的第 4 个元素。 Callable& sigil 一起使用,尽管在大多数情况下,为了简单起见,它被省略了; 在 Callables 的情况下,总是允许消除这种 sigil。

my &a-func= { (^($^þ)).Seq };
say a-func(3), a-func(7); # OUTPUT: «(0 1 2)(0 1 2 3 4 5 6)␤»

Block 是最简单的可调用结构,因为 Callable 无法实例化。在这种情况下,我们实现了一个记录事件的块并可以检索它们:

my $logger = -> $event, $key = Nil  {
  state %store;
  if ( $event ) {
    %store{ DateTime.new( now ) } = $event;
  } else {
    %store.keys.grep( /$key/ )
  }
}
$logger( "Stuff" );
$logger( "More stuff" );
say $logger( Nil, "2018-05-28" ); # OUTPUT: «(Stuff More stuff)␤»

Block 有一个 Signature,在这种情况下有两个参数,第一个是要记录的事件,第二个是要检索的事件的键。它们将以独立的方式使用,但其目的是展示状态变量 的使用,该状态变量从每次调用到下一次调用时都会被保留。此状态变量封装在块中,除非使用块提供的简单 API,否则无法从外部访问:使用第二个参数调用块。前两个调用记录两个事件,示例底部的第三个调用使用第二种类型的调用来检索存储的值。 Block 可以被克隆:

my $clogger = $logger.clone;
$clogger( "Clone stuff" );
$clogger( "More clone stuff" );
say $clogger( Nil, "2018-05-28" );
# OUTPUT: «(Clone stuff More clone stuff)␤»

克隆将重置状态变量; 代替克隆,我们可以创建改变 API 的 façades。例如,无需使用 Nil 作为第一个参数来检索特定日期的日志:

my $gets-logs = $logger.assuming( Nil, * );
$logger( %(changing => "Logs") );
say $gets-logs( "2018-05-28" );
# OUTPUT: «({changing => Logs} Stuff More stuff)␤»

assuming 包裹着一个块调用,给我们需要的参数赋值(在本例中为`Nil`), 将参数传递给我们使用 * 表示的其他参数。 实际上,这对应于自然语言语句 “我们正在调用`$logger` *假设*第一个参数是 Nil”。 我们可以稍微改变这两个块的外观,以澄清它们实际上是在同一个块上运行:

my $Logger = $logger.clone;
my $Logger::logs = $Logger.assuming( *, Nil );
my $Logger::get = $Logger.assuming( Nil, * );
$Logger::logs( <an array> );
$Logger::logs( %(key => 42) );
say $Logger::get( "2018-05-28" );

尽管 :: 通常用于调用类方法,但它实际上是变量名称的有效部分。在这种情况下,我们通常使用它们来简单地指示 $Logger::logs$Logger::get 实际上是在调用 $Logger,我们已经大写使用了类似于类的外观。本教程的重点是,使用函数作为一等公民,以及使用状态变量,允许使用某些有趣的设计模式,例如这个。

作为这样的一等数据结构,可以在其他类型的数据可以使用的任何地方使用 callable。

my @regex-check = ( /<alnum>/, /<alpha>/, /<punct>/ );
say @regex-check.map: "33af" ~~ *;
# OUTPUT: «(「3」␤ alnum => 「3」 「a」␤ alpha => 「a」 Nil)␤»

正则表达式实际上是一种 callable 类型:

say /regex/.does( Callable ); # OUTPUT: «True␤»

在上面的例子中,我们调用存储在数组中的正则表达式,并将它们应用于字符串字面值。

使用函数组合运算符∘组成 Callables:

my $typer = -> $thing { $thing.^name ~ ' → ' ~ $thing };
my $Logger::withtype = $Logger::logs ∘ $typer;
$Logger::withtype( Pair.new( 'left', 'right' ) );
$Logger::withtype( ¾ );
say $Logger::get( "2018-05-28" );
# OUTPUT: «(Pair → left right Rat → 0.75)␤»

我们使用上面定义的函数组合 $Logger::logs$typer,获得一个记录其类型前面的对象的函数,例如,这对于过滤非常有用。 $Logger::withtype 实际上是一个复杂的数据结构,由两个以串行方式应用的函数组成,但每一个组合的 callables 都可以保持状态,从而创建复杂的变换 callables,其设计模式是:类似于面向对象领域中的对象组合。在每种特定情况下,你都必须选择最适合你的问题的编程风格。

29.4. 定义和约束数据结构

Raku 有不同的方法来定义数据结构,但也有许多方法来约束它们,以便你为每个问题域创建最合适的数据结构。例如,but 将角色或值混合到值或变量中:

my %not-scalar := %(2 => 3) but Associative[Int, Int];
say %not-scalar.^name; # OUTPUT: «Hash+{Associative[Int, Int]}␤»
say %not-scalar.of;    # OUTPUT: «Associative[Int, Int]␤»
%not-scalar{3} = 4;
%not-scalar<thing> = 3;
say %not-scalar;       # OUTPUT: «{2 => 3, 3 => 4, thing => 3}␤»

在这种情况下,but 混合在 Associative [Int,Int] 角色中; 请注意我们正在使用绑定,以便变量的类型是所定义的,而不是 % sigil 强加的类型; 这个混合角色显示在用花括号包围的 name 中。 它的真实意义是什么? 该角色包括两个方法,ofkeyof; 通过混合角色,将调用新的 of(旧的 of 将返回 Mu,这是 Hashes 的默认值类型)。 然而,就是这样。 它并没有真正改变变量的类型,因为你可以看到,因为我们在接下来的几个语句中使用了任何类型的键和值。

但是,我们可以使用这种类型的 mixin 为变量提供新功能:

role Lastable {
  method last() {
    self.sort.reverse[0]
  }
}
my %hash-plus := %( 3 => 33, 4 => 44) but Lastable;
say %hash-plus.sort[0]; # OUTPUT: «3 => 33␤»
say %hash-plus.last;    # OUTPUT: «4 => 44␤»

Lastable 中,我们使用通用的 self 变量来指代这个特定角色混合的任何对象; 在这种情况下,它将包含与其混合的哈希; 在其他情况下,它将包含其他内容(并可能以其他方式工作)。这个角色将为它混合的任何变量提供 last 方法,为 *常规*变量提供新的,可附加的功能。甚至可以使用 does 关键字将角色添加到现有变量

Subsets 也可用于约束变量可能包含的值; 他们是 Raku 尝试渐进类型; 它不是一个完整的尝试,因为子集在严格意义上不是真正的类型,但它们允许运行时类型检查。它为常规类型添加了类型检查功能,因此它有助于创建更丰富的类型系统,允许类似以下代码中显示的内容:

subset OneOver where (1/$_).Int == 1/$_;
my OneOver $one-fraction = ⅓;
say $one-fraction; # OUTPUT: «0.333333␤»

另一方面,my OneOver $ = ⅔; 会导致类型检查错误。子集可以使用 Whatever,即 * 来引用参数; 但是每次将它用于不同的参数时都会实例化,所以如果我们在定义中使用它两次,我们就会得到一个错误。在这种情况下,我们使用主题单变量 $_ 来检查实例化。子签名可以在签名 中直接完成,无需声明。

29.5. 无限结构和惰性

可以假设数据结构中包含的所有数据实际上都是*那里*。情况不一定如此:在许多情况下,出于效率原因或仅仅因为不可能,数据结构中包含的元素只有在实际需要时才会跳存。这种按需对项的计算称为 reification.。

# A list containing infinite number of un-reified Fibonacci numbers:
my @fibonacci = 1, 1, * + * … ∞;

# We reify 10 of them, looking up the first 10 of them with array index:
say @fibonacci[^10]; # OUTPUT: «(1 1 2 3 5 8 13 21 34 55)␤»

# We reify 5 more: 10 we already reified on previous line, and we need to
# reify 5 more to get the 15th element at index 14. Even though we need only
# the 15th element, the original Seq still has to reify all previous elements:
say @fibonacci[14]; # OUTPUT: «987␤»

上面我们具体化了用序列运算符创建了的 Seq,但其他数据结构也使用这个概念。例如,未具体化的 Range 只是两个终点。在某些语言中,计算大范围的总和是一个漫长而耗费内存的过程,但 Raku 会立即计算出来:

say sum 1 .. 9_999_999_999_999; # OUTPUT: «49999999999995000000000000␤»

为什么? 因为*不用*具体化范围总就可以计算总和; 也就是说,不用弄清楚它包含的所有元素。这就是此功能存在的原因。你甚至可以使用 gather and take 按需具体化:

my $seq = gather {
    say "About to make 1st element"; take 1;
    say "About to make 2nd element"; take 2;
}
say "Let's reify an element!";
say $seq[0];
say "Let's reify more!";
say $seq[1];
say "Both are reified now!";
say $seq[^2];

# OUTPUT:
# Let's reify an element!
# About to make 1st element
# 1
# Let's reify more!
# About to make 2nd element
# 2
# Both are reified now!
# (1 2)

在上面的输出之后,你可以看到 gather 里面的 print 语句只有当我们在查找元素时确定各个元素时才会执行。另请注意,这些元素只被修改了一次。当我们在示例的最后一行再次打印相同的元素时,就不再打印 gather 内的消息。这是因为该构造使用了来自 Seq 缓存的已经确定的元素。

请注意,上面我们将 gather 赋值给 Scalar 容器( $ sigil),而不是 Positional (@ sigil)。原因是 @-sigiled 变量*主要是eager*。这意味着他们*大部分时间*立即*明确分配给他们的东西*。他们唯一没有这样做的时候知道这些项是 is-lazy,就像我们用无穷大生成序列作为终点一样。如果我们将 gather 赋值给 @-variable,那里面的 say 语句就会被立即打印出来。

完全具体化列表的另一种方法是在其上调用 .elems。这就是为什么检查列表是否包含任何项最好使用 .Bool 方法的原因(或者只使用 if @array { … }),因为你不需要明确*所有*元素以找出它们中的任何一个。

有些时候你*确实*需要在做某事之前完全具体化列表。例如,IO::Handle.lines 返回 Seq。以下代码包含错误; 记住具体化,试着发现它:

my $fh = "/tmp/bar".IO.open;
my $lines = $fh.lines;
close $fh;
say $lines[0];

我们打开 filehandle,然后分配 .linesScalar 变量,因此返回的 Seq 不会立刻被具体化。 然后我们 close 文件句柄,并尝试从 $lines 打印一个元素。

代码中的错误是在我们在最后一行具体化 $lines Seq 时,我们*已经关闭*文件句柄。 当 Seq 的 iterator 试图生成我们请求的项时,会导致尝试从关闭的句柄中读取的错误。 因此,要修复错误,我们可以在关闭句柄之前分配给 @-sigiled 变量或在 $lines 上调用 .elems:

my $fh = "/tmp/bar".IO.open;
my @lines = $fh.lines;
close $fh;
say @lines[0]; # no problem!

我们也可以使用带有具体化副作用的任何函数,如上面提到的 .elems

my $fh = "/tmp/bar".IO.open;
my $lines = $fh.lines;
say "Read $lines.elems() lines"; # reifying before closing handle
close $fh;
say $lines[0]; # no problem!

使用 eager 也将具体化整个序列:

my $fh = "/tmp/bar".IO.open;
my $lines = eager $fh.lines; # Uses eager for reification.
close $fh;
say $lines[0];

29.6. 内省

允许 内省(如Raku)的语言具有附加到类型系统的功能,允许开发人员访问容器和值元数据。该元数据可以在程序中使用,以根据它们的值执行不同的动作。从名称中可以明显看出,元数据是通过元类从值或容器中提取的。

my $any-object = "random object";
my $metadata = $any-object.HOW;
say $metadata.^mro;                   # OUTPUT: «((ClassHOW) (Any) (Mu))␤»
say $metadata.can( $metadata, "uc" ); # OUTPUT: «(uc uc)␤»

使用第一个 say,我们展示了元模型类的类层次结构,在本例中是 Metamodel::ClassHOW。它直接继承自 Any,这意味着可以使用任何方法; 它还混合了几个角色,可以为您提供有关类结构和功能的信息。但是那个特定类的方法之一是 can,我们可以用它来查找对象是否可以使用 uc(大写)方法,它显然可以。但是,在某些其他情况下,当角色直接被混合到变量中时,它可能不那么明显。例如,在上面定义的的 %hash-plus 情况下:

say %hash-plus.^can("last"); # OUTPUT: «(last)␤»

在这种情况下,我们使用 HOW.method 的*语法塘* ^method 来检查你的数据结构是否响应该方法; 输出显示匹配方法的名称,证明我们可以使用它。

另请参见关于类内省的文章,了解如何访问类属性和方法,并使用它来为该类生成测试数据;这篇Advent Calendar 文章详细描述了元对象协议

30. 系统交互

30.1. 通过命令行获取参数

最简单的方法是使用 @*ARGS 变量从命令行获取参数;此数组将包含程序名称后面的字符串。 %*ENV 将包含环境变量,因此如果您使用:

export API_KEY=1967196417966160761fabc1511067
./consume_api.p6

您可以通过以下方式在程序中使用它们:

my $api-key = %*ENV<API_KEY> // die "Need the API key";

如果先前未定义环境变量 API_KEY,则此操作将失败。

Raku 有一个更好的方法来处理命令行参数,如果它们代表文件名:那么使用 $*ARGFILES 动态变量。

for $*ARGFILES.lines -> $l {
    say "Long lines in {$*ARGFILES.path}"
        if $l.chars > 72 ;
}

例如,你可以用 argf​​iles.p6 *.p6 的方式运行这个程序,每次找到一个超过72个字符的行时,它就会打印一个文件名。 $*ARGFILES 包含命令行中描述的所有文件的文件句柄 - .lines 将依次读取每行文件的一行,每次处理新句柄时都会更改 $*ARGFILES.path 的值。通常,它为处理文件集的脚本提供了非常方便的 API。

30.2. 以交互方式获取参数

使用 prompt 让一个正在运行的程序向用户查询数据:

my UInt $num-iters = prompt "How many iterations to run: ";

30.3. 同步和异步运行程序

运行外部程序有两个例程:runshell。两者都存在于 IO 角色中,因此包含在混合该角色的所有类中,如 IO::Path。两者都返回一个 Proc 对象,但主要区别在于 run 会尽可能避免系统 shell,而 shell 会通过默认系统 shell 运行命令。

运行所有外部程序的关键类是 Proc::Async,它以异步方式运行进程,并允许与正在运行的进程进行并发交互。通常,通过这些高级抽象接口与系统进行交互是一种很好的做法。但是,Raku 提供了通过低级接口与系统交互的其他方式。

30.4. 通过原生 API 进行操作系统调用

NativeCall 可用于与系统库以及任何其他可访问库进行交互。这个link:(简短的教程)解释了,例如,如何使用该接口调用系统函数,如 getaddrinfo;通过使用 NativeCall 接口的声明,也可以通过这种方式访问​​其他一些函数,例如 kill

幸运的是,您不必为所有原生功能执行此操作。作为将 Perl 5 作为生态系统的一部分移植到 Raku 的蝴蝶项目的一部分,Elizabeth Mattijsen 正在将许多系统功能移植到 P5getprotobyname 等模块中,这些功能包括 endprotoentgetprotoentgetprotobyname 等功能。 getprotobynumbersetprotoent。如果要使用p6y形式的那些功能,请搜索并安装P5模块

31. Sets、Bags 和 Mixes

简而言之,这些类通常包含无序的对象集合。Set 仅考虑这些对象是否存在,bags 可以容纳多个相同类型的对象,mixes 也允许分数(和负)权重。常规版本是不可变的,Hash 版本是可变的。

让我们详细说明一下。如果要收集容器中的对象但不关心这些对象的顺序,Raku 提供*无序*集合类型 Set, SetHash, Bag, BagHash, Mix, 和 MixHash. 由于无序,这些容器可以比 Lists 更有效地查找元素或处理重复的项目。

另一方面,如果你想获得包含的对象(元素)而没有重复并且你只关心元素*是否*在集合中,你可以使用 SetSetHash。如果你想消除重复但仍保留顺序,请查看 Listunique 例程。

如果你想跟踪每个对象出现的次数,你可以使用 BagBagHash。在这些Baggy 容器中,每个元素都有一个权重(无符号整数),表示同一个对象已包含在集合中的次数。

类型 MixMixHash 类似于 BagBagHash,但它们也允许分数和负权重

SetBagMiximmutable 类型。如果要在构造容器后添加或删除元素,请使用可变变体 SetHash, BagHash, 和 MixHash

六个集合类 SetSetHashBagBagHashMixMixHash,都有相似的语义。

首先,就它们而言,相同的对象引用相同的元素 - 其中使用 WHICH 方法确定身份(即以相同的方式=== 运算符检查身份)。对于像 Str 这样的值类型,这意味着具有相同的值; 对于像“Array”这样的引用类型,它意味着引用相同的对象实例。

其次,它们提供了类似 Hash 的接口,其中集合的实际元素(可以是任何类型的对象)是“键”,关联的权重是“值”:

type of $a

value of $a{$b} if $b is an element

value of $a{$b} if $b is not an element

Set / SetHash

True

False

Bag / BagHash

a positive integer

0

Mix / MixHash

a non-zero real number

0

31.1. Set/Bag operators

有几个中缀运算符致力于在 Set 上执行常见操作,例如并集和差集。其他操作包括布尔检查,例如对象是否是 Set 中的元素,或者一个 Set 是否是另一个 Set 的子集。

这些中缀可以使用代表函数的 UTF-8 字符编写(如 ),或与 (elem)) 或 (^)

大多数情况下,显式地使用带有这些中缀的 Set 对象是不必要的。所有中缀运算符都将处理 Any 类型的任何对象的参数(例如, List),ArrayMix 等)并强制他们到在需要的地方设置。

在某些情况下,如果参数的类型是 Bag,则中缀运算符将以与其行为方式不同但类似的方式运行 Set 参数。

31.1.1. Operators that return Bool

下面的运算符都是 “Chaining infix” 的优先级。

infix (elem)
multi sub infix:<(elem)>($a, Any $b --> Bool)
multi sub infix:<(elem)>($a, Set $b --> Bool)

成员运算符。

返回i True 如果 $a$b 中的一个元素

say 2 (elem) (1, 2, 3).Set;              # OUTPUT: «True␤»
say 4 (elem) (1, 2, 3).Set;              # OUTPUT: «False␤»
infix ∈
only sub infix:<∈>($a, $b --> Bool)

另一个成员运算符。

等价于 (elem)), 在代码点 U+2208 (ELEMENT OF).

infix ∉
only sub infix:<∉>($a, $b --> Bool)

非成员运算符。

等价于 !(elem), 例如, 返回 True 如果 $a 不是 $b 中的元素, 在代码点 U+2209 (NOT AN ELEMENT OF).

say 2 !(elem) (1, 2, 3).Set;             # OUTPUT: «False␤»
say 4 !(elem) (1, 2, 3).Set;             # OUTPUT: «True␤»
infix (cont)
multi sub infix:<(cont)>(Any $a, $b --> Bool)
multi sub infix:<(cont)>(Set $a, $b --> Bool)

包含运算符。

返回 True 如果 $a contains $b 作为一个元素。

say (1, 2, 3).Set (cont) 2;              # OUTPUT: «True␤»
say (1, 2, 3).Set (cont) 4;              # OUTPUT: «False␤»
infix ∋
only sub infix:<∋>($a, $b --> Bool)

另一个包含运算符。

等价于 (cont)), 在代码点 U+220B (CONTAINS AS MEMBER).

infix ∌
only sub infix:<∌>($a, $b --> Bool)

不包含运算符。

等价于 !(cont), 例如, 返回 True 如果 $a 不包含 $b, 在代码点 U+220C (DOES NOT CONTAIN AS MEMBER).

say (1, 2, 3).Set !(cont) 2;             # OUTPUT: «False␤»
say (1, 2, 3).Set !(cont) 4;             # OUTPUT: «True␤»
infix (⇐)
multi sub infix:<<(<=)>>(Any $a, Any $b --> Bool)
multi sub infix:<<(<=)>>(Setty $a, Setty $b --> Bool)

子集或相等运算符。

返回 True 如果 $a$b 的一个子集(subset) 或 $a$b 相等, 例如, 如果 $a 中所有的元素都是 $b 中的元素, 且 $a 的大小小于或等于 $b 的大小。

say (1, 2, 3).Set (<=) (3, 2, 1).Set;    # OUTPUT: «True␤»
say (1, 3).Set (<=) (2, 1).Set;          # OUTPUT: «False␤»
say ∅ (<=) (3, 2, 1).Set;                # OUTPUT: «True␤»
infix ⊆
only sub infix:<⊆>($a, $b --> Bool)

另一个子集或相等运算符。

等价于 (⇐)), 在代码点 U+2286 (SUBSET OF OR EQUAL TO).

infix ⊈
only sub infix:<⊈>($a, $b --> Bool)

既不是子集运算符也不是相等运算符。

等价于 !(⇐), 在代码点 U+2288 (NEITHER A SUBSET OF NOR EQUAL TO).

say (1, 2, 3).Set !(<=) (3, 2, 1).Set;   # OUTPUT: «False␤»
say (1, 3).Set ⊈ (2, 1).Set;             # OUTPUT: «True␤»
infix (<)
multi sub infix:<<(<)>>(Any $a, Any $b --> Bool)
multi sub infix:<<(<)>>(Setty $a, Setty $b --> Bool)

子集运算符。

返回 True 如果 $a$b 的一个真子集(strict subset), 例如, $a 中的所有元素都是 $b 中的元素, 但是 $a 的大小比 $b 的大小要小。

say (1, 2, 3).Set (<) (3, 2, 1).Set;     # OUTPUT: «False␤»
say (1, 3).Set (<) (3, 2, 1).Set;        # OUTPUT: «True␤»
say ∅ (<) (3, 2, 1).Set;                 # OUTPUT: «True␤»
infix ⊂
only sub infix:<⊂>($a, $b --> Bool)

另一个子集运算符。

等价于 (<)), 在代码点 U+2282 (SUBSET OF).

infix ⊄
only sub infix:<⊄>($a, $b --> Bool)

非子集运算符。

等价于 !(<), 在代码点 U+2284 (NOT A SUBSET OF).

say (1, 2, 3).Set !(<) (3, 2, 1).Set;    # OUTPUT: «True␤»
say (1, 3).Set ⊄ (3, 2, 1).Set;          # OUTPUT: «False␤»
infix (>=)
multi sub infix:<<(>=)>>(Any $a, Any $b --> Bool)
multi sub infix:<<(>=)>>(Setty $a, Setty $b --> Bool)

超集或相等运算符。

(⇐)) 但是翻转参数。 返回 True 如果 $a$b超集(superset) 或与 $b 相等。

say (1, 2, 3).Set (>=) (3, 2, 1).Set;    # OUTPUT: «True␤»
say (1, 3).Set (>=) (3, 2, 1).Set;       # OUTPUT: «False␤»
say ∅ (>=) (3, 2, 1).Set;                # OUTPUT: «False␤»
infix ⊇
only sub infix:<⊇>($a, $b --> Bool)

另一个超集或集合相等运算符。

等价于 (>=)), 在代码点 U+2287 (SUPERSET OF OR EQUAL TO).

infix ⊉
only sub infix:<⊉>($a, $b --> Bool)

既不是超集运算符, 也非集合相等运算符。

等价于 !(>=), 在代码点 U+2289 (NEITHER A SUPERSET OF NOR EQUAL TO).

say (1, 2, 3).Set !(>=) (3, 2, 1).Set;   # OUTPUT: «False␤»
say (1, 3).Set ⊉ (3, 2, 1).Set;          # OUTPUT: «True␤»
infix (>)
multi sub infix:<<(>)>>(Any $a, Any $b --> Bool)
multi sub infix:<<(>)>>(Setty $a, Setty $b --> Bool)

超集运算符。

(<)) 但是反转参数。返回 True 如果 $a$b 的一个严格超集(strict superset)。

say (1, 2, 3, 4).Set (>) (3, 2, 1).Set;  # OUTPUT: «True␤»
say (1, 3).Set (>) (3, 2, 1).Set;        # OUTPUT: «False␤»
say ∅ (>) (3, 2, 1).Set;                 # OUTPUT: «False␤»
infix ⊃
only sub infix:<⊃>($a, $b --> Bool)

另一个超集运算符。

等价于 (>)), 在代码点 U+2283 (SUPERSET OF).

infix ⊅
only sub infix:<⊅>($a, $b --> Bool)

非超集运算符。

等价于`!(>)`, 在代码点 U+2285 (NOT A SUPERSET OF).

say (1, 2, 3, 4).Set !(>) (3, 2, 1).Set; # OUTPUT: «False␤»
say (1, 3).Set ⊅ (3, 2, 1).Set;          # OUTPUT: «True␤»

31.1.2. Operators that return Set or Bag

infix (|)
only sub infix:<(|)>(**@p)

并集运算符。 它的优先级是 "Junctive Or".

返回它所有参数的并集。通常, 这创建一个新的包含参数的所有元素的集合:

<a a b c d> (|) <h g f e d c> (|) <i j> === set <a b c d e f g h i j>

如果它的任何参数是 “Baggy”,它会创建一个新的 “Bag”,其中包含参数的所有元素,每个元素都按照该元素出现的最高权重进行加权。

bag(<a a b c a>) (|) bag(<a a b c c>) === bag(<a a a b c c>)
infix ∪
only sub infix:<∪>(|p)

另一个并集运算符。它的优先级是 "Junctive or".

等价于 (|)), 在代码点 U+222A (UNION).

infix (&)
only sub infix:<(&)>(**@p)

交集运算符。它的优先级是 "Junctive and".

返回它所有参数的交集。通常, 这创建一个包含所有参数都共有的元素的新的集合。

<a b c> (&) <b c d> === set <b c>
<a b c d> (&) <b c d e> (&) <c d e f> === set <c d>

如果任何参数是 “Baggy”,则结果是一个包含公共元素的新 “Bag”,每个元素都由最大*共同*权重(这是所有参数中该元素的权重的最小值)加权。

bag(<a a b c a>) (&) bag(<a a b c c>) === bag(<a a b c>)
infix ∩
only sub infix:<∩>(|p)

另一个交集运算符。它的优先级是 "Junctive and".

等价于 (&)), 在代码点 U+2229 (INTERSECTION).

infix (-)
only sub infix:<(-)>(**@p)

差集运算符。它优先于“Junctive or”。

返回其所有参数的差集。通常,这将返回由第一个参数具有的所有元素组成的 “Set”,但不包括其余元素,即第一个参数的所有元素,减去其他参数中的元素。

如果第一个参数是 “Baggy”,则返回一个 “Bag”,其中包含第一个参数的每个元素,其权重减去每个其他参数中该元素的权重。

bag(<a a b c a d>) (-) bag(<a a b c c>) === bag(<a d>)
bag(<a a a a c d d d>) (-) bag(<a b d a>) (-) bag(<d c>) === bag(<a a d>)
infix ∖
only sub infix:<<"\x2216">>(|p)

另一个差集运算符. 它的优先级是 "Junctive or".

等价于 (-)).

infix (^)
multi sub infix:<(^)>(Any $a, Any $b --> Setty)
multi sub infix:<(^)>(Set $a, Set $b --> Setty)

对称差集运算符。 它的优先级是 “Junctive or“。

返回所有参数的对称差集,即 Set$a 所有的元素组成,但 $b 没有,所有元素 $b 都有但是 $a 没有。 相当于 ($a ∖ $b) ∪ ($b ∖ $a)

infix ⊖
only sub infix:<⊖>($a, $b --> Setty)

另一个对称差集运算符。它的优先级是 "Junctive or".

等价于 (^)), 在代码点 U+2296 (CIRCLED MINUS).

infix (.)
only sub infix:<(.)>(**@p)

Baggy 乘法运算符。它的优先级是 “Junctive and”。

返回其参数的 Baggy 倍数,即 Bag 包含参数的每个元素,其中参数的元素权重相乘以获得新的权重。

<a b c> (.) <a b c d> === bag <a b c> # Since 1 * 0 == 0, in the case of 'd'
bag(<a a b c a d>) (.) bag(<a a b c c>) === ("a"=>6,"c"=>2,"b"=>1).Bag
infix ⊍
only sub infix:<⊍>(|p)

另一个 baggy 乘法运算符。 它的优先级是 "Junctive and".

等价于infix (.)), 在代码点 U+228D (MULTISET MULTIPLICATION).

infix (+)
only sub infix:<(+)>(**@p)

Baggy 加法运算符。它的优先级是 “Junctive or”。

返回其参数的 Baggy 加法,即包含参数的每个元素,其中参数的权重加在一起以获得新的权重。

bag(<a a b c a d>) (+) bag(<a a b c c>) === ("a"=>5,"c"=>3,"b"=>2,"d"=>1).Bag
infix ⊎
only sub infix:<⊎>(|p)

另一个 baggy 加法 operator。 它的优先级是 "Junctive or".

等价于 link:https://docs.raku.org/routine/([()]), 在代码点 U+228E (MULTISET UNION).

term ∅

等价于 set(), 即空集, 在代码点 U+2205 (EMPTY SET).

32. 语法

Raku 借用了人类语言中的许多概念。考虑到它是由语言学家设计的,这并不奇怪。

它重用不同语境中的共同元素,具有名词(术语)和动词(运算符)的概念,是上下文敏感的(在日常意义上,不一定在计算机科学解释中),因此符号可以具有不同的含义取决于名词或动词是否是预期的。

它也是自同步的,因此解析器可以检测大多数常见错误并提供良好的错误消息。

32.1. 词法约定

Raku 代码是 Unicode 文本。当前的实现支持 UTF-8 作为输入编码。

32.1.1. 自由形式

Raku 代码也是自由格式的,从某种意义上说,你可以自由选择你使用的空格量,尽管在某些情况下,空格的存在与否具有意义。

所以你可以写

if True {
    say "Hello";
}

    if True {
say "Hello"; # Bad indentation intended
        }

if True { say "Hello" }

或者甚至

if True {say "Hello"}

虽然你不能省略任何剩余的空白。

32.1.2. Unspace

在编译器不允许空格的许多地方,只要用反斜杠引用,就可以使用任意数量的空格。不支持 token 中的空格。当编译器生成行号时,未空格的换行仍然计算。用于非空格的用例是后缀运算符和例程参数列表的分离。

sub alignment(+@l) { +@l };
sub long-name-alignment(+@l) { +@l };
alignment\         (1,2,3,4).say;
long-name-alignment(3,5)\   .say;
say Inf+Inf\i;

在这种情况下,我们的目的是让 . 两个语句以及括号都对齐,所以我们在用于填充的空格之前加上 \

32.1.3. 用分号分割语句

Raku 程序是一组由分号 ; 分割的语句。

say "Hello";
say "world";

最后一个语句之后(或在块内的最终语句之后)的分号是可选的。

say "Hello";
say "world"
if True {
    say "Hello"
}
say "world"

32.1.4. 隐式分隔符规则(对于以块结尾的语句)

以裸块结尾的完整语句可以省略尾随分号,如果同一行上没有其他语句跟随块的结束大括号 }。 这称为“隐式分隔符规则”。例如,您不需要在 if 语句块之后写一个分号,如上所示,以及下面所示。

if True { say "Hello" }
say "world";

但是,需要使用分号将块与同一行中的尾随语句分开。

if True { say "Hello" }; say "world";
#                     ^^^ this ; is required

此隐式语句分隔符规则除了控制语句之外还以其他方式应用,可能以裸块结束。例如,结合冒号:方法调用的语法。

my @names = <Foo Bar Baz>;
my @upper-case-names = @names.map: { .uc }    # OUTPUT: [FOO BAR BAZ]

对于属于同一 if/elsif/else(或类似)构造的一系列块,隐式分隔符规则仅适用于该系列的最后一个块的末尾。这三个是等价的:

if True { say "Hello" } else { say "Goodbye" }; say "world";
#                                            ^^^ this ; is required
if True { say "Hello" } else { say "Goodbye" } # <- implied statement separator
say "world";
if True { say "Hello" }   # still in the middle of an if/else statement
else    { say "Goodbye" } # <- no semicolon required because it ends in a block
                          #    without trailing statements in the same line
say "world";

32.1.5. 注释

注释是程序文本的一部分,仅供人类读者阅读; Raku 编译器不会将它们当作程序文本。

在缺少或存在空白消除可能的解析的地方,注释计为空格。

单行注释

Raku 中最常见的注释形式以单个哈希字符 # 开头,直到该行的结尾。

if $age > 250 {     # catch obvious outliers
    # this is another comment!
    die "That doesn't look right"
}
多行 / 嵌套注释

多行和嵌入式注释以井号字符开头,然后是反引号,然后是一些开口括号字符,并以匹配的闭合括号字符结束。内容不仅可以跨越多行,还可以嵌入内联。

if #`( why would I ever write an inline comment here? ) True {
    say "something stupid";
}

这些注释可以扩展到多行

#`[
And this is how a multi would work.
That says why we do what we do below.
]
say "No more";

注释中的大括号可以嵌套,因此在 #`{ a { b } c }, 中,注释一直持续到字符串的最后。 您也可以使用多个花括号,例如 #`{{ double-curly-brace }},这可能有助于消除嵌套分隔符的歧义。 您可以在表达式中嵌入这些注释,只要不将它们插入关键字或标识符的中间即可。

Pod 注释

Pod 语法可用于多行注释

say "this is code";

=begin comment

Here are several
lines
of comment

=end comment

say 'code again';

32.1.6. 标识符

标识符是语法构建块,可用于为实体/对象赋予名称,例如常量,变量(例如标量)和例程(例如,Subs 和方法)。在变量名中,任何sigil(和twigil)都在标识符之前,并且不形成其一部分。

constant c = 299792458;     # identifier "c" names an Int
my $a = 123;                # identifier "a" in the name "$a" of a Scalar
sub hello { say "Hello!" }; # identifier "hello" names a Sub

标识符有不同的形式:普通标识符,扩展标识符和复合标识符。

普通标识符

普通标识符由前导字母字符组成,后面可以跟着一个或多个字母数字字符。它也可能包含单独的,嵌入的撇号 ' 和/或连字符 -, 前提是下一个字符每次都是字母。

“字母”和“字母数字”的定义包括适当的 Unicode 字符。哪些字符“合适”取决于实现。在 Rakudo/MoarVM Raku 实现中,字母字符包括具有 Unicode 通用类别值 Letter(L) 和下划线 _ 的字符。字母数字字符还包括具有 Unicode 通用类别值编号,十进制数字(Nd) 的字符。

# valid ordinary identifiers:
x
_snake_oil
something-longer
with-numbers1234
don't-do-that
piece_of_π
駱駝道              # "Rakuda-dō", Japanese for "Way of the camel"
# invalid ordinary identifiers:
42                 # identifier does not start with alphabetic character
with-numbers1234-5 # embedded hyphen not followed by alphabetic character
is-prime?          # question mark is not alphanumeric
x²                 # superscript 2 is not alphanumeric (explained above)
扩展标识符

使名称包含普通标识符中不允许的字符通常很方便。用例包括一组实体共享一个共同的“短”名称但仍需要单独识别其每个元素的情况。例如,您可以使用短名称为 Dog 的模块,而其长名称包括其命名所有权和版本号:

Dog:auth<Somebody>:ver<1.0>  # long module names including author and version
Dog:auth<Somebody>:ver<2.0>

use Dog:auth<Somebody>:ver<2.0>;
# Selection of second module causes its full name to be aliased to the
# short name for the rest of # the lexical scope, allowing a declaration
# like this.
my Dog $spot .= new("woof");

类似地,运算符集在各种语法类别中一起工作,其名称如 prefix,infix 和 postfix。这些运算符的官方名称通常包含从普通标识符中排除的字符。长名称是扩展标识符的构成,包括这个句法类别;短名称将包含在定义中的引号中:

infix:<+>                 # the official name of the operator in $a + $b
infix:<*>                 # the official name of the operator in $a * $b
infix:«<=»                # the official name of the operator in $a <= $b

对于所有此类用途,您可以将一个或多个冒号分隔的字符串附加到普通标识符,以创建所谓的扩展标识符。 附加到标识符(即后缀位置)时,此冒号分隔的字符串会生成该标识符的唯一变体。

这些字符串的格式为 :key<value>,其中 key 或 value 是可选的; 也就是说,在将它与常规标识符分开的冒号之后,将存在一个键和/或引用包围结构,例如 <>,«» 或 [' '],它引用一个或多个任意字符值。[1]

# exemplary valid extended identifiers:
postfix:<²>               # the official long name of the operator in $x²
WOW:That'sAwesome
WOW:That's<<🆒>>
party:sweet(16)

# exemplary invalid extended identifiers:
party:16<sweet>           # 16 is not an ordinary identifier
party:16sweet
party:!a                  # ...and neither is !a
party:$a                  # ...nor $a

在扩展标识符中,后缀字符串被视为名称的组成部分,因此 infix:<+>infix:<→ 是两个不同的运算符。但是,使用的包围字符不算作其中的一部分;只有引用的数据很重要。所以这些都是同一个名字:

infix:<+>
infix:<<+>>
infix:«+»
infix:['+']
infix:('+')

同样,所有这些都有效:

my $foo:bar<baz> = 'quux';
say $foo:bar«baz»;                               # OUTPUT: «quux␤»
my $take-me:<home> = 'Where the glory has no end';
say $take-me:['home'];                           # OUTPUT: «Where [...]␤»
my $foo:bar<2> = 5;
say $foo:bar(1+1);                               # OUTPUT: «5␤»

如果扩展标识符包含两个或更多个冒号对,则它们的顺序通常很重要:

my $a:b<c>:d<e> = 100;
my $a:d<e>:b<c> = 200;
say $a:b<c>:d<e>;               # OUTPUT: «100␤», NOT: «200␤»

此规则的一个例外是模块版本控制;所以这些标识符有效地命名相同的模块:

use ThatModule:auth<Somebody>:ver<2.7.18.28.18>
use ThatModule:ver<2.7.18.28.18>:auth<Somebody>

此外,扩展标识符支持编译时插值;这需要使用常量作为插值:

constant $c = 42;  # Constant binds to Int; $-sigil enables interpolation
my $a:foo<42> = "answer";
say $a:foo«$c»;    # OUTPUT: «answer␤»

虽然引用包围结构在标识符的上下文中通常是可互换的,但它们并不相同。特别是,尖括号 <>(模仿单引号插值特征)不能用于常量名称的插值。

constant $what = 'are';
my @we:<are>= <the champions>;
say @we:«$what»;     # OUTPUT: «[the champions]␤»
say @we:<$what>;
# Compilation error: Variable '@we:<$what>' is not declared
组合标识符

复合标识符是由两个或多个普通和/或扩展标识符组成的标识符,这些标识符通过双冒号 :: 彼此分开。

双冒号 :: 被称为命名空间分隔符或包分隔符,它在名称中阐明了它的语义功能:强制将名称的前一部分视为包名/命名空间,名称的后续部分通过该包/命名空间位于:

module MyModule {               # declare a module package
    our $var = "Hello";         # declare package-scoped variable
}
say $MyModule::var              # OUTPUT: «Hello␤»

在上面的示例中,MyModule::var 是一个复合标识符,由包名称标识符 MyModule 和变量名称 var 的标识符部分组成。加在一块, $MyModule::var 通常被称为包限定名。

使用双冒号分隔标识符会导致最右边的名称插入到现有包(参见上面的示例)或自动创建的包中:

my $foo::bar = 1;
say OUR::.keys;           # OUTPUT: «(foo)␤»
say OUR::foo.HOW          # OUTPUT: «Raku::Metamodel::PackageHOW.new␤»

最后几行显示了如何自动创建 foo 包,作为该命名空间中变量的存放。

双冒号语法允许使用 ::($expr) 将字符串运行时插入到包或变量名中,您通常会在其中放置包或变量名:

my $buz = "quux";
my $bur::quux = 7;
say $bur::($buz);               # OUTPUT: «7␤»

32.1.7. 项 term:<>

您可以使用 term:<> 来引入新的项,这对于引入违反常规标识符规则的常量非常方便:

use Test; plan 1; constant &term:<👍> = &ok.assuming(True);
👍
# OUTPUT: «1..1␤ok 1 - ␤»

但是项不必是常量:您也可以将它们用于不带任何参数的函数,并强制解析器在它们之后期望运算符。例如:

sub term:<dice> { (1..6).pick };
say dice + dice;

可以打印 2 到 12 之间的任何数字。

相反,我们已经声明 dice 为常规子例程

sub dice() {(1...6).pick }

表达式 dice + dice 将被解析为 dice(+(dice())),导致错误,因为子 dice 需要零个参数。

32.2. 语句和表达式

Raku 程序由一组组成。语句的一个特例是表达式,它返回一个值。例如,` if True { say 42 }` 在语法上是一个语句,而不是一个表达式,而 1 + 2 是一个表达式(因此也是一个语句)。

do 前缀将语句转换为表达式。所以虽然

my $x = if True { 42 };     # Syntax error!

是一个错误,

my $x = do if True { 42 };

if 语句(此处为 42)的返回值赋给变量 $x

32.3. 项

项是基本名词,可选地与运算符一起形成表达式。项的示例是变量($x),诸如类型名称(Int),字面量(42),声明(sub f() { })和调用(f())之类的裸字。

例如,在表达式 2 * $salary 中,2$salary 是两个项(整数字面量和变量)。

32.3.1. 变量

变量通常以称为 sigil 的特殊字符开头,后跟一个标识符。必须先声明变量才能使用它们。

# declaration:
my $number = 21;
# usage:
say $number * 2;

有关更多详细信息,请参阅变量文档。

32.3.2. 裸字 (常量,类型名)

预先声明的标识符可以是它们自己的术语。这些通常是类型名称或常量,但也是术语 self,它指的是调用方法的对象(请参阅对象)和无符号变量:

say Int;                # OUTPUT: «(Int)␤»
#   ^^^ type name (built in)

constant answer = 42;
say answer;
#   ^^^^^^ constant

class Foo {
    method type-name {
        self.^name;
      # ^^^^ built-in term 'self'
    }
}
say Foo.type-name;     # OUTPUT: «Foo␤»
#   ^^^ type name

32.3.3. 包和限定名

命名实体(如变量,常量,类,模块或子)是命名空间的一部分。名称的嵌套部分使用 :: 来分隔层次结构。一些例子:

$foo                # simple identifiers
$Foo::Bar::baz      # compound identifiers separated by ::
$Foo::($bar)::baz   # compound identifiers that perform interpolations
Foo::Bar::bob(23)   # function invocation given qualified name

有关更多详细信息,请参阅包中的文档。

32.3.4. 字面量

字面量是源代码中常量值的表示。 Raku 具有几种内置类型的字面量,如字符串,几种数字类型,pair 对儿等等。

字符串字面量

字符串字面量用引号括起来:

say 'a string literal';
say "a string literal\nthat interprets escape sequences";

请参阅引用以获取更多选项,包括转义引用 q。 Raku 在字面量中使用标准转义字符 \a \b \t \n \f \r \e, 与设计文档中指定的 ASCII 转义码具有相同的含义。

say "🔔\a";  # OUTPUT: «🔔␇␤»
数字字面量

数字字面量通常用十进制表示(除非前缀为 0x(十六进制,基数为16),0o(八进制,基数为8)或 0b(二进制,基数为2),否则可以通过前缀0d逐字地使用(如果需要,可以使用前缀 0d)。 )或状语符号中的显式基数,如 :16<A0> 另有说明。与其他编程语言不同,前导零不表示基数 8;而是发出编译时警告。

在所有字面量格式中,你可以使用下划线来分组数字;他们没有任何语义信息;以下字面量都计算为相同的数字:

1000000
1_000_000
10_00000
100_00_00
Int 字面量

整数默认为有符号十进制的,但您可以使用其他基数。有关详细信息,请参阅 Int。

# actually not a literal, but unary - operator applied to numeric literal 2
-2
12345
0xBEEF      # base 16
0o755       # base 8
:3<1201>    # arbitrary base, here base 3
Rat 字面量

Rat 字面量(有理数)非常常见,取代许多其他语言中的小数或浮点数。整除也会产生 Rat。

1.0
3.14159
-2.5        # Not actually a literal, but still a Rat
:3<21.0012> # Base 3 rational
⅔
2/3         # Not actually a literal, but still a Rat
Num 字面量

e 产生浮点数后,使用整数指数到十进制的科学记数法:

1e0
6.022e23
1e-9
-2e48
2e2.5       # error
Complex 字面量

复数可以写为虚数(只是附加后缀 i 的有理数),也可以是实数和虚数之和:

1+2i
6.123e5i    # note that this is 6.123e5 * i, not 6.123 * 10 ** (5i)
Pair 字面量

对由键和值组成,构造它们有两种基本形式:key ⇒ 'value':key('value')

Arrow pairs

箭头对可以有一个表达式,一个字符串字面量或一个“裸标识符”,这是一个普通标识符语法的字符串,左侧不需要引号:

like-an-identifier-ain't-it => 42
"key" => 42
('a' ~ 'b') => 1
副词对儿 (colon pairs)

没有显式值的简短形式:

my $thing = 42;
:$thing                 # same as  thing => $thing
:thing                  # same as  thing => True
:!thing                 # same as  thing => False

变量形式也适用于其他符号,例如::&callback:@elements。如果值是数字字面量,它也可以用这种简短形式表示:

:42thing            # same as  thing => 42
:٤٢thing            # same as  thing => 42

如果您使用其他字母,则此顺序将被反转:

:٤٢ث              # same as   ث => ٤٢

thaa 字母在数字之前。

具有显式值的长形式:

:thing($value)              # same as  thing => $value
:thing<quoted list>         # same as  thing => <quoted list>
:thing['some', 'values']    # same as  thing => ['some', 'values']
:thing{a => 'b'}            # same as  thing => { a => 'b' }
Boolean 字面量

True 和 False 是 Boolean 字面量; 他们始终是首字母大写的。

Array 字面量

一对方括号可以围绕表达式以形成逐项数组字面量; 通常在里面有一个以逗号分隔的列表:

say ['a', 'b', 42].join(' ');   # OUTPUT: «a b 42␤»
#   ^^^^^^^^^^^^^^ Array constructor

如果构造函数被赋予单个 Iterable,它将克隆并展平它。如果你想要一个只有 1 个 Iterable 元素的数组,请确保在它之后使用逗号:

my @a = 1, 2;
say [@a].perl;  # OUTPUT: «[1, 2]␤»
say [@a,].perl; # OUTPUT: «[[1, 2],]␤»

Array 构造函数不会展平其他类型的内容。使用 Slip 前缀运算符(|)展平所需项:

my @a = 1, 2;
say [@a, 3, 4].perl;  # OUTPUT: «[[1, 2], 3, 4]␤»
say [|@a, 3, 4].perl; # OUTPUT: «[1, 2, 3, 4]␤»
Hash 字面量

一个前导的关联符号和一对括号 %( ) 可以包围一对列表以形成一个哈希字面量; 通常在里面有一个以逗号分隔的 Pairs 列表。如果使用非 pair 对,则假定它是一个键,下一个元素是值。大多数情况下,它与简单的箭头对一起使用。

say %( a => 3, b => 23, :foo, :dog<cat>, "french", "fries" );
# OUTPUT: «a => 3, b => 23, dog => cat, foo => True, french => fries␤»

say %(a => 73, foo => "fish").keys.join(" ");   # OUTPUT: «a foo␤»
#   ^^^^^^^^^^^^^^^^^^^^^^^^^ Hash constructor

当赋值给左侧的 % sigiled 变量时,右侧 Pairs 周围的符号和括号是可选的。

my %ages = fred => 23, jean => 87, ann => 4;

默认情况下, %( ) 中的键被强制为字符串。要使用非字符串键组合散列,请使用带有冒号前缀的花括号分隔符 :{}

my $when = :{ (now) => "Instant", (DateTime.now) => "DateTime" };

请注意,将对象作为键,您不能将非字符串键作为字符串访问:

say :{ -1 => 41, 0 => 42, 1 => 43 }<0>;  # OUTPUT: «(Any)␤»
say :{ -1 => 41, 0 => 42, 1 => 43 }{0};  # OUTPUT: «42␤»
Regex 字面量

使用 /foo/ 等斜杠声明正则表达式。请注意,此 // 语法是完整的 rx// 语法的简写。

/foo/          # Short version
rx/foo/        # Longer version
Q :regex /foo/ # Even longer version

my $r = /foo/; # Regexes can be assigned to variables
签名字面量

除了 sub 和块声明中的典型用法之外,签名可以单独用于模式匹配。从冒号开始声明独立签名:

say "match!" if 5, "fish" ~~ :(Int, Str); # OUTPUT: «match!␤»

my $sig = :(Int $a, Str);
say "match!" if (5, "fish") ~~ $sig; # OUTPUT: «match!␤»

given "foo", 42 {
  when :(Str, Str) { "This won't match" }
  when :(Str, Int $n where $n > 20) { "This will!" }
}

有关签名的更多信息,请参阅签名文档。

32.3.5. 声明

变量声明
my $x;                          # simple lexical variable
my $x = 7;                      # initialize the variable
my Int $x = 7;                  # declare the type
my Int:D $x = 7;                # specify that the value must be defined (not undef)
my Int $x where { $_ > 3 } = 7; # constrain the value based on a function
my Int $x where * > 3 = 7;      # same constraint, but using Whatever shorthand

有关其他作用域的更多详细信息,请参阅变量声明符和作用域(ourhas)。

子例程声明
# The signature is optional
sub foo { say "Hello!" }

sub say-hello($to-whom) { say "Hello $to-whom!" }

您还可以将子例程赋值给变量。

my &f = sub { say "Hello!" } # Un-named sub
my &f = -> { say "Hello!" }  # Lambda style syntax. The & sigil indicates the variable holds a function
my $f = -> { say "Hello!" }  # Functions can also be put into scalars
包, 模块, 类, 角色 和 Grammar 声明

有几种类型的包,每种类型都使用关键字,名称,一些可选特征以及子例程,方法或规则体声明。

package P { }

module M { }

class C { }

role R { }

grammar G { }

可以在单个文件中声明多个包。但是,您可以在文件的开头声明一个单元包(仅在注释或 use 语句之前),并且该文件的其余部分将被视为包的主体。在这种情况下,不需要花括号。

unit module M;
# ... stuff goes here instead of in {}'s
多重分派的声明

另请参见多重分派。

可以使用多个签名声明同名子例程。

multi sub foo() { say "Hello!" }
multi sub foo($name) { say "Hello $name!" }

在类里面, 你还可以声明多重分派方法。

multi method greet { }
multi method greet(Str $name) { }

32.3.6. 子例程调用

子程序使用关键字 sub 创建,后跟可选名称,可选签名和代码块。子例程是词法作用域的,因此如果在声明时指定了名称,则可以在词法作用域中使用相同的名称来调用子例程。子例程是 Sub 类型的实例,可以赋值给任何容器。

foo;   # Invoke the function foo with no arguments
foo(); # Invoke the function foo with no arguments
&f();  # Invoke &f, which contains a function
&f.(); # Same as above, needed to make the following work
my @functions = ({say 1}, {say 2}, {say 3});
@functions>>.(); # hyper method call operator

当在类中声明时,子例程被命名为“方法”:方法是针对对象(即,类实例)调用的子例程。在方法中,特殊变量 self 包含对象实例(请参阅方法)。

# Method invocation. Object (instance) is $person, method is set-name-age
$person.set-name-age('jane', 98);   # Most common way
$person.set-name-age: 'jane', 98;   # Precedence drop
set-name-age($person: 'jane', 98);  # Invocant marker
set-name-age $person: 'jane', 98;   # Indirect invocation

有关更多信息,请参阅函数。

优先级下降

在方法调用的情况下(即,在针对类实例调用子例程时),可以应用由冒号标识的优先级下降:在方法名称之后和参数列表之前。参数列表优先于方法调用,另一方面“降低”其优先级。为了更好地理解,请考虑以下简单示例(仅添加额外空格以对齐方法调用):

my $band = 'Foo Fighters';
say $band.substr( 0, 3 ) .substr( 0, 1 ); # F
say $band.substr: 0, 3   .substr( 0, 1 ); # Foo

在第二种方法调用中,最右边的 substr 应用于“3”,而不是最左边的 substr 的结果,另一方面,它产生优先级最右边的 substr。

32.3.7. 运算符

有关详细信息,请参阅运算符。

运算符是具有更多符号重和可组合语法的函数。与其他函数一样,运算符可以进行多重分派以允许特定于上下文的使用。

运算符有五种类型(排列),每种类型都有一个或两个参数。

++$x           # prefix, operator comes before single input
5 + 3          # infix, operator is between two inputs
$x++           # postfix, operator is after single input
<the blue sky> # circumfix, operator surrounds single input
%foo<bar>      # postcircumfix, operator comes after first input and surrounds second
元运算符

运算符可以组合。一个常见的例子是将中缀(二元)运算符与赋值相结合。您可以将赋值与任何二元运算符组合。

$x += 5     # Adds 5 to $x, same as $x = $x + 5
$x min= 3   # Sets $x to the smaller of $x and 3, same as $x = $x min 3
$x .= child # Equivalent to $x = $x.child

[ ] 中包装中缀运算符以创建一个新的化简运算符,该运算符在单个输入列表上工作,从而产生单个值。

say [+] <1 2 3 4 5>;    # OUTPUT: «15␤»
(((1 + 2) + 3) + 4) + 5 # equivalent expanded version

« »(或等效的 ASCII)包装一个中缀运算符,以创建一个在两个列表上成对工作的新超运算符。

say <1 2 3> «+» <4 5 6> # OUTPUT: «(5 7 9)␤»

箭头的方向表示当列表的大小不同时该怎么做。

@a «+« @b # Result is the size of @b, elements from @a will be re-used
@a »+» @b # Result is the size of @a, elements from @b will be re-used
@a «+» @b # Result is the size of the biggest input, the smaller one is re-used
@a »+« @b # Exception if @a and @b are different sizes

您还可以使用超运算符包装一元运算符。

say -« <1 2 3> # OUTPUT: «(-1 -2 -3)␤»

33. 容器

本节介绍了处理变量和容器元素时所涉及的间接级别。解释了 Raku 中使用的容器的不同类型,以及适用于它们的操作,如赋值,绑定和展平。最后讨论了更多高级主题,如自引用数据,类型约束和自定义容器。

33.1. 变量是什么?

有些人喜欢说“一切都是对象”,但实际上在 Raku 中变量不是对用户暴露的对象。

当编译器遇到类似 my $x 的变量声明时,它会将其注册到某个内部符号表中。此内部符号表用于检测未声明的变量,并将变量的代码生成与正确的作用域联系起来。

在运行时,变量显示为*词法板中*的条目,或*简称*为*lexpad*。这是一个每个作用域的数据结构,它存储每个变量的指针。

my $x 这种情况下,变量的 $x 的 lexpad 条目是指向 Scalar 类型对象的指针,通常称为*容器*。

33.2. 标量容器

虽然 Scalar 类型的对象在 Raku 中无处不在,但您很少直接将它们视为对象,因为大多数操作*都是去容器化的*,这意味着它们会对 Scalar 容器的内容而不是容器本身起作用。

在这样的代码中:

my $x = 42;
say $x;

赋值 $x = 42 在标量容器中存储指向 Int 对象 42 的指针,lexpad 条目 $x 指向该标量容器。

赋值运算符要求左侧的容器将值存储在其右侧。究竟是什么意思取决于容器类型。因为 Scalar 它意味着“用新的值替换先前存储的值”。

请注意,子例程签名允许传递容器:

sub f($a is rw) {
    $a = 23;
}
my $x = 42;
f($x);
say $x;         # OUTPUT: «23»

在子例程内部,lexpad 条目 $a 指向 $x 指向子例程外部的同一容器。这就是为什么给 $a 赋值也修改了 $x 的内容。

同样,例程可以返回容器,如果它被标记为 is rw

my $x = 23;
sub f() is rw { $x };
f() = 42;
say $x;         # OUTPUT: «42»

对于显式返回,必须使用 return-rw 而不是 return

返回容器是 is rw 属性访问器的工作方式。所以:

class A {
    has $.attr is rw;
}

相当于

class A {
    has $!attr;
    method attr() is rw { $!attr }
}

标量容器对类型检查和大多数只读访问都是透明的。.VAR 使它们可见:

my $x = 42;
say $x.^name;       # OUTPUT: «Int»
say $x.VAR.^name;   # OUTPUT: «Scalar»

并且参数上的 is rw 需要存在可写的 Scalar 容器:

sub f($x is rw) { say $x };
f 42;
CATCH { default { say .^name, ': ', .Str } };
# OUTPUT: «X::Parameter::RW: Parameter '$x' expected a writable container, but got Int value»

33.3. Callable 容器

可调用容器在 Routine 调用语法和存储在容器中的对象的 CALL-ME 方法的实际调用之间提供了桥梁。声明容器时需要使用符号 & ,执行时必须省略 Callable。默认类型约束是 Callable

my &callable = -> $ν { say "$ν is", $ν ~~ Int??" whole"!!" not whole" }
callable( ⅓ );
callable( 3 );

当提到存储在容器中的值时,必须提供 signal 符号。这反过来允许 Routine 被用作调用的参数

sub f() {}
my &g = sub {}
sub caller(&c1, &c2){ c1, c2 }
caller(&f, &g);

33.4. 绑定

在赋值之后,Raku 还支持 := *绑定*运算符。将值或容器绑定到变量时,会修改变量的 lexpad 条目(而不仅仅是它指向的容器)。如果你这样写:

my $x := 42;

然后 $x 的 lexpad 条目直接指向 Int 42. 这意味着你不能再给它赋值了:

my $x := 42;
$x = 23;
CATCH { default { say .^name, ': ', .Str } };
# OUTPUT: «X::AdHoc: Cannot assign to an immutable value»

您还可以将变量绑定到其他变量:

my $a = 0;
my $b = 0;
$a := $b;
$b = 42;
say $a;         # OUTPUT: «42»

这里,在初始绑定之后,$a 的 lexpad 条目和 $b 的lexpad 条目两者都指向同一个标量容器,因此给一个变量赋值也会改变另一个变量的内容。

您之前已经看到过这种情况:它正是签名参数标记为 is rw 的情况。

无符号变量和带有 is raw trait 的参数总是绑定的(无论使用 =:= ):

my $a = 42;
my \b = $a;
b++;
say $a;         # OUTPUT: «43»

sub f($c is raw) { $c++ }
f($a);
say $a;         # OUTPUT: «44»

33.5. Scalar 容器和 listy things

在 Raku 中有许多位置容器类型,其语义略有不同。最基本的是 List; 它由逗号运算符创建。

say (1, 2, 3).^name;    # OUTPUT: «List»

列表是不可变的,这意味着您无法更改列表中的元素数。但是,如果其中一个元素恰好是标量容器,您仍然可以给它赋值:

my $x = 42;
($x, 1, 2)[0] = 23;
say $x;                 # OUTPUT: «23»
($x, 1, 2)[1] = 23;     # Cannot modify an immutable value
CATCH { default { say .^name, ': ', .Str } };
# OUTPUT: «X::Assignment::RO: Cannot modify an immutable Int»

所以列表不关心它的元素是值还是容器,它们只是存储和检索给它们的任何东西。

列表也可以是惰性的; 在这种情况下,最终的元素是根据迭代器的要求生成的。

Array 就像一个列表,除了它强制所有元素都是容器,这意味着你总是可以给元素赋值:

my @a = 1, 2, 3;
@a[0] = 42;
say @a;         # OUTPUT: «[42 2 3]»

@a 实际上存储了三个标量容器。@a[0] 返回其中一个,赋值运算符用新的整数替换该容器中存储的整数值 42

33.6. 赋值和绑定给数组变量

对标量变量和数组变量的赋值都执行相同的操作:丢弃旧值,并输入一些新值。

然而,很容易观察到它们有多么不同:

my $x = 42; say $x.^name;   # OUTPUT: «Int»
my @a = 42; say @a.^name;   # OUTPUT: «Array»

这是因为 Scalar 容器类型隐藏得很好,但 Array 没有这样的效果。对数组变量的赋值也是强制性的,因此可以将非数组值赋给数组变量。

要将非 Array 放入数组变量,绑定起作用:

my @a := (1, 2, 3);
say @a.^name;               # OUTPUT: «List»

33.7. 绑定到数组元素

作为一个奇怪的旁注,Raku 支持绑定到数组元素:

my @a = (1, 2, 3);
@a[0] := my $x;
$x = 42;
say @a;                     # OUTPUT: «[42 2 3]»

如果您已经阅读并理解了之前的解释,那么现在是时候知道这是如何工作的了。毕竟,绑定到变量需要该变量的 lexpad 条目,虽然数组有一个 lexpad 条目 ,但每个数组元素都没有 lexpad 条目,因为您无法在运行时展开 lexpad。

答案是在语法级别识别绑定到数组元素,而不是为正常绑定操作发出代码,在数组上调用特殊方法(BIND-KEY 被调用)。此方法处理与数组元素的绑定。

请注意,虽然支持,但通常应避免直接将非容器化事物绑定到数组元素中。这样做可能会在以后使用数组时产生反直觉的结果。

my @a = (1, 2, 3);
@a[0] := 42;         # This is not recommended, use assignment instead.
my $b := 42;
@a[1] := $b;         # Nor is this.
@a[2] = $b;          # ...but this is fine.
@a[1, 2] := 1, 2;    # runtime error: X::Bind::Slice
CATCH { default { say .^name, ': ', .Str } };
# OUTPUT: «X::Bind::Slice: Cannot bind to Array slice»

混合列表和数组的操作通常可以防止发生这种意外情况。

33.8. 展平, 项和容器

Raku 中的 %@ Sigils 通常指示迭代构造的多个值,而 $ sigil 仅指示一个值。

my @a = 1, 2, 3;
for @a { };         # 3 iterations
my $a = (1, 2, 3);
for $a { };         # 1 iteration

@-sigiled 变量不会在列表上下文中展平:

my @a = 1, 2, 3;
my @b = @a, 4, 5;
say @b.elems;               # OUTPUT: «3»

有些操作会使不在标量容器内的子列表被展平:slurpy parameters(*@a)和显式调用 flat

my @a = 1, 2, 3;
say (flat @a, 4, 5).elems;  # OUTPUT: «5»

sub f(*@x) { @x.elems };
say f @a, 4, 5;             # OUTPUT: «5»

您还可以使用 | 创建 Slip,将列表引入另一个列表中。

my @l := 1, 2, (3, 4, (5, 6)), [7, 8, (9, 10)];
say (|@l, 11, 12);    # OUTPUT: «(1 2 (3 4 (5 6)) [7 8 (9 10)] 11 12)»
say (flat @l, 11, 12) # OUTPUT: «(1 2 3 4 5 6 7 8 (9 10) 11 12)»

在第一种情况下,@l 的每个元素都作为结果列表的相应元素*滑动*。另一方面,flat *扁平化*所有元素,包括所包含数组的元素,除了 (9 10)

如上所述,标量容器可防止扁平化:

sub f(*@x) { @x.elems };
my @a = 1, 2, 3;
say f $@a, 4, 5;            # OUTPUT: «3»

@ 字符也可以用作将参数强制为列表的前缀,从而删除标量容器:

my $x = (1, 2, 3);
.say for @$x;               # 3 iterations

但是,*解容器*运算符 <> 更适合去除非列表项:

my $x = ^Inf .grep: *.is-prime;
say "$_ is prime" for @$x;  # WRONG! List keeps values, thus leaking memory
say "$_ is prime" for $x<>; # RIGHT. Simply decontainerize the Seq

my $y := ^Inf .grep: *.is-prime; # Even better; no Scalars involved at all

方法通常不关心他们的调用者是否在标量中,所以:

my $x = (1, 2, 3);
$x.map(*.say);              # 3 iterations

在三个元素的列表上 map,而不是在一个元素上 map。

33.9. 自引用数据

容器类型(包括 ArrayHash)允许您创建自引用结构。

my @a;
@a[0] = @a;
put @a.perl;
# OUTPUT: «((my @Array_75093712) = [@Array_75093712,])»

虽然 Raku 不会阻止您创建和使用自引用数据,但这样做可能会导致您尝试转储数据。作为最后的手段,您可以使用 Promises 来处理超时。

33.10. 类型约束

任何容器都可以具有类型对象subset形式的类型约束。两者都可以放在声明符和变量名之间,也可以放在 trait of。之后。约束是变量的属性,而不是容器的属性。

subset Three-letter of Str where .chars == 3;
my Three-letter $acronym = "ÞFL";

在这种情况下,类型约束是(编译类型定义的)subset Three-letter

变量可能没有容器,但仍然提供重新绑定和类型检查重新绑定的能力。原因是在这种情况下绑定运算符:= 执行类型检查:

my Int \z = 42;
z := 100; # OK
z := "x"; # Typecheck failure

例如,当绑定到 Hash 键时,情况并非如此,因为绑定随后由方法调用处理(即使语法保持不变,使用 := 运算符)。

Scalar 容器的默认类型约束是 Mu.VAR.of 方法提供了对容器类型约束的内省,对于 @% sigiled 变量,它给出了值的约束:

my Str $x;
say $x.VAR.of;  # OUTPUT: «(Str)»
my Num @a;
say @a.VAR.of;  # OUTPUT: «(Num)»
my Int %h;
say %h.VAR.of;  # OUTPUT: «(Int)»

33.10.1. Definedness 约束

容器还可以强制执行变量是定义的。在声明中放一个笑脸:

my Int:D $def = 3;
say $def;   # OUTPUT: «3»
$def = Int; # Typecheck failure

您还需要在声明中初始化变量,毕竟变量不能是未定义的。

也可以在使用默认定义变量 pragma 的作用域中声明的所有变量中强制执行此约束。来自其他语言的人们总是会定义变量,他们希望看看。

33.11. 自定义容器

为了提供自定义容器,Raku 提供了 Proxy 这个类 。当从容器中存储或提取值时,需要调用两个方法。类型检查不是由容器本身完成的,并且 readonlyness 等其他限制可以被破坏。因此,返回的值必须与它绑定的变量的类型相同。我们可以使用类型捕获来处理 Raku 中的类型。

sub lucky(::T $type) {
    my T $c-value; # closure variable
    return Proxy.new(
        FETCH => method () { $c-value },
        STORE => method (T $new-value) {
            X::OutOfRange.new(what => 'number', got => '13', range => '-∞..12, 14..∞').throw
                if $new-value == 13;
            $c-value = $new-value;
        }
    );
}

my Int $a := lucky(Int);
say $a = 12;    # OUTPUT: «12»
say $a = 'FOO'; # X::TypeCheck::Binding
say $a = 13;    # X::OutOfRange
CATCH { default { say .^name, ': ', .Str } };

34. Raku 原生类型

Raku 提供了一组原生类型,在内存中具有固定且已知的表示。此页面显示了存在哪些原生类型以及如何使用它们。有关它们的更多信息,请查看有关原生数字 的页面。

34.1. Types with native representation

Raku 中的一些简单类型具有原生表示,表示它们将使用编译器,操作系统和原生提供的 C 语言表示。这些是可用的四种原生类型:

int

Equivalent to Int (with limited range)

uint

Equivalent to Int (with limited range) with the unsigned trait

num

Equivalent to Num

str

Equivalent to Str

但是,这些类型不一定具有 NativeCall 接口所需的大小(例如,Raku 的 int 可以是 8 个字节,但 C 的 int 只有 4 个字节); 必须使用以下类型而不是上面列出的 intnum 类型。

通常,这些变量的行为与常规标量变量的行为方式相同,称为自动装箱; 然而,存在一些差异,因为您实际宣称的是如何表示它们,而不是它们的实际类型。第一个是它们的类型实际上是它们的等效类型,而不是它们的原生类型。

my int $intillo = 3;
say $intillo.^name; # OUTPUT: «Int␤»

这显然意味着他们将智能匹配他们的等效(自动装箱)类型,而不是他们的原生类型:

my str $strillo = "tres";
say $strillo ~~ str; # OUTPUT: «False␤»
say $strillo ~~ Str; # OUTPUT: «True␤»

并且与非原生对应物不同,他们将始终具有默认值:

say (my Str $); # OUTPUT: «(Str)␤»
say (my str $); # OUTPUT: «␤»
say (my num $); # OUTPUT: «0␤»

注意: 在 v6.c 中,num 的默认值是 NaN。

这是因为 Natives 不知道他们的类型,因为他们只是值,没有任何元数据。在多重分派 中,您可以拥有原生候选者,但无法区分相同原生类型的不同大小。也就是说,你可以有一个 Intint 候选者,但是 int, atomicint, int64 等候选者之间会有歧义。

它们也不能被绑定。尝试做 my num $numillo := 3.5 会发出异常 Cannot bind to natively typed variable '$variable-name'; use assignment instead

原生类型也可以是复合的。

my int @intillos = ^10_000_000;
say [+] @intillos; # OUTPUT: «49999995000000␤»

在这种情况下,*native*ness 扩展到复合类型,它将是 array

my num @many-pi  = ^8 »*» π ; say @many-pi.^name;  # OUTPUT: «array[num]␤»

原生`数组`是 Iterable,但它们不是 List 的子类。但是,它们的行为类似于 Array; 例如,它们可以成形

my str @letter-pairs[10] = 'a'..'j' Z~ 'A'..'J';
say @letter-pairs.perl;
# OUTPUT: «array[str].new(:shape(10,), ["aA", "bB", "cC", "dD", "eE", "fF", "gG", "hH", "iI", "jJ"])␤»

34.2. Types with native representation and size

关于具有原生表示的类型的提及也适用于此;它们将自动装入 Raku 类型,并且不受限制。但是,下表中列出的这些类型具有可在NativeCall函数中使用的特性:

int8

(int8_t in C, also used for char)

int16

(int16_t in C, also used for short)

int32

(int32_t in C, also used for int)

int64

(int64_t in C)

byte, uint8

(uint8_t in C, also used for unsigned char)

uint16

(uint16_t in C, also used for unsigned short)

uint32

(uint32_t in C, also used for unsigned int)

uint64

(uint64_t in C)

num32

(float in C)

num64

(double in C)

这些类型具有固定大小的表示,它独立于平台,因此可以安全地用于那些原生调用。如果我们愿意,没有什么能阻止我们在任何其他环境中使用它们。与上述类型相同,在为此类型的变量赋值时,必须考虑此大小:

my byte $intillo = 257;
say $intillo; # OUTPUT: «1␤»

由于 byte 只能容纳 8 位,因此它将换行并分配模值为 256 的原始值的结果,这就是所示的内容。

声明原生大小的类型与没有声明原生大小的类型之间的主要区别是在声明中使用了 nativesize。例如,int8 以这种方式声明:

my native int8 is repr('P6int') is Int is nativesize( 8) { }

表示除了整数表示(P6int)之外,它还将使用仅 8 位的原生大小。但是,这个特性并不打算在您的程序中使用,因为它不是 Raku 规范的一部分。

34.3. void 类型

原生 void 类型对应于 C 的 void 类型。虽然是有效类型,但您可以在表达式中使用它

use NativeCall;
my void $nothing;
say $nothing.perl; # OUTPUT: «NativeCall::Types::void␤»

实际上,它是一个很难单独使用的 Uninstantiable 类型,实际上它在 link:(return) 类型中被明确禁止。但是,它通常在类型指针中找到,表示等效于 C 中的 void * 指针。

sub malloc( int32 $size --> Pointer[void] ) is native { * };
my Pointer[void] $for-malloc = malloc( 32 );
say $for-malloc.perl;

如果您需要在使用该类型的原生函数中使用它们,您还可以将 Blob nativecast 到此类指针上。

use NativeCall;
my Pointer[void] $native = nativecast(Pointer[void], Blob.new(0x22, 0x33));

但是,除此之外,它提供的功能非常有限,因为指向 void 的指针无法解引用:

use NativeCall;
my Pointer[void] $native = nativecast(Pointer[void], Buf.new(0x22, 0x33));
say $native.deref; # ERROR OUTPUT: «Internal error: unhandled target type␤»

34.4. Atomic types

在这种情况下,atomic 指的是线程下的安全操作。 Raku 提供了一个类型,atomicint一些操作,它们共同保证了这一点。有关详细信息,请查看 link:(Numerics) 页面上的原子操作部分。

34.5. Rakudo specific native types

本节中描述的类型是特定于 Rakudo 的,因此不保证它们在其他实现中或在将来的版本中保持不变。

long

(long in C)

longlong

(longlong in C)

ulong

(long and unsigned in C)

ulonglong

(longlong and unsigned in C)

size_t

(size_t and unsigned in C)

ssize_t

(size_t in C)

bool

(bool in C)

您可以像在本机 C 中使用它们一样使用它们:

use NativeCall;

my $just-an-array = CArray[int32].new( 1, 2, 3, 4, 5 );

loop ( my size_t $i = 0; $i < $just-an-array.elems; $i++ ) {
    say $just-an-array[$i];
}

这将打印数组的五个元素,因为它应该是你期望的。

35. 数值

35.1. Int

Int 类型提供任意大小的整数。它们可以像计算机内存允许的那样大,虽然有些实现在被要求生成真正惊人大小的整数时会选择抛出数字溢出错误:

say 10**600**600
# OUTPUT: «Numeric overflow»

与某些语言不同,当两个操作数都是 Int 类型时,使用`/`运算符执行除法将生成小数,而不执行任何舍入。

say 4/5; # OUTPUT: «0.8»

这种除法产生的类型是 RatNum 类型。换算后,如果分数的分母是小于64位,则产生 Rat, 否则产生 Num 类型。

如果你想落得 Int 的结果,那么 divnarrow 例程可能会有帮助,只要有可能。div运算符执行整除,丢弃余数,而narrow 会把数拟合到它适合的最窄类型:

say 5 div 2; # OUTPUT: «2»

# Result `2` is narrow enough to be an Int:
say (4/2).narrow; # OUTPUT: «2»
say (4/2).narrow.^name; # OUTPUT: «Int»

# But 2.5 has fractional part, so it ends up being a Rat type:
say (5/2).narrow.^name; # OUTPUT: «Rat»
say (5/2).narrow;       # OUTPUT: «2.5»

# Denominator is too big for a Rat, so a Num is produced:
say 1 / 10⁹⁹; # OUTPUT: «1e-99»

Raku 具有 FatRat 类型,可提供任意精度的分数。为什么在上一个例子中生成了有限精度的 Num 而不是 FatRat 类型?原因是:性能。大多数操作都很好,精度损失很少,因此不需要使用更昂贵的 FatRat 类型。如果你希望获得额外的精度,则需要自己实例化一个。

35.2. Num

Num 类型提供 双精度浮点十进制数,在其他语言中有时被称为“doubles”。

Num 字面量的写法是使用字母 e 与指数分割开。请记住,即使指数为零,字母`e` 也是必需的,否则你将得到一个Rat 有理数字面量:

say 42e0.^name; # OUTPUT: «Num»
say 42.0.^name; # OUTPUT: «Rat»

区分大小写的单词 InfNaN 分别表示特殊值 infinity 和 not-a-number。可以使用 U+221E INFINITY()字符代替 Inf

Raku 尽可能遵循IEEE 754-2008浮点运算标准,计划在以后的语言版本中实现更多的一致性。该语言保证为任何给定的 Num 字面量选择最接近的可表示数字,并且确实支持负零和非正规(也称为“次正规”)。

请记住,像 sayput 这样的输出例程不会非常难以区分输出数字类型的方式,并且可能选择将Num显示为IntRat数字。要获得更明确的输出字符串,请使用perl方法:

say  1e0;      # OUTPUT: «1»
say .5e0;      # OUTPUT: «0.5»
say  1e0.perl; # OUTPUT: «1e0»
say .5e0.perl; # OUTPUT: «0.5e0»

35.3. Complex

复平面复数型数值。复数对象包括两个 Num 对象以表示复数的实部虚部

要创建复数,可以在任何其他非复数上使用后缀`i`运算符,可选择使用加法设置实部。要使用`i`运算符作用在 NaNInf 字面量上,请使用反斜杠将其与它们分开。

say 42i;      # OUTPUT: «0+42i»
say 73+42i;   # OUTPUT: «73+42i»
say 73+Inf\i; # OUTPUT: «73+Inf\i»

请记住,上面的语法只是一个附加表达式和优先级规则适用。它也不能用于禁止表达式的地方,例如常规参数中的字面量。

# Precedence of `*` is higher than that of `+`
say 2 * 73+10i; # OUTPUT: «146+10i»

为了避免这些问题,你可以选择使用复数字面量语法,其中包括使用尖括号包围实部和虚部,而不包含任何空格

say 2 * <73+10i>; # OUTPUT: «146+20i»

multi how-is-it (<2+4i>) { say "that's my favorite number!" }
multi how-is-it (|)      { say "meh"                        }
how-is-it 2+4i;  # OUTPUT: «that's my favorite number!»
how-is-it 3+2i;  # OUTPUT: «meh»

35.4. Rational

执行 Rational 角色的类型提供高精度和任意精度的十进制数。由于精度越高,性能损失越大,Rational 类型有两种形式:RatFatRatRat 是最常用的变体, 其在大多数情况下降级成 Num,当它不再能容纳所有的要求精度时。FatRat 是保持增长提供所有所需的精度任意精度的变体。

35.4.1. Rat

最常见的 Rational 类型。它支持有 64 位分母的有理数(在将分数换算到最小分母之后)。Rat 可以直接创建具有较大分母的对象,但是,当具有这样的分母的 Rat 是数学运算的结果时,它们会降级为 Num 对象。

在许多其他语言中 Rat 字面量使用和 Num 字面量类似的语法,使用点来表示数字是十进制:

say .1 + .2 == .3; # OUTPUT: «True»

如果你在许多常用语言中执行与上述类似的语句, 由于浮点数学的精度,你将得到 False 作为答案。要在 Raku 中获得相同的结果,你必须使用 Num 字面量:

say .1e0 + .2e0 == .3e0; # OUTPUT: «False»

你还可以使用具有 IntRat 对象的`/`除法运算符来生成 Rat

say 3/4;     # OUTPUT: «0.75»
say 3/4.2;   # OUTPUT: «0.714286»
say 1.1/4.2; # OUTPUT: «0.261905»

请记住,上面的语法只是一个除法表达式,优先规则适用。也不能用在禁止表达式的地方,比如例程参数中的字面量。

# Precedence of power operators is higher than division
say 3/2²; # OUTPUT: «0.75»

为了避免这些问题,你可以选择使用 Rational 字面量语法,它用尖括号括起分子和分母,不带任何空格

say <3/2>²; # OUTPUT: «2.25»

multi how-is-it (<3/2>) { say "that's my favorite number!" }
multi how-is-it (|)     { say "meh"                        }
how-is-it 3/2;  # OUTPUT: «that's my favorite number!»
how-is-it 1/3;  # OUTPUT: «meh»

最后,任何具有 No 属性的表示小数的 Unicode 字符都可以用作Rat 字面量:

say ½ + ⅓ + ⅝ + ⅙; # OUTPUT: «1.625»
分解为 Num

如果产生 Rat 答案的*数学运算*会产生分母大于64位的 Rat,则该操作将返回 Num 对象。当*构建*一个Rat(即,当它不是一些数学表达式的结果)时,但是,更大的分母可以使用:

my $a = 1 / (2⁶⁴ - 1);
say $a;                   # OUTPUT: «0.000000000000000000054»
say $a.^name;             # OUTPUT: «Rat»
say $a.nude;              # OUTPUT: «(1 18446744073709551615)»

my $b = 1 / 2⁶⁴;
say $b;                   # OUTPUT: «5.421010862427522e-20»
say $b.^name;             # OUTPUT: «Num»

my $c = Rat.new(1, 2⁶⁴);
say $c;                   # OUTPUT: «0.000000000000000000054»
say $c.^name;             # OUTPUT: «Rat»
say $c.nude;              # OUTPUT: «(1 18446744073709551616)»
say $c.Num;               # OUTPUT: «5.421010862427522e-20»

35.4.2. FatRat

最后一个 Rational 类型 - FatRat - 保留你所要求的所有精度,将分子和分母存储为两个 Int 对象。FatRatRat 更具传染性,有这么多的 FatRat 数学运算会产生另一个 FatRat,保留所有可用的精度。当 Rat 退化为 Num 时,使用 FatRat 的数学运算会持续不断:

say ((42 + Rat.new(1,2))/999999999999999999).^name;         # OUTPUT: «Rat»
say ((42 + Rat.new(1,2))/9999999999999999999).^name;        # OUTPUT: «Num»
say ((42 + FatRat.new(1,2))/999999999999999999).^name;      # OUTPUT: «FatRat»
say ((42 + FatRat.new(1,2))/99999999999999999999999).^name; # OUTPUT: «FatRat»

没有特殊的运算符或语法可用于构造 FatRat 对象。只需使用 FatRat.new 方法,将分子作为第一个位置参数,将分母作为第二个位置参数。

如果你的程序需要大量的 FatRat 创建,你可以创建自己的自定义运算符:

sub infix:<🙼> { FatRat.new: $^a, $^b }
say (1🙼3).perl; # OUTPUT: «FatRat.new(1, 3)»

35.4.3. 打印 rationals

请记住,像 sayput 这样的输出例程不会力图区分数字类型如何输出,并且可能选择将 Num 显示为 IntRat 数字。要获得更明确的输出字符串,请使用 perl 方法:

say 1.0;        # OUTPUT: «1»
say ⅓;          # OUTPUT: «0.333333»
say 1.0.perl;   # OUTPUT: «1.0»
say ⅓.perl;     # OUTPUT: «<1/3>»

有关更多信息,你可以选择在 nude 中查看 Rational 对象,显示其分子和分母:

say ⅓;          # OUTPUT: «0.333333»
say 4/2;        # OUTPUT: «2»
say ⅓.perl;     # OUTPUT: «<1/3>»
say <4/2>.nude; # OUTPUT: «(2 1)»

35.5. 除零

在许多语言中,除以零立马会抛出一个异常。在 Raku 中,会发生什么取决于你要除的东西以及你如何使用结果。

Raku 遵循 IEEE 754-2008浮点运算标准,但由于历史原因,6.c 和 6.d 语言版本不完全符合。Num被零除产生 Failure,而复数被零除产生 NaN 部件, 无论分子是什么。

从 6.e 语言开始,NumComplex 除以零将产生-Inf+InfNaN, 这取决于分子分别是负数,正数还是零(对于复数,实部和虚部是 Num 并且被分别考虑)。

Int 数字的除法产生一个 Rat 对象(或 Num,如果在换算之后分母大于64位,当你除以零时就不是这种情况)。这意味着这种除法永远不会产生异常失败。结果是零分母有理数,这可能是爆炸性的。

35.5.1. Zero-denominator rationals

零分母 有理数是一个扮演 Rational 角色的数字,它在核心数字中将是 RatFatRat 对象,其分母为零。这样根据原始分子是否为负,分别为零或正数, 有理数的分子被归一化到`-1`,0`或`1

可以在不需要实际除法的情况下执行的操作是非爆炸性的。例如,你可以单独检查 nude 中的分子分母,或执行数学运算,而不会出现任何异常或失败。

转换零分母有理数到 Num 遵循 IEEE 公约,结果是`-Inf`,Inf,或 NaN,这取决于分子是否分别是负,正,或零。从另一个方面来看也是如此:转换`±Inf`/ `NaN`到其中一个 Rational 类型将产生具有适当分子的零分母有理数:

say  <1/0>.Num;   # OUTPUT: «Inf»
say <-1/0>.Num;   # OUTPUT: «-Inf»
say  <0/0>.Num;   # OUTPUT: «NaN»
say Inf.Rat.nude; # OUTPUT: «(1 0)»

要求非 IEEE 除法的分子和分母的所有其他操作将导致抛出异常 X::Numeric::DivideByZero。最常见的此类操作可能是尝试打印或字符串化零分母有理数:

say 0/0;
# OUTPUT:
# Attempt to divide by zero using div
#  in block <unit> at -e line 1

35.6. 同质异形

Allomorphs 是两种类型的子类,可以表现为它们中的任何一种。例如,同质异形 IntStrIntStr 类型的子类,并且将被需要 IntStr 对象的任何类型约束所接受。

同质异形可以使用尖括号创建,可以单独使用或作为散列键查找的一部分使用; 直接使用方法`.new`,也由一些结构提供,如 sub MAIN 的参数。

say <42>.^name;                 # OUTPUT: «IntStr»
say <42e0>.^name;               # OUTPUT: «NumStr»
say < 42+42i>.^name;            # OUTPUT: «ComplexStr»
say < 1/2>.^name;               # OUTPUT: «RatStr»
say <0.5>.^name;                # OUTPUT: «RatStr»

@*ARGS = "42";
sub MAIN($x) { say $x.^name }   # OUTPUT: «IntStr»

say IntStr.new(42, "42").^name; # OUTPUT: «IntStr»

上面的几个结构在打开角括号之后有一个空格。那个空格不是故意的。通常使用运算符编写的数字,例如`1/2`(Rat,除法运算符)和`1+2i`(复数,加法)可以写成不涉及使用运算符的字面值:在尖括号和尖括号里面的字符之间*没有*任何空格。通过在尖括号中添加空格,我们告诉编译器我们不仅需要 RatComplex 字面量,而且我们还希望它是一个allomorph:在这种情况下是 RatStrComplexStr

如果数字字面量不使用任何运算符,则将其写入尖括号内,即使不包含任何空格,也会产生同形异形体。(逻辑:如果你不想要同质异形,你就不会使用尖括号。对于使用运算符的数字也是如此,因为某些结构,例如签名字面量,不允许你使用运算符,所以你不能只为这些数字字面量省略尖括号)。

35.6.1. 可用的同质异形

核心语言提供以下同质异形:

Type

Allomorph of

Example

IntStr

Int and Str

<42>

NumStr

Num and Str

<42e0>

ComplexStr

Complex and Str

< 1+2i>

RatStr

Rat and Str

<1.5>

注意:没有 FatRatStr 类型。

35.6.2. Coercion of allomorphs

请记住,同质异形只是它们所代表的两种(或三种)类型的子类。正如变量或参数类型约束为`Foo`可以接受任何 Foo 子类一样,所以变量或参数类型约束为 Int 的将接受 IntStr 同质异形:

sub foo(Int $x) { say $x.^name }
foo <42>;                          # OUTPUT: «IntStr»
my Num $y = <42e0>;
say $y.^name;                      # OUTPUT: «NumStr»

当然,这也适用于参数coercers

sub foo(Int(Cool) $x) { say $x.^name }
foo <42>;  # OUTPUT: «IntStr»

给定的同质异形*已经*是 Int 类型的对象,因此在这种情况下它不会转换为“普通的” Int

当然,如果没有办法将它们“折叠”到其中一个组件,那么同质异形体的力量将会严重减弱。因此,如果你使用所要强制到的类型的名字显式调用方法,那么你将获得该组件。这同样适用于任何代理方法,例如调用方法.Numeric而不是.Int或使用prefix:<> `运算符而不是.Str`方法调用。

my $al := IntStr.new: 42, "forty two";
say $al.Str;  # OUTPUT: «forty two»
say +$al;     # OUTPUT: «42»

say <1/99999999999999999999>.Rat.^name;    # OUTPUT: «Rat»
say <1/99999999999999999999>.FatRat.^name; # OUTPUT: «FatRat»

强制整个同质异形体列表的一种方便方法是将 hyper 运算符应用于适当的前缀:

say map *.^name,   <42 50e0 100>;  # OUTPUT: «(IntStr NumStr IntStr)»
say map *.^name, +«<42 50e0 100>;  # OUTPUT: «(Int Num Int)»
say map *.^name, ~«<42 50e0 100>;  # OUTPUT: «(Str Str Str)»

35.6.3. Object identity

当我们考虑对象一致性时,上面关于强制同形异形的讨论变得更加重要。一些构造利用它来确定两个对象是否“相同”。而对于人类而言,同质异形`42`和常规的`42`可能看起来“相同”,对于那些构造,它们是完全不同的对象:

# "42" shows up twice in the result: 42 and <42> are different objects:
say unique 1, 1, 1, 42, <42>; # OUTPUT: «(1 42 42)»
# Use a different operator to `unique` with:
say unique :with(&[==]), 1, 1, 1, 42, <42>; # OUTPUT: «(1 42)»
# Or coerce the input instead (faster than using a different `unique` operator):
say unique :as(*.Int), 1, 1, 1, 42, <42>; # OUTPUT: «(1 42)»
say unique +«(1, 1, 1, 42, <42>);         # OUTPUT: «(1 42)»

# Parameterized Hash with `Any` keys does not stringify them; our key is of type `Int`:
my %h{Any} = 42 => "foo";
# But we use the allomorphic key of type `IntStr`, which is not in the Hash:
say %h<42>:exists;           # OUTPUT: «False»
# Must use curly braces to avoid the allomorph:
say %h{42}:exists;           # OUTPUT: «True»

# We are using a set operator to look up an `Int` object in a list of `IntStr` objects:
say 42 ∈ <42 100 200>; # OUTPUT: «False»
# Convert it to an allomorph:
say <42> ∈ <42 100 200>; # OUTPUT: «True»
# Or convert the items in the list to plain `Int` objects:
say 42 ∈ +«<42 100 200>; # OUTPUT: «True»

注意这些对象一致性的差异,并根据需要强制你的同形异形体。

35.7. 原生数字

顾名思义,原生数字可以访问原生数字 - 即由硬件直接提供的数字。这反过来又提供两个功能:溢出/下溢和更好的性能。

注意:在撰写本文时(2018.05),某些实现(例如 Rakudo)提供了有关原生类型的一些细节,例如 int64 是否可用且在32位计算机上具有64位大小,以及如何检测何时你的程序正在这样的硬件上运行。

35.7.1. 可用的原生数字

Native type

Base numeric

Size

atomicint

integer

sized to offer CPU-provided atomic operations. (typically 64 bits on 64-bit platforms and 32 bits on 32-bit ones)

int

integer

64-bits

int16

integer

16-bits

int32

integer

32-bits

int64

integer

64-bits

int8

integer

8-bits

num

floating point

64-bits

num32

floating point

32-bits

num64

floating point

64-bits

uint

unsigned integer

64-bits

uint16

unsigned integer

16-bits

uint32

unsigned integer

32-bits

uint64

unsigned integer

64-bits

uint8

unsigned integer

8-bits

35.7.2. 创建原生数字

要创建原生类型的变量或参数,只需使用其中一个可用数字的名称作为类型约束:

my int32 $x = 42;
sub foo(num $y) {}
class { has int8 $.z }

有时,你可能希望在不创建任何可用变量的情况下将某些值强制转换为原生类型。没有`.int`或类似的强制方法(方法调用是后期的,所以它们不适合这个目的)。相反,只需使用匿名变量:

some-native-taking-sub (my int $ = $y), (my int32 $ = $z)

35.7.3. 溢出/下溢

尝试分配不适合特定原生类型的值会产生异常。这包括尝试为原生参数提供过大的参数:

my int $x = 2¹⁰⁰;
# OUTPUT:
# Cannot unbox 101 bit wide bigint into native integer
#  in block <unit> at -e line 1

sub f(int $x) { $x }; say f 2⁶⁴
# OUTPUT:
# Cannot unbox 65 bit wide bigint into native integer
#   in sub f at -e line 1
#   in block <unit> at -e line 1

但是,以这样一种太大/太小的方式修改已存在的值会产生溢出/下溢行为:

my int $x = 2⁶³-1;
say $x;             # OUTPUT: «9223372036854775807»
say ++$x;           # OUTPUT: «-9223372036854775808»

my uint8 $x;
say $x;             # OUTPUT: «0»
say $x -= 100;      # OUTPUT: «156»

创建使用原生类型的对象不涉及程序员的直接分配; 这就是为什么这些构造提供溢出/下溢行为而不是抛出异常。

say Buf.new(1000, 2000, 3000).List; # OUTPUT: «(232 208 184)»
say my uint8 @a = 1000, 2000, 3000; # OUTPUT: «232 208 184»

35.7.4. Auto-boxing

虽然它们可以被称为“原生类型 ”,但原生数字实际上并不是具有任何可用方法的类。但是,你*可以*调用这些数字的非原生版本上可用的任何方法。这是怎么回事?

my int8 $x = -42;
say $x.abs; # OUTPUT: «42»

此行为称为“自动装箱”。编译器使用所有方法自动将原生类型“装箱”为功能齐全的高级类型。换句话说,`int8`上面的内容自动转换为Int,然后它是Int类,然后提供被调用的abs方法。

当你使用原生类型获得性能提升时,此详细信息非常重要。如果你正在使用的代码导致执行大量自动装箱,那么使用原生类型的性能可能会比使用非原生类型时*更差*:

my $a = -42;
my int $a-native = -42;
{ for ^1000_000 { $a.abs        }; say now - ENTER now } # OUTPUT: «0.38180862»
{ for ^1000_000 { $a-native.abs }; say now - ENTER now } # OUTPUT: «0.938720»

如你所见,原生变体的速度慢了两倍多。原因是方法调用需要将原生类型装箱,而非原生变体不需要这样的东西,因此性能损失。

在这种特殊情况下,我们可以简单地切换到abs的子程序形式,它可以使用原生类型而无需装箱。在其他情况下,你可能需要寻找其他解决方案以避免过多的自动装箱,包括切换到部分代码的非原生类型。

my $a = -42;
my int $a-native = -42;
{ for ^1000_000 { abs $a        }; say now - ENTER now } # OUTPUT: «0.38229177»
{ for ^1000_000 { abs $a-native }; say now - ENTER now } # OUTPUT: «0.3088305»

35.7.5. 默认值

由于原生类型后面没有类,因此通常没有使用尚未初始化的变量获得的类型对象。因此,原生类型自动初始化为零。在6.c语言,原生的浮点类型(numnum32,和`num64`)被初始化为值 NaN; 在 6.d 语言中默认为 0e0

35.7.6. 原生分派

例如,当大小可预测时,可以使原生候选者与非原生候选者一起提供具有原生候选者的更快算法,但是否则回退到较慢的非原生候选者。以下是涉及原生候选人的多重分派的规则。

首先,原生类型的大小在分派中不起作用,并且`int8`被认为与`int16`或`int` 例如,当大小可预测时,可以使本地候选者与非本地候选者一起提供具有本地候选者的更快算法,但是否则回退到较慢的非本地候选者。以下是涉及本地候选人的多次派遣的规则。

首先,原生类型的大小在调度中不起作用,并且`int8`被认为与`int16`或`int` 相同:

multi foo(int   $x) { say "int" }
multi foo(int32 $x) { say "int32" }
foo my int $x = 42;
# OUTPUT:
# Ambiguous call to 'foo(Int)'; these signatures all match:
# :(int $x)
# :(int32 $x)

其次,如果例程是一个 only-ie,它不是一个multi非原生类型,而是在调用期间给出一个原生类型,反之亦然,那么参数将被自动装箱或自动取消装箱以使可以被调用。如果给定的参数太大而无法放入native参数,则会抛出异常:

-> int {}( 42 );            # OK; auto-unboxing
-> int {}( 2¹⁰⁰ );          # Too large; exception
-> Int {}( 2¹⁰⁰ );          # OK; non-native parameter
-> Int {}( my int $ = 42 ); # OK; auto-boxing

当涉及到multi例程时,如果没有可用的原生候选者,则原生参数将始终自动装箱:

multi foo (Int $x) { $x }
say foo my int $ = 42; # OUTPUT: «42»

另一种方式是不能提供相同的 luxury。如果只有原生候选者可用,则非原生参数将*不会*被自动取消装箱,而是指示不会抛出匹配的候选者的异常(这种不对称的原因是原生类型总是可以装箱,但是非原生的可能太大而无法融入原生):

multi f(int $x) { $x }
my $x = 2;
say f $x;
# OUTPUT:
# Cannot resolve caller f(Int); none of these signatures match:
#     (int $x)
#   in block <unit> at -e line 1

但是,如果正在进行调用,其中一个参数是原生类型而另一个是数字字面量,则放弃此规则:

multi f(int, int) {}
f 42, my int $x; # Successful call

这样,你就不必不断将诸如 $n +> 2 写为 $n +> (my int $ = 2) 了。编译器知道字面量小到足以适合原生类型并将其转换为原生类型。

35.7.7. 原子操作

该语言提供了一些保证以原子方式执行的操作,即安全地由多个线程执行而无需锁定而没有数据争用的风险。

对于此类操作,需要atomicint原生类型。此类型与普通原生int类似,不同之处在于它的大小使得可以对其执行CPU提供的原子操作。在32位CPU上,它通常是32位大小,而在64位CPU上,它通常是64位大小。

# !!WRONG!! Might be non-atomic on some systems
my int $x;
await ^100 .map: { start $x⚛++ };
say $x; # OUTPUT: «98»

# RIGHT! The use of `atomicint` type guarantees operation is atomic
my atomicint $x;
await ^100 .map: { start $x⚛++ };
say $x; # OUTPUT: «100»

相似性`int`也存在于多重分派中: atomicint,普通的 `int`和固定大小的`int`变量都是相同的,并且不能通过多重分派来区分。

35.8. Numeric infectiousness

当一些数学运算中涉及两个不同类型的数字时,数字“传递性”决定了结果类型。如果结果是该类型而不是其他操作数的类型,则认为类型比其他类型更具传递性。例如,Num类型比Int更具传递性,因此我们可以期望`42e0 + 42`产生Num作为结果。

传递性如下,首先列出最具传递性的类型

  • Complex

  • Num

  • FatRat

  • Rat

  • Int

say (2 + 2e0).^name; # Int + Num => OUTPUT: «Num»
say (½ + ½).^name; # Rat + Rat => OUTPUT: «Rat»
say (FatRat.new(1,2) + ½).^name; # FatRat + Rat => OUTPUT: «FatRat»

同质异形体具有与其数字成分相同的传递性。原生类型获得自动装箱,并具有与其盒装变体相同的传递性。

36. Unicode

Raku 对 Unicode 有很高的支持。本文档旨在概述和描述不属于例程和方法文档的 Unicode 功能。

有关 MoarVM 内部字符串表示的概述,请参阅 MoarVM 字符串文档

36.1. 文件句柄和输入输出

36.1.1. 标准化

默认情况下,Raku 对所有输入和输出应用标准化,但存储为 link:(UTF8-C8) 的文件名除外;字形是用户可见的字符形式,将使用标准化表示。这是什么意思?例如,字形数字 á 可以用两种方式表示,或者使用一个代码点:

á (U+E1 "LATIN SMALL LETTER A WITH ACUTE")

或两个代码点:

a +  ́ (U+61 "LATIN SMALL LETTER A" + U+301 "COMBINING ACUTE ACCENT")

Raku 将这两个输入转换为一个代码点,如规范化形式 C(NFC)所指定的那样。在大多数情况下,这很有用,意味着两个相同的输入都被视为相同。 Unicode 具有规范等价的概念,它允许我们确定字符串的规范形式,允许我们正确地比较字符串并操纵它们,而不必担心文本丢失这些属性。默认情况下,您处理或从 Raku 输出的任何文本都将采用此“规范”形式,即使在对字符串进行修改或连接时也是如此(请参阅下文,了解如何避免这种情况)。有关规范化表单C和规范等效性的更多详细信息,请参阅Unicode Foundation 的规范化和规范等效性页面。

我们不默认的一种情况是文件名。这是因为必须完全访问文件的名称,就像在磁盘上写入字节一样。

为避免规范化,您可以使用名为 UTF8-C8 的特殊编码格式。将此编码与任何文件句柄一起使用将允许您读取磁盘上的确切字节,而不进行规范化。如果使用 UTF8 句柄打印出来,打印出来时看起来会很滑稽。如果将其打印到输出编码为 UTF8-C8 的句柄,则它将按照您通常的预期进行渲染,并且是字节精确复制的字节。有关 MoarVM 上 UTF8-C8 的更多技术细节, 请参见下文。

36.1.2. UTF8-C8

UTF-8 Clean-8 是一种编码器/解码器,主要用作 UTF-8。但是,遇到一个不能解码为有效 UTF-8 的字节序列,或者由于规范化而不会往返的字节序列时,它将使用 link:(NFG) 合成来跟踪所涉及的原始字节。这意味着编码回 UTF-8 Clean-8 将能够重新创建它们最初存在的字节。合成物包含4个代码点:

  • 代码点 0x10FFFD (这是一个私用的代码点)

  • 代码点 'x'

  • 高4位作为十六进制字符的不可解码字节 (0..9A..F)

  • 低4位作为十进制字符的不可解码字节 (0..9A..F)

在正常的 UTF-8 编码下,这意味着不可代表的字符会像 ?xFF 那样出现。

UTF-8 Clean-8 用于 MoarVM 从环境,命令行参数和文件系统查询接收字符串的地方,例如解码缓冲区时:

say Buf.new(ord('A'), 0xFE, ord('Z')).decode('utf8-c8');
#  OUTPUT: «A􏿽xFEZ␤»

您可以看到 UTF8-C8 使用的两个初始代码点如何显示在此处,就在“FE”之前。您可以使用此类编码来读取具有未知编码的文件:

my $test-file = "/tmp/test";
given open($test-file, :w, :bin) {
  .write: Buf.new(ord('A'), 0xFA, ord('B'), 0xFB, 0xFC, ord('C'), 0xFD);
  .close;
}

say slurp($test-file, enc => 'utf8-c8'); # OUTPUT: «(65 250 66 251 252 67 253)»

使用这种类型的编码进行读取并将它们编码回 UTF8-C8 将返回原始字节;使用默认的 UTF8-C8 是不可能的。

请注意,到目前为止,这种编码在 Rakudo 的 JVM 实现中不受支持。

36.2. 输入 unicode 代码点和代码点序列

您可以按编号(十进制和十六进制)输入 Unicode 代码点。例如,名为“带有macron的拉丁大写字母ae”的字符具有十进制代码点482和十六进制代码点0x1E2:

say "\c[482]"; # OUTPUT: «Ǣ␤»
say "\x1E2";   # OUTPUT: «Ǣ␤»

您还可以按名称访问 Unicode 代码点:Rakudo 支持所有 Unicode 9.0 名称。

say "\c[PENGUIN]"; # OUTPUT: «🐧␤»
say "\c[BELL]";    # OUTPUT: «🔔␤» (U+1F514 BELL)

所有 Unicode 代码点名称/命名seq /emoji 序列现在都不区分大小写:[从2017.02开始]

say "\c[latin capital letter ae with macron]"; # OUTPUT: «Ǣ␤»
say "\c[latin capital letter E]";              # OUTPUT: «E␤» (U+0045)

您可以使用带有 \c[] 的逗号分隔列表来指定多个字符。 您也可以组合数字和命名样式:

say "\c[482,PENGUIN]"; # OUTPUT: «Ǣ🐧␤»

除了在内插字符串中使用 \chttps://docs.raku.org/routine/uniparse 之外,您还可以使用 [uniparse]::

say "DIGIT ONE".uniparse;  # OUTPUT: «1␤»
say uniparse("DIGIT ONE"); # OUTPUT: «1␤»

36.2.1. 名称别名

按名称别名。名称别名主要用于没有正式名称的代码点,缩写或更正(Unicode 名称永远不会更改)。有关它们的完整列表,请参见此处

没有任何官方名称的控制代码:

say "\c[ALERT]";     # Not visible (U+0007 control code (also accessible as \a))
say "\c[LINE FEED]"; # Not visible (U+000A same as "\n")

更正:

say "\c[LATIN CAPITAL LETTER GHA]"; # OUTPUT: «Ƣ␤»
say "Ƣ".uniname; # OUTPUT: «LATIN CAPITAL LETTER OI␤»
# This one is a spelling mistake that was corrected in a Name Alias:
say "\c[PRESENTATION FORM FOR VERTICAL RIGHT WHITE LENTICULAR BRACKET]".uniname;
# OUTPUT: «PRESENTATION FORM FOR VERTICAL RIGHT WHITE LENTICULAR BRAKCET␤»

缩写:

say "\c[ZWJ]".uniname;  # OUTPUT: «ZERO WIDTH JOINER␤»
say "\c[NBSP]".uniname; # OUTPUT: «NO-BREAK SPACE␤»

36.2.2. 命名序列

您也可以使用任何命名序列,这些不是单个代码点,而是它们的序列。 [从2017.02开始]

say "\c[LATIN CAPITAL LETTER E WITH VERTICAL LINE BELOW AND ACUTE]";      # OUTPUT: «É̩␤»
say "\c[LATIN CAPITAL LETTER E WITH VERTICAL LINE BELOW AND ACUTE]".ords; # OUTPUT: «(201 809)␤»
Emoji 序列

Rakudo 支持表情符号 4.0(最新的非草稿版本)序列。 对于他们所有人看到:link:(表情符号 ZWJ 序列)和表情符号序列。 请注意,任何带逗号的名称都应删除逗号,因为 Raku 使用逗号分隔同一 \c 序列中的不同代码点/序列。

say "\c[woman gesturing OK]";         # OUTPUT: «🙆‍♀️␤»
say "\c[family: man woman girl boy]"; # OUTPUT: «👨‍👩‍👧‍👦␤»

37. Phasers

程序的生命周期(执行时间表)分为几个阶段。*phaser*是在特定执行阶段调用的代码块。

37.1. Phasers

phaser 块只是包含它的闭包的 trait,并在适当的时刻自动调用。这些自动调用的块称为 phasers,因为它们通常标记从计算的一个阶段到另一个阶段的转换。例如,在编译编译单元结束时调用 CHECK 块。也可以安装其他类型的 phasers; 它们会在适当的时候自动调用,其中一些 phasers 响应各种控制异常和退出值。例如,如果块的退出成功或失败,则可能会调用某些 phasers,在这种情况下*成功*退出, 则在这时返回定义的值或列表,而不带任何 Failure 或异常。

以下是摘要:

  BEGIN {...} #  * at compile time, as soon as possible, only ever runs once
  CHECK {...} #  * at compile time, as late as possible, only ever runs once
   INIT {...} #  * at runtime, as soon as possible, only ever runs once
    END {...} #  at runtime, as late as possible, only ever runs once
    DOC [BEGIN|CHECK|INIT] {...} # only in documentation mode

  ENTER {...} #  * at every block entry time, repeats on loop blocks.
  LEAVE {...} #  at every block exit time (even stack unwinds from exceptions)
   KEEP {...} #  at every successful block exit, part of LEAVE queue
   UNDO {...} #  at every unsuccessful block exit, part of LEAVE queue

  FIRST {...} #  at loop initialization time, before any ENTER
   NEXT {...} #  at loop continuation time, before any LEAVE
   LAST {...} #  at loop termination time, after any LEAVE

    PRE {...} #  assert precondition at every block entry, before ENTER
   POST {...} #  assert postcondition at every block exit, after LEAVE

  CATCH {...} #  catch exceptions, before LEAVE
CONTROL {...} #  catch control exceptions, before LEAVE

   LAST {...} #  supply tapped by whenever-block is done, runs very last
   QUIT {...} #  catch async exceptions within a whenever-block, runs very last

COMPOSE {...} #  when a role is composed into a class (Not yet implemented)
  CLOSE {...} #  appears in a supply block, called when the supply is closed

标记为 * 号的 phaser 具有运行时值,并且如果早于周围表达式进行求值,则只需保存其结果,以便在以后计算表达式的其余部分时在表达式中使用:

my $compiletime = BEGIN { now };
our $random = ENTER { rand };

与其他语句前缀一样,这些产生值的构造可以放在块或语句的前面:

my $compiletime = BEGIN now;
our $random = ENTER rand;

这些 phaser 的大多数将接收块或函数引用。语句形式对于将词法作用域的声明暴露给周围的词法作用域而不在块中“捕获”它特别有用。

它们声明了与前面示例相同作用域的相同变量,但在指定时间把语句作为整体运行:

BEGIN my $compiletime = now;
ENTER our $random = rand;

(但请注意,在运行时克隆任何周围闭包时,在编译时计算的变量值可能不会持久存在。)

大多数非值生成 phasers 也可能如此使用:

END say my $accumulator;

但请注意:

END say my $accumulator = 0;

END time 时将变量设置为 0 ,因为这是实际执行 “my” 声明的时间。只有无参数的 phasers 可以使用语句形式。这意味着 CATCHCONTROL 始终需要一个块,因为它们接收一个设置 $ 为当前主题的参数,以便内部行为能够表现为 switch 语句。(如果允许使用裸语句,那么 $ 临时绑定会在 CATCH`或者`CONTROL 结束时泄漏出来,带来不可预测的,甚至可能是可怕的后果。异常处理程序应该减少不确定性,而不是增加它。)

其中一些 phasers 也具有可以在变量上设置的相应 trait; 他们使用 will 后面跟着小写的 phaser 名称。这些优点是将讨论中的变量作为主题传递给闭包:

our $h will enter { .rememberit() } will undo { .forgetit() };

只有在块内可以多次出现的 phaser 才有资格获得这种每个变量(per-variable)形式; 这不包括 CATCH 和其他例如 CLOSEQUIT phaser 。

phaser 外部的块的主题作为 OUTER::<$_> 仍然可用。返回值是否可修改可能是所讨论的 phaser 的策略。特别地,不应在 POST phaser 内修改返回值,但 LEAVE phaser 可能更自由。

在方法的词法作用域中定义的任何 phaser 都是闭合 self 以及正常词汇。(或者等效地,实现可以简单地将所有这样的 phaser 转换为其引导的调用者是当前对象的子方法。)

当多个 phaser 被安排在同一时刻运行时,一般的打破平局的原则是初始化 phaser 按照声明的顺序执行,而最终 phaser 以相反的顺序执行,因为设置和拆除通常希望以相反的顺序相互发生。

37.1.1. 执行顺序

编译开始

      BEGIN {...} #  at compile time, As soon as possible, only ever runs once
      CHECK {...} #  at compile time, As late as possible, only ever runs once
    COMPOSE {...} #  when a role is composed into a class (Not yet implemented)

执行开始

       INIT {...} #  at runtime, as soon as possible, only ever runs once

在块执行开始之前

        PRE {...} #  assert precondition at every block entry, before ENTER

循环执行开始

      FIRST {...} #  at loop initialization time, before any ENTER

块执行开始

      ENTER {...} #  at every block entry time, repeats on loop blocks.

可能会发生异常

      CATCH {...} #  catch exceptions, before LEAVE
    CONTROL {...} #  catch control exceptions, before LEAVE

循环结束,继续或结束

       NEXT {...} #  at loop continuation time, before any LEAVE
       LAST {...} #  at loop termination time, after any LEAVE

块结束

      LEAVE {...} #  at every block exit time (even stack unwinds from exceptions)
       KEEP {...} #  at every successful block exit, part of LEAVE queue
       UNDO {...} #  at every unsuccessful block exit, part of LEAVE queue

块的后置条件

       POST {...} #  assert postcondition at every block exit, after LEAVE

异步 whenever-block 结束

       LAST {...} #  if ended normally with done, runs once after block
       QUIT {...} #  catch async exceptions

程序终止

        END {...} #  at runtime, ALAP, only ever runs once

37.2. 程序执行 phasers

37.2.1. BEGIN

编译时运行,一旦 phaser 中的代码编译完毕,就只运行一次。

返回值可在以后的 phaser 中使用:

say "About to print 3 things";
for ^3 {
    say ^10 .pick ~ '-' ~ BEGIN { say  "Generating BEGIN value"; ^10 .pick }
}
# OUTPUT:
# Generating BEGIN value
# About to print 3 things
# 3-3
# 4-3
# 6-3

phaser 中的 ^10 .pick 只产生一次,并在运行时期间由循环重用。注意怎么 BEGIN 块中的 say 是在上述循环执行之前是怎么执行的。

37.2.2. CHECK

在编译时运行,尽可能晚,只运行一次。

可以具有即使在后期 phases 提供的返回值。

在运行时生成的代码仍然可以启动 CHECKINIT phasers,但当然这些 phaser 无法做出需要及时返回的事情。你需要一个虫洞。

37.2.3. INIT

在 main 执行期间编译后运行,尽快运行一次。它可以具有即使在后期 phases 也提供的返回值。

当 phaser 位于不同的模块中时, phaser INITEND phaser 将被视为在使用模块中就像在 use 时声明一样。(如果模块被多次使用,则依赖于此顺序是错误的,因为仅在第一次注意到它们时才安装 phaser 。)

在运行时生成的代码仍然可以启动 CHECKINIT phaser,但当然这些 phaser 无法做出需要及时返回的事情。你需要一个虫洞。

INIT 克隆闭包的所有副本只运行一次。

37.2.4. END

在 main 执行期间编译后运行,尽可能晚,只运行一次。

当 phaser 位于不同的模块中时, INITEND phaser 将被视为在正使用的模块中就像在 use 时声明一样。(如果模块被多次使用,则依赖于此顺序是错误的,因为仅在第一次注意到它们时才安装 phaser 。)

37.3. Block phasers

块的上下文中的执行具有其自己的 phases。

块离开 phaser 等待直到调用堆栈实际展开才能运行。只有在某个异常处理程序决定以这种方式处理异常之后才会展开。也就是说,仅仅因为异常被抛出堆栈帧并不意味着我们已经正式离开了块,因为异常可能是可恢复的。在任何情况下,异常处理程序都指定在失败代码的动态作用域内运行,无论异常是否可恢复。堆栈已展开,仅在未恢复异常时才调用 phaser 。

这些可以在块内多次出现。所以它们确实不是真正的 trait - 它们将自己添加到存储在实际 trait 中的列表中。如果你检查块的 ENTER trait,你会发现它实际上是一个 phaser 列表而不是一个 phaser 。

所有这些 phaser 块都可以看到任何先前声明的词法变量,即使在调用闭包时尚未详细说明这些变量(在这种情况下,变量会计算为未定义的值。)

37.3.1. ENTER

在每个块进入时运行,在循环块上重复。

可以具有即使在后期 phases 提供的返回值。

ENTER phaser 抛出的异常将中止 ENTER 队列,但是从 LEAVE phaser 抛出的异常将不会。

37.3.2. LEAVE

在每个块退出时运行(甚至堆栈从异常中展开),除非程序突然退出(例如 exit)。

LEAVE 在任何 CATCHCONTROL phaser 之后必须计算给定块的 phaser 。这包括 LEAVE 变体,KEEPUNDOPOST 在其他一切之后对 phaser 进行计算,以保证偶数 LEAVE phaser 不会违反后置条件。

ENTER phaser 抛出的异常将中止 ENTER 队列,但是从 LEAVE phaser 抛出的异常将不会。

如果 POST 失败或任何类型的 LEAVE 块在堆栈展开时抛出异常,则展开继续并收集要处理的异常。展开完成后,将从该点抛出所有新异常。

sub answer() {
    LEAVE say „I say after the return value.“;

    42 # this is the return value
}

注意: 铭记 LEAVE phaser 直接在程序的块,即使用错误的参数尝试调用该例程, 他们也将得到执行:

sub foo (Int) {
    say "Hello!";
    LEAVE say "oh noes!"
}
try foo rand; # OUTPUT: «oh noes!»

虽然子程序的主体没有得到执行,因为 sub 的Intrand 期望返回一个 Num,其块进入和离开时(指令绑定失败),因此 LEAVE phaser *正*运行。

37.3.3. KEEP

在每个成功的块出口处运行,作为 LEAVE 队列的一部分(共享相同的执行顺序)。

37.3.4. UNDO

在每个不成功的块出口处运行,作为 LEAVE 队列的一部分(共享相同的执行顺序)。

37.3.5. PRE

断言每个块条目的前提条件。在 ENTER phase 之前运行。

PRE phaser 在任何 ENTERFIRST 之前启动。

失败的 PREPOST phaser 抛出的异常不能被同一个块中的 CATCH 异常捕获,这意味着如果`PRE`phaser 失败,则 POST phaser 不会运行。

37.3.6. POST

在每个块条目处断言后置条件。在 LEAVE phase 后运行。

对于如 KEEPPOST 的 phaser,在正常情况下退出作用域时运行,返回值(如果有的话)从该作用域可作为 phaser 中的当前主题。

POST 块可以以两种方式之一来定义。要么 POST 定义为单独的 phaser ,在这种情况下 PREPOST 不共享词法作用域。或者,任何 PRE phaser 都可以将其对应的 POST 定义为嵌入式 phaser 块,该 phaser 块封闭在 PRE 的词法作用域内。

如果 POST 失败或任何类型的 LEAVE 块在堆栈展开时抛出异常,则展开继续并收集要处理的异常。展开完成后,将从该点抛出所有新异常。

PREPOST phaser 抛出的异常不能被同一个块中的 CATCH 异常捕获,这意味着如果 PRE phaser 失败,POST phaser 就不会运行。

37.4. Loop phasers

FIRSTNEXTLAST 仅在循环的词法作用域内有意义,并且可能仅在这样的循环块的顶层发生。

37.4.1. FIRST

在 ENTER 之前运行循环初始化。

37.4.2. NEXT

循环继续(通过 next 或因为你到达循环的底部并循环回来)时运行,在LEAVE之前。

仅当正常到达循环块的末尾或 next`显式 执行时,才执行 `NEXT。 与 LEAVE phaser 不同,NEXT 如果通过除由 next 引发的控制异常之外的任何异常退出循环块,则不执行 NEXT phaser。特别地,last 绕过了 NEXT phaser 的计算。

37.4.3. LAST

在循环结束时运行,在 LEAVE 之后(或者当它使用 lastreturn 退出时; 或者因为你到了循环的底部) 。

37.5. Exception handling phasers

37.5.1. CATCH

在 LEAVE phase 之前,当前块引发异常时运行。

37.5.2. CONTROL

在 LEAVE phase 之前,当前块引发控制异常时运行。它通过 returnfailredonextlastemittakewarnproceedsucceed 发生。

say elems gather {
    CONTROL {
        when CX::Warn { say "WARNING!!! $_"; .resume }
        when CX::Take { say "Don't take my stuff"; .resume }
    }
    warn 'people take stuff here';
    take 'keys';
}
# OUTPUT:
# WARNING!!! people take stuff here
# Don't take my stuff
# 0

37.6. Object phasers

37.6.1. COMPOSE (Not yet implemented)

将角色组合到一个类中时运行。

37.7. Asynchronous phasers

37.7.1. LAST

Supply 完成 done 调用或当一个 supply 块正常退出时运行。它在 whenever 块完成后完全运行。

此 phaser 重用该名称 LAST,但与 LAST 循环 phaser 的工作方式不同。此 phaser 类似于用 tap supply 设置例程 done

37.7.2. QUIT

Supply 以异常提前终止时运行。它在放置的 whenever 块完成后运行。

此 phaser 类似于 quittap supply 时设置例程 quit

37.7.3. CLOSE

出现在 supply 块中。supply 关闭时调用。

37.8. DOC phasers

37.8.1. DOC

phaser BEGINCHECKINIT 仅在文档模式时,前面带有 DOC 关键字。当使用 --doc 运行时编译器在文档中。

DOC INIT { say 'init'  }  # prints 'init' at initialization time when in documentation mode.

38. 性能

该页面是关于在 Raku 上下文中 计算机性能 的。

38.1. 首先,剖析你的代码

确保你没有在错误的代码上浪费时间: 通过剖析你的代码的性能以从识别你的 "临界 3%" 开始。本文档的其余部分将向您展示如何执行此操作。

38.1.1. Time with now - INIT now

对于 now - INIT now 形式的表达式, 其中 INIT 是一个 Raku 程序中运行的 phase, 为计时代码片段提供了一个很好的习惯用法。

使用 m: your code goes here raku 频道 evalbot 来写出这样的行:

m: say now - INIT now
rakudo-moar abc1234: OUTPUT«0.0018558␤»

INIT 左边的 nowINIT 右边的 now *晚*运行了 0.0018558 秒, 因为后者在INIT phase 期间出现。

38.1.2. 本地剖析

当使用 MoarVM 后端时, Rakudo 编译器的 --profile 命令行选项将剖析数据写到一个 HTML 文件中。

此文件将打开“概述”部分,该部分提供有关程序如何运行的一些总体数据,例如总运行时间,执行垃圾回收所花费的时间。您将获得的一个重要信息是被解释的总调用帧(即,块)的百分比(最慢,红色),拼写(更快,橙色)和 jitted(最快,绿色)。

下一节“常规”可能是您花费最多时间的地方。它有一个可排序和可过滤的例程(或块)名称+文件+行的表,它运行的次数,包含时间(在该例程中花费的时间+从它调用的所有例程中花费的时间),独占时间(仅在该例程中花费的时间),以及它是否被解释,拼写或jitted(与“概述”页面相同的颜色代码)。按专属时间排序是了解从哪里开始优化的好方法。文件名从 SETTING::src/core/gen/moar/ 开始的例程来自编译器,从您自己的代码中看到的东西的一个好方法是将您描述的脚本的文件名放在“名称”中“ 搜索框。

“调用图”部分给出了与“例程”部分大致相同信息的火焰图表示。

“分配”部分为您提供有关分配的不同类型的数量以及分配的例程的信息。

“GC”部分为您提供有关所发生的所有垃圾收集的详细信息。

“OSR/Deopt”部分为您提供有关堆栈替换(OSR)的信息,这是在将例程从“已解释”升级为“拼写”或“jitted”时。当拼写或jitted代码必须被“降级”为被解释时,De是相反的。

如果配置文件数据太大,浏览器可能需要很长时间才能打开该文件。在这种情况下,使用 --profile-filename 选项输出到扩展名为 .json 的文件,然后使用 Qt 查看器打开该文件。

要处理更大的配置文件,请输出到扩展名为 .sql 的文件。这将把配置文件数据写成一系列SQL语句,适合在 SQLite 中打开。

=== create a profile
raku --profile --profile-filename=demo.sql -e 'say (^20).combinations(3).elems'

=== create a SQLite database
sqlite3 demo.sqlite

=== load the profile data
sqlite> .read demo.sql

=== the query below is equivalent to the default view of the "Routines" tab in the HTML profile
sqlite> select
      case when r.name = "" then "<anon>" else r.name end as name,
      r.file,
      r.line,
      sum(entries) as entries,
      sum(case when rec_depth = 0 then inclusive_time else 0 end) as inclusive_time,
      sum(exclusive_time) as exclusive_time
    from
      calls c,
      routines r
    where
      c.id = r.id
    group by
      c.id
    order by
      inclusive_time desc
    limit 30;

要了解如何解释配置文件信息,请使用 evalbot(如上所述)并在 IRC 频道上提问。

38.1.3. Profile 编译

If you want to profile the time and memory it takes to compile your code, use Rakudo’s --profile-compile or --profile-stage`options. 如果要分析编译代码所需的时间和内存,请使用 Rakudo 的 `--profile-compile--profile-stage 选项。

38.1.4. 创建或查看基准

使用 raku-bench

如果您为多个编译器(通常是 Perl 5,Raku 或 NQP 的版本)运行 raku-bench,则每个编译器的结果将在视觉上覆盖在相同的图形上,以便快速轻松地进行比较。

38.1.5. Share problems

Once you’ve used the above techniques to identify the code to improve, you can then begin to address (and share) the problem with others:

  • 对于每个问题,将其提取到单行或 gist,并提供性能数字或使片段足够小,以便可以使用 prof-m: your code or gist URL goes here 进行分析。

  • 考虑你需要/想要的最低速度增加(或减少或减少什么),并考虑与实现该目标相关的成本。在人们的时间和精力方面,改进的价值是什么?

  • 让其他人知道您的 Raku 用例是在生产环境中还是仅仅是为了好玩。

38.2. 解决问题

这需要重复:确保你没有浪费时间在错误的代码上。首先确定代码的“关键3%”。

38.2.1. 逐行

尝试逐行改进代码的快速,有趣和高效的方法是使用 raku evalbot camelia 与其他人协作。

38.2.2. 逐个例程

使用 multidispatch,您可以在现有的例程“旁边”添加新的例程变体:

=== existing code generically matches a two arg foo call:
multi sub foo(Any $a, Any $b) { ... }

=== new variant takes over for a foo("quux", 42) call:
multi sub foo("quux", Int $b) { ... }

拥有多个 foo 定义的调用开销通常是微不足道的(虽然请参见下面的讨论),因此如果您的新定义比以前存在的定义集更有效地处理其特定情况,那么您可能只是使您的代码更有效率对于那种情况。

38.2.3. 加速类型检测和调用解析

大多数 where 子句 - 以及大多数子集 - 强制动态(运行时)类型检查和调用解析它可能匹配的任何调用。这比编译时更慢,或者至少晚一些。

方法调用通常尽可能晚地解析(在运行时动态),而 sub 调用通常在编译时静态解析。

38.2.4. 选择更好的算法

无论语言或编译器如何,提高性能的最可靠技术之一是选择更合适的算法。

一个典型的例子是 Boyer-Moore。要匹配大字符串中的小字符串,一个明显的方法是比较两个字符串的第一个字符然后,如果它们匹配,则比较第二个字符,或者,如果它们不匹配,则比较第一个字符大字符串中第二个字符的小字符串的字符,依此类推。相反,Boyer-Moore 算法首先将小字符串的 last 字符与大字符串中相应定位的字符进行比较。对于大多数字符串,Boyer-Moore 算法在算法上接近 N 倍,其中 N 是小字符串的长度。

接下来的几节讨论了算法改进的两大类,这些类别在 Raku 中特别容易实现。有关这个一般主题的更多信息,请阅读有关算法效率的维基百科页面,尤其是接近结尾的“另请参阅”部分。

38.2.5. 将顺序/阻塞代码更改为并行/非阻塞

这是另一个非常重要的算法改进类。

38.2.6. 使用已有的高性能代码

您可以在 Raku 中使用大量高性能 C 库,而 NativeCall 可以轻松地为它们创建包装器。还有对 C++ 库的实验性支持。

如果要在 Raku 中使用 Perl 5 模块,请混合使用 Raku 类型和元对象协议

更一般地说,Raku 旨在与其他语言平滑地互操作,并且有许多模块旨在促进使用来自其他语言的库

38.2.7. 让 Rakudo 编译器生成更快的代码

到目前为止,编译器的重点是正确性,而不是它生成代码的速度有多快,或者生成的代码运行速度有多快。但是预计会发生变化,最终…​…​你可以在 freenode IRC 频道#raku 和 #moarvm 上与编译器开发人员讨论预期的内容。更好的是,你可以自己贡献代码:

  • Rakudo 主要用 Raku 编写。因此,如果您可以编写 Raku,那么您可以破解编译器,包括优化任何影响代码速度的大量现有高级代码(以及其他所有代码)。

  • 大多数编译器的其余部分都是用一种名为 NQP 的小语言编写的,它基本上是 Raku 的一个子集。如果你可以编写 Raku,你也可以很容易地学会使用和改进中级 NQP 代码,至少从一种纯粹的语言观点。要深入了解 NQP 和 Rakudo 的内涵,请从 NQP 和内部课程开始。

  • 如果低级别的 C 黑客是你的乐趣,请查看 MoarVM 并访问 freenode IRC 频道 #moarvm(日志)。

38.2.8. 仍然需要更多想法?

此页面中尚未涵盖的一些已知当前 Rakudo 性能缺陷包括使用 gather/takejunctions,正则表达式和字符串处理。

如果您认为某个主题需要在此页面上进行更多报道,请提交 PR 或告诉某人您的想法。谢谢。 :)

38.3. 没有得到你需要/想要的结果?

如果您已尝试此页面上的所有内容无效,请考虑使用 #raku 上的编译器开发人员进行讨论,以便我们可以从您的用例中了解到目前为止您已经发现的内容。

一旦开发人员知道您的困境,请留出足够的时间做出明智的回应(几天或几周,具体取决于问题的确切性质和潜在的解决方案)。

如果还没有成功,请考虑在继续之前提交有关您在我们的用户体验仓库中的体验的问题。

谢谢。 :)

39. 语句前缀

语句前缀写在语句之前, 改变语句的意思, 语句的输出或语句运行的时刻。因为他们拥有特定的行为, 他们有时候也对某些语句或语句组有特定作用。

39.1. lazy

作为语句前缀,lazy 会在任何语句(包括 for 循环)之前起作用,从而在实际需要将其赋值给变量时保存执行。

my $incremented = 0;
my $var = lazy for <1 2 3 4> -> $d {
    $incremented++
};
say $incremented; # OUTPUT: «0␤»
say eager $var;   # OUTPUT: «(0 1 2 3)␤»
say $incremented; # OUTPUT: «4␤»

$incremented 变量仅递增,也就是说,仅当我们热切计算包含惰性循环变量 $var 时,才运行循环的内部部分。 渴望可以通过其他方式应用于变量,例如在其上调用 .eager 方法。

my @array = lazy { (^3).map( *² )  };
say @array;       # OUTPUT: «[...]»
say @array.eager; # OUTPUT: «[0 1 4]␤»

这个前缀也可以在 gather 前面使用,以使内部语句表现得懒惰。 通常,使用此方法会使返回值的任何语句集变得懒惰。

39.2. eager

eager 语句前缀将热切地返回后面的语句的结果,从而消除惰性并返回结果。

my $result := eager gather { for 1..3 { say "Hey"; take $_² } };
say $result[0]; # OUTPUT: «Hey␤Hey␤Hey␤1␤»

当与标量绑定时,gather 隐式地是惰性的。 但是,使用 eager 作为语句前缀,即使我们只是连续请求第一个,它也会在循环中运行所有三个迭代,如打印的 "Hey" 所示。

39.3. hyper, race

hyperrace 使用(可能是同时)线程在循环中运行不同的迭代:

my @a = hyper for ^100_000 { .is-prime }

此代码比裸代码快3倍左右。 但是这里有一些警告:

  • 循环内的操作应花费足够的时间使线程有意义。

  • 循环内不应有对同一数据结构的读取或写入访问。 让循环产生一个结果,并分配它。

  • 如果循环中存在I / O操作,则可能存在争用,因此请避免使用。

hyperrace 之间的主要区别是结果的顺序。 如果您需要按顺序生成循环结果,请使用 hyper;如果您不关心,请使用 race

39.4. quietly

作为语句前缀,quietly 抑制其前面的语句产生的所有警告。

sub marine() {};
quietly say ~&marine; # OUTPUT: «marine␤»

在代码上调用 .Str 会产生警告。 在该语句前面加一个 quietly 只会产生输出,即例程的名称。

39.5. try

如果在语句前使用 try,它将包含其中产生的异常并将其存储在 $! 中。 变量,就像在块前使用它一样

try [].pop;
say $!; # OUTPUT: «Cannot pop from an empty Array␤..»

39.6. do

do 可以用作语句前缀,以消除它们之前的语句的歧义; 例如,如果要分配 for 语句的结果,则需要使用此命令。 裸 for 将失败,但这将起作用:

my $counter = 0;
my $result = do for ^5 { $counter++ };
say $counter; # OUTPUT: «5␤»
say $result;  # OUTPUT: «(0 1 2 3 4)␤»

在其他情况下,do 等效于用括号将语句括起来。 它可以用作(可能更多)简单语法的替代方法。

39.7. sink

与例程一样sink 将运行该语句以丢弃结果。 如果您想对其产生的副作用运行某些语句,请使用它。

my $counter = 0;
my $result = sink for ^5 { $counter++ };
say $counter; #  OUTPUT: «5␤»
say $result;  #  OUTPUT: «(Any)␤»

39.8. once

在循环内, 仅运行带前缀的语句一次。

my $counter;
my $result = do for ^5 { once $counter = 0; $counter++ };
say $result; # OUTPUT: «(0 1 2 3 4)␤»

39.9. gather

可以在语句前面使用 gather,在该语句的任何位置接收并收集从一次 take 运行发出的所有数据结构的列表:

proto sub fact( Int ) {*}
multi sub fact( 1 --> 1 ) {}
multi sub fact( $x ) { take $x * fact( $x-1 ) }

my @factors = gather say fact(13); # OUTPUT: «6227020800»
say @factors;
# OUTPUT: «[2 6 24 120 720 5040 40320 362880 3628800 ...]»

在此示例中,gathersay 之前,它打印阶乘的第一个结果; 同时,它从每次对事实的调用中都收集了结果,该结果发送到 @factor

39.10. start

作为语句前缀,start 的行为与在块前面的行为相同,即,它以异步方式运行该语句,并返回 promise。

proto sub fact( Int ) {*}
multi sub fact( 1 --> 1 ) {}
multi sub fact( $x ) {  $x * fact( $x-1 ) }

my @promises = gather {
    for <3 4> {
        take start fact( 10 ** $_ );
    }
}

say await @promises;

start 创建的 Promises 收集在一个数组中,一旦实现了 Promise,它就会返回操作的结果。

39.11. react

react 可以在并发程序中用于创建代码块,这些代码块在某些事件发生时运行。 它适用于块,也可用作语句前缀。

my Channel $KXGA .= new;
for ^100 {
    $KXGA.send( (100000..200000).pick );
}

my @sums = ( start react whenever $KXGA -> $number {
    say "In thread ", $*THREAD.id;
    say "→ ", (^$number).sum;
} ) for ^10;

start { sleep 10; $KXGA.close(); }

await @sums;

在这种情况下,react 前置于 whenever,这会使从通道中读取的每个数字都变得很长。

39.12. supply

关键字 supply 可创建您可以点击的按需供应。 它与 emit 配对,可以在 supply 前缀语句中的任何位置使用它。

my &cards = ->  {
    my @cards = 1..10 X~ <♠ ♥ ♦ ♣>;
    emit($_) for @cards.pick(@cards.elems);
}
my $supply = supply cards;

$supply.tap( -> $v { say "Drawing: $v" });
$supply.tap( -> $v { say "Drawing: $v" }, done => { say "No more cards" });
# OUTPUT:
# [...]
# Drawing: 1♥
# Drawing: 7♥
# Drawing: 9♥
# No more cards

40. 引号结构

40.1. The Q Lang

在 Raku 中, 字符串通常使用一些引号结构来表示. 这些引号结构中,最简单的就是 Q, 通过便捷方式 「…」Q 后跟着由任意一对儿分隔符包围着的文本. 大多数时候, 你需要的只是 '…'"…".

40.1.1. Literal strings: Q

Q[A literal string]
「More plainly.」
Q ^Almost any non-word character can be a delimiter!^
Q 「「Delimiters can be repeated/nested if they are adjacent.」」

分隔符能够嵌套, 但是在普通的 Q 形式中, 反斜线转义是不允许的. 换种说法就是, Q 字符串尽可能被作为字面量.

Qqqq 之后不允许立即使用一些分隔符。标识符中允许的任何字符都不允许使用,因为在这种情况下,引号结构和这些字符一起被解释为标识符。此外,( ) 是不允许的,因为它被解释为函数调用。如果你仍然希望使用这些字符作为分隔符,请用空格将它们与 Qqqq 分隔开。请注意,一些自然语言在字符串的右侧使用左分隔引号。Q 不支持这些,因为它依赖unicode 属性来区分左分隔符和右分隔符。

Q'this will not work!'
Q(this won't work either!)

上面对例子会产生错误。然而,下面这个能起作用:

Q (this is fine, because of space after Q)
Q 'and so is this'
Q<Make sure you <match> opening and closing delimiters>
Q{This is still a closing curly brace → \}

这些例子产生:

this is fine, because of space after Q
and so is this
Make sure you <match> opening and closing delimiters
This is still a closing curly brace → \

引号结构的行为可以用副词修改,后面的章节会详细解释。

Short

Long

Meaning

:x

:exec

Execute as command and return results

:w

:words

Split result on words (no quote protection)

:ww

:quotewords

Split result on words (with quote protection)

:q

:single

Interpolate \\, \qq[…​] and escaping the delimiter with \

:qq

:double

Interpolate with :s, :a, :h, :f, :c, :b

:s

:scalar

Interpolate $ vars

:a

:array

Interpolate @ vars

:h

:hash

Interpolate % vars

:f

:function

Interpolate & calls

:c

:closure

Interpolate {…​} expressions

:b

:backslash

Enable backslash escapes (\n, \qq, \$foo, etc)

:to

:heredoc

Parse result as heredoc terminator

:v

:val

Convert to allomorph if possible

40.1.2. Escaping: q

'Very plain';
q[This back\slash stays];
q[This back\\slash stays]; # Identical output
q{This is not a closing curly brace → \}, but this is → };
Q :q $There are no backslashes here, only lots of \$\$\$>!$;
'(Just kidding. There\'s no money in that string)';
'No $interpolation {here}!';
Q:q!Just a literal "\n" here!;

q 形式的引号结构允许使用反斜线转义可能会结束字符串的字符. 反斜线自身也能被转义, 就像上面的第三个例子那样. 通常的形式是 '…​'q 后跟着分隔符, 但是它也能作为 Q 上的副词使用, 就像上面的第五个和最后一个例子那样.

这些例子产生:

Very plain
This back\slash stays
This back\slash stays
This is not a closing brace → } but this is →
There are no backslashes here, only lots of $$$!
(Just kidding. There's no money in that string)
No $interpolation {here}!
Just a literal "\n" here

\qq…​ 转义序列允许 [qq 插值] 的一部分字符串。当字符串中有 HTML 标记时,使用这个转义序列非常方便,可以避免将尖括号解释为散列键:

my $var = 'foo';
say '<code>$var</code> is <var>\qq[$var.uc()]</var>';
# OUTPUT: «<code>$var</code> is <var>FOO</var>␤»

40.1.3. Interpolation: qq

my $color = 'blue';
say "My favorite color is $color!" # My favorite color is blue!

qq 形式 — 通常使用双引号写成 — 允许变量的插值, 例如字符串中能写入变量, 以使变量的内容能插入到字符串中. 在 qq 引起字符串中, 也能转义变量.

say "The \$color variable contains the value '$color'";
# The $color variable contatins the value 'blue'

qq 的另外一种功能是使用花括号在字符串中插值 Raku 代码:

my ($x, $y, $z) = 4, 3.5, 3;
say "This room is $x m by $y m by $z m."
say "Therefore its volume should be { $x * $y * $z } m³!"

输出:

This room is 4 m by 3.5 m by 3 m.
Therefore its volume should be 42 m³!

默认情况下, 只有带有 $ 符号的变量才能正常插值. 这时, "documentation@raku.org" 不会插值 @raku 变量. 如果你确实想那么做, 在变量名后面添加一个 []:

my @neighbors = "Felix", "Danielle", "Lucinda";
say "@neighbors[] and I try our best to coexist peacefully."

输出:

Felix Danielle Lucinda and I try our best to coexist peacefully.

通常使用一个方法调用会更合适. 只有在 qq 引号中, 方法调用后面有圆括号, 就能进行插值:

say "@neighbors.join(', ') and I try our best to coexist peacefully."

输出:

Felix, Danielle, Lucinda and I try our best to coexist peacefully.

"@example.com" 产生 @example.com.

要调用子例程请使用 & 符号。

say "abc&uc("def")ghi";
# OUTPUT: «abcDEFghi␤»

后环缀操作符和 subscripts 也会被插值。

my %h = :1st; say "abc%h<st>ghi";
# OUTPUT: «abc1ghi␤»

要输入 unicode 序列,请使用 \x\x[] 加上字符的十六进制编码或字符列表。

my $s = "I \x2665 Raku!";
say $s;
# OUTPUT: «I ♥ Raku!␤»

$s = "I really \x[2661,2665,2764,1f495] Raku!";
say $s;
# OUTPUT: «I really ♡♥❤💕 Raku!␤»

您还可以在 \c[] 中使用 unicode 名称命名序列名称别名

my $s = "Camelia \c[BROKEN HEART] my \c[HEAVY BLACK HEART]!";
say $s;
# OUTPUT: «Camelia 💔 my ❤!␤»

对未定义值进行插值将引发控件异常,该异常可以在当前控件块中使用 CONTROL 捕获。

sub niler {Nil};
my Str $a = niler;
say("$a.html", "sometext");
say "alive"; # this line is dead code
CONTROL { .die };

40.1.4. Word quoting: qw

qw|! @ # $ % ^ & * \| < > | eqv '! @ # $ % ^ & * | < >'.words.list
q:w { [ ] \{ \} } eqv ('[', ']', '{', '}')
Q:w | [ ] { } | eqv ('[', ']', '{', '}')

:w 通常写作 qw, 把字符串分割为 "words" (单词). 在这种情景下, 单词被定义为由空格分割的一串非空白字符. q:wqw 继承了 q 的插值和转义语法, 还有单引号字符串分割符, 而 QwQ:w 继承了 Q 的非转义语法.

my @directions = 'left', 'right,', 'up', 'down';

这样读和写都更容易:

my @directions = qw|left right up down|;

40.1.5. Word quoting: <>

say <a b c> eqv ('a', 'b', 'c');   # OUTPUT: «True␤»
say <a b 42> eqv ('a', 'b', '42'); # OUTPUT: «False␤», the 42 became an IntStr allomorph
say < 42 > ~~ Int; # OUTPUT: «True␤»
say < 42 > ~~ Str; # OUTPUT: «True␤»

尖括号的引号类似于 qw,但有一个额外的特性,可以让你构造特定数字的同质异形体或字面量:

say <42 4/2 1e6 1+1i abc>.perl;
# OUTPUT: «(IntStr.new(42, "42"), RatStr.new(2.0, "4/2"), NumStr.new(1000000e0, "1e6"), ComplexStr.new(<1+1i>, "1+1i"), "abc")␤»

要构造 RatComplex 字面量,请在数字周围使用尖括号,不带任何额外的空格:

say <42/10>.^name;   # OUTPUT: «Rat␤»
say <1+42i>.^name;   # OUTPUT: «Complex␤»
say < 42/10 >.^name; # OUTPUT: «RatStr␤»
say < 1+42i >.^name; # OUTPUT: «ComplexStr␤»

42/101+42i 相比,不涉及除法(或加法)运算。这对于例程签名中的字面量很有用,例如:

sub close-enough-π (<355/113>) {
    say "Your π is close enough!"
}
close-enough-π 710/226; # OUTPUT: «Your π is close enough!␤»

# WRONG: can't do this, since it's a division operation

sub compilation-failure (355/113) {}

40.1.6. Word quoting with quote protection: qww

单词引用的 qw 格式将按字面意思处理引用字符,将它们保留在结果单词中:

say qw{"a b" c}.perl; # OUTPUT: «("\"a", "b\"", "c")␤»

因此,如果您希望在结果单词中保留引用的子字符串作为单个项,则需要使用 qww 变体:

say qww{"a b" c}.perl; # OUTPUT: «("a b", "c")␤»

40.1.7. Word quoting with interpolation: qqw

qw 形式的 word quoting 不会进行变量插值:

my $a = 42; say qw{$a b c};  # $a b c

因此, 如果你想在引号字符串中进行变量插值, 你需要使用 qqw 变体:

my $a = 42;
my @list = qqw{$a b c};
say @list;                # 42 b c

注意,变量插值发生在单词分割之前:

my $a = "a b";
my @list = qqw{$a c};
.say for @list; # OUTPUT: «a␤b␤c␤»

40.1.8. Word quoting with interpolation and quote protection: qqww

qqw 形式的单词引用会把引起的字符当作字面量,将引起的字符留在结果单词中:

my $a = 42; say qqw{"$a b" c}.perl;  # OUTPUT: «("\"42", "b\"", "c")␤»

因此,如果希望在结果单词中保留引起的子字符串为单个项,则需要使用 qqww 变体:

my $a = 42; say qqww{"$a b" c}.perl; # OUTPUT: «("42 b", "c")␤»

引号保护发生在插值之前,插值发生在分词之前,所以来自插值变量内部的引号只是字面引号字符:

my $a = "1 2";
say qqww{"$a" $a}.perl; # OUTPUT: «("1 2", "1", "2")␤»
my $b = "1 \"2 3\"";
say qqww{"$b" $b}.perl; # OUTPUT: «("1 \"2 3\"", "1", "\"2", "3\"")␤»

40.1.9. Word quoting with interpolation and quote protection: « »

这种引用方式类似于 qqww,但它具有构造 allomorphs 的额外好处(使其功能相当于 qq:ww:v)。与 «» 等价的 ASCII 是双尖括号 << >>

# Allomorph Construction
my $a = 42; say «  $a b c    ».perl;  # OUTPUT: «(IntStr.new(42, "42"), "b", "c")␤»
my $a = 42; say << $a b c   >>.perl;  # OUTPUT: «(IntStr.new(42, "42"), "b", "c")␤»

# Quote Protection
my $a = 42; say «  "$a b" c  ».perl;  # OUTPUT: «("42 b", "c")␤»
my $a = 42; say << "$a b" c >>.perl;  # OUTPUT: «("42 b", "c")␤»

40.1.10. Shell quoting: qx

要将字符串作为外部程序运行,不仅可以将字符串传递给 shellrun 函数,还可以执行 shell 引用。然而,有一些微妙之处需要考虑。qx 引号不插入变量。因此

my $world = "there";
say qx{echo "hello $world"}

仅仅打印 hello. 然而, 如果你在调用 raku 之前声明了一个环境变量, 这在 qx 里是可用的, 例如:

WORLD="there" raku
> say qx{echo "hello $WORLD"}

现在会打印 hello there.

调用 qx 会返回结果, 所以这个结果能被赋值给一个变量以便后来使用:

my $output = qx{echo "hello!"};
say $output;    # hello!

40.1.11. Shell quoting with interpolation: qqx

如果希望在外部命令中使用 Raku 变量的内容,那么应该使用 qqx shell 引用结构:

my $world = "there";
say qqx{echo "hello $world"};  # hello there

再一次, 外部命令的输出结果可以保存在一个变量中:

my $word = "cool";
my $option = "-i";
my $file = "/usr/share/dict/words";
my $output = qqx{grep $option $word $file};
# runs the command: grep -i cool /usr/share/dict/words
say $output;      # Cooley␤Cooley's␤Coolidge␤Coolidge's␤cool␤ ...

有关执行外部命令的更好方法,请参见 runProc::Async

40.1.12. Heredocs: :to

一种方便的写多行字符串字面量的方式是 heredocs,它让你选择自己的分隔符:

say q:to/END/;
Here is
some multi-line
string
END

heredoc 的内容总是从下一行开始,所以你可以(也应该)完成这一行。

my $escaped = my-escaping-function(q:to/TERMINATOR/, language => 'html');
Here are the contents of the heredoc.
Potentially multiple lines.
TERMINATOR

如果终止分隔符缩进了, 同等数量的缩进会从字符串字面量上移除. 因此下面这个 heredoc

say q:to/END/;
    Here is
    some multi line
        string
    END

输出:

Here is
some multi line
    string

heredoc 包含了终止符之前的换行符。

要允许对变量进行插值,可以使用 qq 形式,但如果不是已定义变量的标识符,则必须转义元字符 {\$。例如:

my $f = 'db.7.3.8';
my $s = qq:to/END/;
option \{
    file "$f";
};
END
say $s;

会产生:

option {
    file "db.7.3.8";
};

您可以在同一行开始多个 heredoc。

my ($first, $second) = qq:to/END1/, qq:to/END2/;
  FIRST
  MULTILINE
  STRING
  END1
   SECOND
   MULTILINE
   STRING
   END2

40.1.13. Unquoting

字面量字符串允许使用转义序列插入内嵌的引用结构,例如:

my $animal="quaggas";
say 'These animals look like \qq[$animal]'; # OUTPUT: «These animals look like quaggas␤»
say 'These animals are \qqw[$animal or zebras]'; # OUTPUT: «These animals are quaggas or zebras␤»

在本例中,\qq 将做双引号内插,\qqw 文字内插。如上所述,转义任何其他引用结构都将以相同的方式进行,从而允许在字面量字符串中进行插值。

40.2. Regexes

有关在 regexes 中应用的引用的信息,请参阅正则表达式文档

41. 类型系统

41.1. Raku类型的定义

类型通过创建类型对象来定义新对象,该类型对象提供用于创建对象实例或检查值的接口。任何类型对象都是 AnyMu 的子类。通过从这些基类和内省后缀 . 继承来提供内省方法。在编译时由以下类型声明符之一或在运行时使用元对象协议将新类型引入当前作用域。所有类型名称的作用域必须是唯一的。

41.1.1. 默认类型

如果用户没有提供类型,则 Raku 假定类型为 Any。这包括容器,基类,参数和返回类型。

my $a = 1;
$a = Nil;
say $a.^name;
# OUTPUT: «Any»

class C {};
say C.^parents(:all);
# OUTPUT: «((Any) (Mu))»

对于容器,默认类型为 Any,但默认类型约束为 Mu。请注意,绑定会替换容器,而不仅仅是值。在这种情况下,类型约束可能会变。

41.1.2. 类型对象

要测试对象是否为类型对象,请对使用类型为 smiley.DEFINITE 方法约束的类型使用 smartmatch

my $a = Int;
say $a ~~ Mu:U;
# OUTPUT: «True»
say not $a.DEFINITE;
# OUTPUT: «True»

如果调用者是实例,则 .DEFINITE 将返回 True。如果它返回 False,则调用者是一个类型对象。

Undefinedness

未定义的对象在 Raku 中维护类型信息。类型对象用于表示未定义值和未定义值的类型。要提供一般的未定义值,请使用 Any。如果要区分容器和参数的默认类型 Any,则需要使用 Mu

.CREATE 创建的对象实例是按惯例定义的。方法 .defined 将返回 Bool::True 以指示定义。该规则的例外是 NilFailure。请注意,任何对象都可以重载 .defined,因此可以携带其他信息。此外,Raku 明确区分了定义和真假。很多值是有定义的, 即使它们具有错误或空值的含义。这些值为 0Bool::False, () (空列表) 和 NaN

值可以在运行时通过 mixin 变为未定义。

my Int $i = 1 but role::{ method defined { False } };
say $i // "undefined";
# OUTPUT: «undefined»

要测试定义需调用 .defined,请使用 //with/withoutsignatures

强制转换

将一种类型转换为另一种类型是使用与目标类型同名的强制方法完成的。Signatures 强制要求此约定。源类型必须知道如何将自身转换为目标类型。要允许内置类型将自己转换为用户定义的类型,请使用 augment 或者 MOP

class C {
    has $.int;
    method this-is-c { put 'oi' x $!int ~ '‽' }
}

use MONKEY-TYPING;
augment class Int {
    method C { C.new(:int(self))}
}

my $i = 10;
$i.=C;
$i.this-is-c();
# OUTPUT: «oioioioioioioioioioi‽»

Raku 提供了在 Cool 中定义的方法,以便在应用进一步操作之前转换为目标类型。大多数内置类型都来自 Cool,因此可能会提供可能不需要的隐式强制。用户有责任关心这些方法的无陷阱使用。

my $whatever = "123.6";
say $whatever.round;
# OUTPUT: «124»
say <a b c d>.starts-with("ab");
# OUTPUT: «False»

41.2. 类型声明符

类型声明符将新类型引入给定作用域。嵌套作用域可以用 :: 分隔。如果不存在此类作用域,则会自动创建新 packages

class Foo::Bar::C {};
put Foo::Bar::.keys;
# OUTPUT: «C»

可以使用仅包含 …​ 的块来提供前置声明。如果定义了类型,编译器将在当前作用域的末尾检查。

class C {...}
# many lines later
class C { has $.attr }

41.2.1. class

class 声明符创建一个编译为类型对象的编译时构造。后者是一个简单的 Raku 对象,它提供了通过执行初始化程序和子方法来构造实例的方法,以填充在类中声明的所有属性,以及任何具有值的父类。初始化程序可以提供属性声明或构造函数。 Metamodel::ClassHOW 负责知道如何运行它们。这是在 Raku 中构建对象的唯一神奇部分。默认父类型是 Any,它继承自 Mu。后者提供了默认的按照惯例命名的构造函数 .new。除此之外,.new 不具有任何特殊含义,也不以任何特殊方式对待。

有关如何使用类的更多信息,请参阅类和对象教程。

Mixins

class 引入的类型可以在运行时使用 infix: 进行扩展。原始类型不会被修改,而是返回一个新的类型对象,并且可以存储在一个容器中,该容器对原始类型或混合的角色进行成功类型检查。

class A {}
role R { method m { say 'oi‽' } }
my R $A = A but R;
my $a1 = $A.new;
$a1.m;
say [$A ~~ R, $a1 ~~ R];
# OUTPUT: «oi‽[True True]»
自省
元类

要测试给定类型对象是否为类,请针对 Metamodel::ClassHOW 测试元对象方法 .HOW

class C {};
say C.HOW ~~ Metamodel::ClassHOW;
# OUTPUT: «True»
私有属性

私有属性用任何一个 $!@!%! twigils 来处理。它们没有自动生成的公共访问器方法。因此,它们不能从它们所定义的类的外面进行更改。

class C {
    has $!priv;
    submethod BUILD { $!priv = 42 }
};

say (.name, .package, .has_accessor) for C.new.^attributes;
# OUTPUT: «($!priv (C) False)»
方法

method 声明符定义 Method 类型的对象,并将其绑定到类的作用域中提供的名称上。默认情况下,类中的方法具有 has 作用域。our 作用域的那些方法默认不会添加到方法缓存中,因此不能使用访问器符号 $. 来调用。使用完全限定名称和调用者作为第一个参数来调用它们。

继承和 multis

子类中的普通方法不与父类中的 multis 竞争。

class A {
    multi method m(Int $i){ say 'Int' }
    multi method m(int $i){ say 'int' }
}

class B is A {
    method m(Int $i){ say 'B::Int' }
}

my int $i;
B.new.m($i);
# OUTPUT: «B::Int»
Only 方法

要明确声明方法不是 multi 方法,请使用 only 方法声明符。

class C {
    only method m {};
    multi method m {};
};
# OUTPUT: «X::Comp::AdHoc: Cannot have a multi candidate for 'm' when an only method is also in the package 'C'»
Submethod BUILD

submethod BUILD 是(间接地)由被称为 .bless 的方法调用的。它旨在设置类的私有和公共属性,并接收传入 .bless 的所有名称属性。定义在 Mu 中的默认构造函数 .new 是调用它的方法。鉴于公共访问器方法在 BUILD 中不可用,您必须使用私有属性表示法。

class C {
    has $.attr;
    submethod BUILD (:$attr = 42) {
        $!attr = $attr
    };
    multi method new($positional) {
        self.bless(:attr($positional), |%_)
   }
};

C.new.say; C.new('answer').say;
# OUTPUT: «C.new(attr => 42)
#          C.new(attr => "answer")»
Fallback 方法

当其他解析名称的方法不产生结果时,将调用具有特殊名称的 FALLBACK 方法。第一个参数保存名称,所有后续参数都从原始调用转发。支持 multi 方法和子签名

class Magic {
    method FALLBACK ($name, |c(Int, Str)) {
    put "$name called with parameters {c.perl}"  }
};
Magic.new.simsalabim(42, "answer");

# OUTPUT: «simsalabim called with parameters ⌈\(42, "answer")⌋»
保留方法名

一些内置的内省方法实际上是由编译器提供的特殊语法, 即 WHATWHOHOWVAR。使用这些名称声明的方法将无声地失败。动态调用将起作用,允许从外部对象调用方法。

class A {
    method WHAT { "ain't gonna happen" }
};

say A.new.WHAT;    # OUTPUT: «(A)»
say A.new."WHAT"() # OUTPUT: «ain't gonna happen»
包作用域中的方法

任何 our 作用域方法都将在类的包作用域内可见。

class C {
    our method packaged {};
    method loose {}
};
say C::.keys
# OUTPUT: «(&packaged)»
使用同名变量和方法设置属性

如果您用和属性属性同名的名字设置属性的变量(或方法调用),则可以节省一些输入,例如 attr ⇒ $attr ` 或 `:attr($attr)

class A { has $.i = 42 };
class B {
    has $.i = "answer";
    method m() { A.new(:$.i) }
    #                  ^^^^  Instead of i => $.i or :i($.i)
};
my $a = B.new.m;
say $a.i; # OUTPUT: «answer»

由于 $.i 方法调用名字叫 i 且属性也叫 i,因此 Raku 允许我们使用快捷方式。这同样适用于 :$var:$!private-attribute:&attr-with-code-in-it,等等。

trait is nodal

标记一个List方法,指示 hyperoperator 不要进入内部 Iterables 以调用此方法。这个特性通常不是终端用户会使用的东西,除非他们子类化或扩展核心 List 类型。

为了证明差异,请考虑以下示例,第一个使用 is nodal 方法(elems),第二个使用方法 Int , 它不是节点方法。

say ((1.0, "2", 3e0), [^4], '5')».elems; # OUTPUT: «(3, 4, 1)»
say ((1.0, "2", 3e0), [^4], '5')».Int    # OUTPUT: «((1 2 3) [0 1 2 3] 5)»
trait handles

定义为:

multi sub trait_mod:<handles>(Attribute:D $target, $thunk)

trait handles 应用于类的属性,会将对提供的方法名称的所有调用代理给和属性同名名的方法。必须初始化属性引用的对象。可以提供代理调用的对象的类型约束。

class A      { method m(){ 'A::m has been called.' } }
class B is A { method m(){ 'B::m has been called.' } }
class C {
    has A $.delegate handles 'm';
    method new($delegate){ self.bless(delegate => $delegate) }
};
say C.new(B.new).m(); # OUTPUT: «B::m has been called.»

可以提供一个 Pair(或用于重命名)或一个 PairRegexWhatever 的列表而不是一个方法名。在后一种情况下,在类本身及其继承链中的现有方法将优先。如果 FALLBACK 要搜索本地,请使用 HyperWhatever

class A {
    method m1(){}
    method m2(){}
}

class C {
    has $.delegate handles <m1 m2> = A.new()
}
C.new.m2;

class D {
    has $.delegate handles /m\d/ = A.new()
}
D.new.m1;

class E {
    has $.delegate handles (em1 => 'm1') = A.new()
}
E.new.em1;
trait is

定义为:

multi sub trait_mod:<is>(Mu:U $child, Mu:U $parent)

trait is 接受一个类型对象,该类型对象在其定义中被添加为类的父类。为了允许多重继承,可以多次应用 is trait。将父类添加到类中会将其方法导入目标类。如果在多个父类中出现同名方法,则第一个添加的父类将胜出。

如果没有提供 is trait,则默认值 Any 将用作父类。这迫使所有 Raku 对象具有相同的基本方法集,以提供内省和强制到基本类型的接口。

class A {
    multi method from-a(){ 'A::from-a' }
}
say A.new.^parents(:all).perl;
# OUTPUT: «(Any, Mu)»

class B {
    method from-b(){ 'B::from-b ' }
    multi method from-a(){ 'B::from-A' }
}

class C is A is B {}
say C.new.from-a();
# OUTPUT: «A::from-a»
trait is rw

定义为:

sub trait_mod:<is>(Mu:U $type, :$rw!)

类的trait is rw 在该类的所有公共属性上创建可写的访问器方法。

class C is rw {
    has $.a;
};
my $c = C.new.a = 42;
say $c; # OUTPUT: «42»
trait is required

定义为:

multi sub trait_mod:<is>(Attribute $attr, :$required!)
multi sub trait_mod:<is>(Parameter:D $param, :$required!)

将类或角色属性标记为必要的。如果在对象构造时未初始化该属性,则抛出 X::Attribute::Required

class Correct {
    has $.attr is required;
    submethod BUILD (:$attr) { $!attr = $attr }
}
say Correct.new(attr => 42);
# OUTPUT: «Correct.new(attr => 42)»

class C {
    has $.attr is required;
}
C.new;
CATCH { default { say .^name => .Str } }
# OUTPUT: «X::Attribute::Required => The attribute '$!attr' is required, but you did not provide a value for it.»

你可以为 is required 提供一个理由作为参数,说明它为什么是必须的。

class Correct {
    has $.attr is required("it's so cool")
};
say Correct.new();
# OUTPUT: «The attribute '$!attr' is required because it's so cool,but you did not provide a value for it.»
trait hides

trait hides 提供继承而不需要重新分派

class A {
    method m { say 'i am hidden' }
}
class B hides A {
    method m { nextsame }
    method n { self.A::m }
};

B.new.m;
B.new.n;
# OUTPUT: «i am hidden»

trait is hidden 允许类从 重新分派 中隐藏自己。

class A is hidden {
    method m { say 'i am hidden' }
}
class B is A {
    method m { nextsame }
    method n { self.A::m }
}

B.new.m;
B.new.n;
# OUTPUT: «i am hidden»
trait trusts

要允许一个类访问另一个类的私有方法,请使用该 trait trusts。可能需要可信类的前置声明。

class B {...};
class A {
    trusts B;
    has $!foo;
    method !foo { return-rw $!foo }
    method perl { "A.new(foo => $!foo)" }
};
class B {
    has A $.a .= new;
    method change { $!a!A::foo = 42; self }
};
say B.new.change;
# OUTPUT: «B.new(a => A.new(foo => 42))»
扩展类

要在编译时向类添加方法和属性,请在类定义片段前面使用 augment。编译器将要求编译指令 use MONKEY-TYPINGuse MONKEY 早一点出现在同一作用域中。请注意,可能会对性能产生影响,因此可能会出现问题。

use MONKEY; augment class Str {
    method mark(Any :$set){
        state $mark //= $set; $mark
    }
};
my $s = "42";
$s.mark(set => "answer");
say $s.mark
# OUTPUT: «answer»

在类片段内可以做什么的限制很少。其中之一是将方法或子方法重新声明为 multi 方法。使用添加的属性尚未被实现。请注意,添加仅在其命名参数方面不同的多候选项将在已定义的候选项后面添加该候选项,因此调度程序不会选择该候选项。

版本和作者

版权和作者身份可以通过副词 :ver<>:auth<> 应用。两者都以字符串作为参数,对于 :ver, 字符串被转换为 Version 对象。查询类版本和作者请使用 .^ver^.auth

class C:ver<4.2.3>:auth<me@here.local> {}
say [C.^ver, C.^auth];
# OUTPUT: «[v4.2.3 me@here.local]»

41.2.2. role

角色是类片段,它允许定义类共享的接口。role 声明符还引入了可用于类型检查的类型对象。角色可以在运行时和编译时混合到类和对象中。role 声明符返回创建的类型对象因而允许匿名角色和就地混入定义。

role Serialize {
    method to-string { self.Str }
    method to-number { self.Num }
}

class A does Serialize {}
class B does Serialize {}

my Serialize @list;
@list.push: A.new;
@list.push: B.new;

say @list».to-string;
# OUTPUT: «[A<57192848> B<57192880>]»

使用 …​ 作为方法体的唯一元素声明一个要抽象的方法。任何混合使用这种方法的类都必须重载它。如果在编译单元结束之前该方法没有被重载,则抛出 X::Comp::AdHoc

EVAL 'role R { method overload-this(){...} }; class A does R {}; ';
CATCH { default { say .^name, ' ', .Str } }
# OUTPUT: «X::Comp::AdHoc Method 'overload-this' must be implemented by A because it is required by roles: R.»
自动双关

可以使用角色而不是类来创建对象。由于角色在运行时不能存在,因此会创建一个同名的类,该类将对角色类型检查成功。

role R { method m { say 'oi‽' } };
R.new.^mro.say;
# OUTPUT: «((R) (Any) (Mu))»
say R.new.^mro[0].HOW.^name;
# OUTPUT: «Raku::Metamodel::ClassHOW»
say R.new ~~ R;
# OUTPUT: «True»
trait does

trait does 可以应用于提供编译时混合的角色和类。要引用尚未定义的角色,请使用前置声明。混合角色的类的类型名称不反射 mixin,类型检查反射。如果在多个混合角色中提供方法,则首先定义的方法优先。可以提供以逗号分隔的角色列表。在这种情况下,将在编译时报告冲突。

role R2 {...};
role R1 does R2 {};
role R2 {};
class C does R1 {};

say [C ~~ R1, C ~~ R2];
# OUTPUT: «[True True]»

对于运行时混入请参阅butdoes

参数化

可以在角色名称后面的 https://docs.raku.org/type/Signature#Type_captures 之间提供角色的参数。支持[类型捕获]。

role R[$d] { has $.a = $d };
class C does R["default"] { };

my $c = C.new;
say $c;
# OUTPUT: «C.new(a => "default")»

参数可以有类型约束,类型不支持 where 子句,但可以通过 subset 实现。

class A {};
class B {};
subset A-or-B where * ~~ A|B;
role R[A-or-B ::T] {};
R[A.new].new;

可以提供默认参数。

role R[$p = fail("Please provide a parameter to role R")] {};
my $i = 1 does R;
CATCH { default { say .^name, ': ', .Str} }
# OUTPUT: «X::AdHoc: Could not instantiate role 'R':Please provide a parameter to role R»
As 类型约束

在期望类型的任何地方,角色都可以用作类型约束。如果使用 doesbut 混合角色,则其 type-object 将添加到相关对象的 type-object 列表中。如果使用角色而不是类(使用自动生成),则自动生成的类与角色同名的类型对象将添加到继承链中。

role Unitish[$unit = fail('Please provide a SI unit quantifier as a parameter to the role Unitish')] {
    has $.SI-unit-symbol = $unit;
    method gist {
        given self {
            # ...
            when * < 1 { return self * 1000 ~ 'm' ~ $.SI-unit-symbol }
            when * < 1000 { return self ~ $.SI-unit-symbol }
            when * < 1_000_000 { return self / 1_000 ~ 'k' ~ $.SI-unit-symbol }
            # ...
        }
    }
}

role SI-second   does Unitish[<s>] {}
role SI-meter    does Unitish[<m>] {}
role SI-kilogram does Unitish[<g>] {}

sub postfix:<s>(Numeric $num) { ($num) does SI-second }
sub postfix:<m>(Numeric $num) { ($num) does SI-meter }
sub postfix:<g>(Numeric $num) { ($num) does SI-kilogram }
sub postfix:<kg>(Numeric $num){ ($num * 1000) does SI-kilogram }

constant g = 9.806_65;

role SI-Newton does Unitish[<N>] {}

multi sub N(SI-kilogram $kg, SI-meter $m, SI-second $s --> SI-Newton ){ ($kg * ($m / $s²)) does SI-Newton }
multi sub N(SI-kilogram $kg --> SI-Newton)                            { ($kg * g) does SI-Newton }

say [75kg, N(75kg)];
# OUTPUT: «[75kg 735.49875kN]»
say [(75kg).^name, N(75kg).^name];
# OUTPUT: «[Int+{SI-kilogram} Rat+{SI-Newton}]»

41.2.3. enum

枚举提供具有关联类型的常量键-值对。任何键都属于该类型,并作为符号注入当前作用域。如果使用该符号,则将其视为常量表达式,并将该符号替换为枚举对的值。任何枚举都从角色 Enumeration 继承方法。不支持用于生成键值对的复杂表达式。通常,enum 是一个 Map 其元素具有混合的`Enumeration` 角色; 对于每个元素,此角色包括在 map 上创建顺序的索引。

符号的字符串化,在字符串上下文中自动完成,并且与其名称完全相同,这也是枚举对的键。

enum Names ( name1 => 1, name2 => 2 );
say name1, ' ', name2; # OUTPUT: «name1 name2»
say name1.value, ' ', name2.value; # OUTPUT: «1 2»

比较符号将使用类型信息和枚举对的值。支持 Num 类型和 Str 类型。

enum Names ( name1 => 1, name2 => 2 );
sub same(Names $a, Names $b){
   $a eqv $b
}

say same(name1, name1); # OUTPUT: «True»
say same(name1, name2); # OUTPUT: «False»
my $a = name1;
say $a ~~ Names; # OUTPUT: «True»
say $a.^name;    # OUTPUT: «Names»

所有键必须属于同一类型。

enum Mass ( mg => 1/1000, g => 1/1, kg => 1000/1 );

say Mass.enums;
# OUTPUT: «Map.new((g => 1, kg => 1000, mg => 0.001))»

如果没有给出值,则 Int 将假定为值类型,并且每个键从零开始递增 1。作为枚举键类型 IntNumRatStr 都被支持。

enum Numbers <one two three four>;

say Numbers.enums;
# OUTPUT: «Map.new((four => 3, one => 0, three => 2, two => 1))»

可以提供不同的起始值。

enum Numbers «:one(1) two three four»;

say Numbers.enums;
# OUTPUT: «Map.new((four => 4, one => 1, three => 3, two => 2))»

枚举也可以是匿名的,和具名 enum 的唯一的区别在于您不能在签名中使用它或用它声明变量。

my $e = enum <one two three>;
say two;       # OUTPUT: «two»
say one.^name; # OUTPUT: «»
say $e.^name;  # OUTPUT: «Map»

有多种方法可以访问已定义的符号的键和值。所有这些都将值转换为 Str,这可能是不可取的。通过将枚举视为包,我们可以获得键的类型列表。

enum E(<one two>);
my @keys = E::.values;
say @keys.map: *.enums;
# OUTPUT: «(Map.new((one => 0, two => 1)) Map.new((one => 0, two => 1)))»
元类

要测试给定类型对象是否为 enum,请 .HOW 针对 Metamodel::EnumHOW 测试元对象方法,或者仅针对该 Enumeration 角色进行测试。

enum E(<a b c>);
say E.HOW ~~ Metamodel::EnumHOW; # OUTPUT: «True»
say E ~~ Enumeration;            # OUTPUT: «True»
Methods
method enums

定义为:

method enums()

返回枚举对列表。

enum Mass ( mg => 1/1000, g => 1/1, kg => 1000/1 );
say Mass.enums; # OUTPUT: «{g => 1, kg => 1000, mg => 0.001}»
Coercion

如果要将枚举元素的值强制转换为其合适的枚举对象,请使用带有枚举名称的 coercer:

my enum A (sun => 42, mon => 72);
A(72).pair.say;   # OUTPUT: «mon => 72»
A(1000).say; # OUTPUT: «(A)»

最后一个示例显示了如果没有枚举对包含它作为值会发生什么。

41.2.4. module

模块通常是一个或多个公开 Raku 结构的源文件,例如类,角色,grammars,子例程和变量。模块通常用于将 Raku 代码分发为可在另一个 Raku 程序中使用的库。

有关完整说明,请参阅模块

41.2.5. package

Packages are nested namespaces of named program elements. Modules, classes and grammars are all types of package.

For a full explanation see Packages.

包是命名程序元素的嵌套命名空间。模块,类和语法都是所有类型的包。

有关完整说明,请参阅

41.2.6. grammar

Grammar 是用于解析文本的特定类型。Grammars 由 rule,token 和 regex 组成,它们实际上是方法,因为 grammars 是类。

有关完整说明,请参阅Grammars

版本和作者

版权和作者身份可以通过副词 :ver<>:auth<> 应用。两者都以字符串作为参数,对于 :ver, 字符串被转换为 Version对象。查询语法版本和作者使用 .^ver^.auth

grammar G:ver<4.2.3>:auth<me@here.local> {}
say [G.^ver, G.^auth];
# OUTPUT: «[v4.2.3 me@here.local]»

41.2.7. subset

subset 声明一个会重新分配到其基类型的新类型。如果提供了 where 子句,则将针对给定的代码对象检查任何赋值。

subset Positive of Int where * > -1;
my Positive $i = 1;
$i = -42;
CATCH { default { put .^name,': ', .Str } }
# OUTPUT: «X::TypeCheck::Assignment: Type check failed in assignment to $i; expected Positive but got Int (-42)»

Subsets 可用于签名,例如通过键入下面的输出:

subset Foo of List where (Int,Str);
sub a($a, $b, --> Foo) { $a, $b }
# Only a List with the first element being an Int and the second a Str will pass the type check.
a(1, "foo");  # passes
a("foo", 1);  # fails

Subsets 可以是匿名的,允许在需要 subset 的情况下进行内联放置,但名字既不需要也不值得。

my enum E1 <A B>;
my enum E2 <C D>;
sub g(@a where { .all ~~ subset::where E1|E2 } ) {
    say @a
}
g([A, C]);
# OUTPUT: «[A C]»

Subsets 可用于动态检查类型,这可以与 require 结合使用。

require ::('YourModule');
subset C where ::('YourModule::C');

42. 散列和映射

42.1. 关联角色和关联类

关联角色是 Hash 和 Map 以及 MixHash 等其他类的基础。它定义了将在关联类中使用的两种类型; 默认情况下,您可以使用任何内容(字面意思,因为任何 Any 子类的类都可以使用)作为键keyof 方法。

默认情况下,使用 % sigil 声明的任何对象都将获得 Associative 角色,默认情况下将表现为散列,但此角色仅提供上述两种方法,以及默认的 Hash 行为。

say (%).^name ; # 输出 Hash

相反,如果未混入 Associative 角色,则不能使用 % sigil,但由于此角色没有任何关联属性,因此你必须重新定义散列下标操作符的行为。为此,你必须重写几个函数:

class Logger does Associative[Cool,DateTime] {
    has %.store;

    method log( Cool $event ) {
        %.store{ DateTime.new( now ) } = $event;
    }

    multi method AT-KEY ( ::?CLASS:D: $key) {
        my @keys = %.store.keys.grep( /$key/ );
        %.store{ @keys };
    }

    multi method EXISTS-KEY (::?CLASS:D: $key) {
        %.store.keys.grep( /$key/ )??True!!False;
    }

    multi method DELETE-KEY (::?CLASS:D: $key) {
        X::Assignment::RO.new.throw;
    }

    multi method ASSIGN-KEY (::?CLASS:D: $key, $new) {
        X::Assignment::RO.new.throw;
    }

    multi method BIND-KEY (::?CLASS:D: $key, \new){
        X::Assignment::RO.new.throw;
    }
}
say Logger.of;                   # OUTPUT: «(Cool)»
my %logger := Logger.new;
say %logger.of;                  # OUTPUT: «(Cool)»

%logger.log( "Stuff" );
%logger.log( "More stuff");

say %logger<2018-05-26>;         # OUTPUT: «(More stuff Stuff)»
say %logger<2018-04-22>:exists;  # OUTPUT: «False»

在这里,我们定义了一个具有关联语义的 logger,它可以使用日期(或其中一部分)作为键。由于我们将参数化 Associative 为那些特定类,of 将返回我们使用的值类型,在这里为 Cool(我们只能记录列表或字符串)。混合 Associative 角色赋予其使用 % sigil 的权利; 因为 %-sigilled 变量默认获得 Hash 类型,所以在定义中需要绑定。

此 log 将仅附加,这就是为什么我们转义关联数组隐喻以使用 log 方法向日志添加新事件。但是,一旦添加它们,我们就可以按日期检索它们或检查它们是否存在。对于第一个,我们必须重写 AT-KEY multi 方法,对于后者 EXIST-KEY。在最后两个语句中,我们展示了下标操作如何调用 AT-KEY,而 :exists 副词调用 EXISTS-KEY

我们重写 DELETE-KEYASSIGN-KEYBIND-KEY,但只抛出异常。尝试赋值,删除或绑定值到键上将导致 Cannot modify an immutable Str (value) 异常抛出。

使类关联提供了一种使用哈希来使用和使用它们的非常方便的方法; 在 Cro 中可以看到一个例子,它广泛使用它来方便使用哈希定义结构化请求并表达其响应。

42.2. 可变哈希和不可变映射

Hash 是从键到值的可变映射(在其他编程语言中称为字典,哈希表或映射)。这些值都是标量容器,这意味着你可以给它们赋值。另一方面,Maps是不可变的。键与值配对后,此配对无法更改。

Maps 和 hashes 通常存储在百分号 % 变量中,用于表示它们是关联的(Associative)。

通过 {} postcircumfix 运算符使用键访问 Hash 和 map 元素:

say %*ENV{'HOME', 'PATH'}.perl;
# OUTPUT: «("/home/camelia", "/usr/bin:/sbin:/bin")␤»

一般的下标规则适用于提供字符串字面量列表的快捷方式,包括插值和不插值。

my %h = oranges => 'round', bananas => 'bendy';
say %h<oranges bananas>;
# OUTPUT: «(round bendy)␤»

my $fruit = 'bananas';
say %h«oranges "$fruit"»;
# OUTPUT: «(round bendy)␤»

您只需分配一个未使用的键即可添加新对:

my %h;
%h{'new key'} = 'new value';

42.3. Hash 赋值

将一个元素列表赋值给一个哈希变量首先清空该变量,然后迭代右侧的元素。如果元素是 Pair,则将其键作为新的哈希键,并将其值作为该键的新哈希值。否则,该值被强制转换为 Str 并用作散列键,而列表的下一个元素则被视为相应的值。

my %h = 'a', 'b', c => 'd', 'e', 'f';

等价于

my %h = a => 'b', c => 'd', e => 'f';

或者

my %h = <a b c d e f>;

甚至

my %h = %( a => 'b', c => 'd', e => 'f')

或者

my $h = { a => 'b', c => 'd', e => 'f'};

请注意,花括号仅在我们未将其分配给 %-sigilled 变量的情况下使用;如果我们将它用于 %-sigilled 变量,我们将遇到 Potential difficulties:␤ Useless use of hash composer on right side of hash assignment; did you mean := instead? 的错误。但是,正如此错误所示,只要我们使用绑定,我们就可以使用花括号:

my %h := { a => 'b', c => 'd', e => 'f'};
say %h; # OUTPUT: «{a => b, c => d, e => f}␤»

嵌套哈希也可以使用相同的语法定义:

my %h =  e => f => 'g';
say %h<e><f>; # OUTPUT: «g␤»

但是,你在这里定义的是一个指向 Pair 的键,如果你想要的话,这很好,如果你的嵌套哈希有一个键。但是 %h <e> 将指向 Pair 会产生这些后果:

my %h =  e => f => 'g';
%h<e><q> = 'k';
# OUTPUT: «(exit code 1) Pair␤Cannot modify an immutable Str (Nil)␤  in block <unit>»

但是,这将有效地定义嵌套哈希:

my %h =  e => { f => 'g'};
say %h<e>.^name;  # OUTPUT: «Hash␤»
say %h<e><f>;     # OUTPUT: «g␤»

如果遇到期望值的 Pair,则将其用作哈希值:

my %h = 'a', 'b' => 'c';
say %h<a>.^name;            # OUTPUT: «Pair␤»
say %h<a>.key;              # OUTPUT: «b␤»

如果同一个键出现多次,则与其最后一次出现的值存储在哈希中:

my %h = a => 1, a => 2;
say %h<a>;                  # OUTPUT: «2␤»

要将哈希值分配给不具有%sigil的变量,可以使用%()哈希构造函数:

my $h = %( a => 1, b => 2 );
say $h.^name;               # OUTPUT: «Hash␤»
say $h<a>;                  # OUTPUT: «1␤»

如果一个或多个值引用主题变量$ _,则赋值的右侧将被解释为,而不是哈希:

my @people = [
    %( id => "1A", firstName => "Andy", lastName => "Adams" ),
    %( id => "2B", firstName => "Beth", lastName => "Burke" ),
    # ...
];

sub lookup-user (Hash $h) { #`(Do something...) $h }

my @names = map {
    # While this creates a hash:
    my  $query = { name => "$person<firstName> $person<lastName>" };
    say $query.^name;      # OUTPUT: «Hash␤»

    # Doing this will create a Block. Oh no!
    my  $query2 = { name => "$_<firstName> $_<lastName>" };
    say $query2.^name;       # OUTPUT: «Block␤»
    say $query2<name>;       # fails

    CATCH { default { put .^name, ': ', .Str } };
    # OUTPUT: «X::AdHoc: Type Block does not support associative indexing.␤»
    lookup-user($query);   # Type check failed in binding $h; expected Hash but got Block
}, @people;

如果您使用了%()哈希构造函数,则可以避免这种情况。仅使用花括号来创建块。

42.4. Hash 切片

您可以使用切片同时分配多个键。

my %h; %h<a b c> = 2 xx *; %h.perl.say;  # OUTPUT: «{:a(2), :b(2), :c(2)}␤»
my %h; %h<a b c> = ^3;     %h.perl.say;  # OUTPUT: «{:a(0), :b(1), :c(2)}␤»

42.5. 非字符串键(对象哈希)

默认情况下,{} 中的键被强制为字符串。要使用非字符串键组合散列,请使用冒号前缀:

my $when = :{ (now) => "Instant", (DateTime.now) => "DateTime" };

请注意,将对象作为键,您通常无法使用<…​>构造进行键查找,因为它只创建字符串和同形异义。请改用{…​}:

:{  0  => 42 }<0>.say;   # Int    as key, IntStr in lookup; OUTPUT: «(Any)␤»
:{  0  => 42 }{0}.say;   # Int    as key, Int    in lookup; OUTPUT: «42␤»
:{ '0' => 42 }<0>.say;   # Str    as key, IntStr in lookup; OUTPUT: «(Any)␤»
:{ '0' => 42 }{'0'}.say; # Str    as key, Str    in lookup; OUTPUT: «42␤»
:{ <0> => 42 }<0>.say;   # IntStr as key, IntStr in lookup; OUTPUT: «42␤»

注意:Rakudo实现目前错误地对{}应用与{}相同的规则,并且可以在某些情况下构造块。为避免这种情况,您可以直接实例化参数化哈希。还支持%-sigiled变量的参数化:

my Num %foo1      = "0" => 0e0; # Str keys and Num values
my     %foo2{Int} =  0  => "x"; # Int keys and Any values
my Num %foo3{Int} =  0  => 0e0; # Int keys and Num values
Hash[Num,Int].new: 0, 0e0;      # Int keys and Num values

现在,如果您要定义一个哈希来保存您正在使用的对象作为您提供给哈希用作键的确切对象的键,那么对象哈希就是您要查找的内容。

my %intervals{Instant};
my $first-instant = now;
%intervals{ $first-instant } = "Our first milestone.";
sleep 1;
my $second-instant = now;
%intervals{ $second-instant } = "Logging this Instant for spurious raisins.";
for %intervals.sort -> (:$key, :$value) {
    state $last-instant //= $key;
    say "We noted '$value' at $key, with an interval of {$key - $last-instant}";
    $last-instant = $key;
}

此示例使用仅接受Instant类型的键的对象哈希来实现基本但类型安全的日志记录机制。我们利用一个命名的状态变量来跟踪前一个Instant,以便我们可以提供一个间隔。

对象哈希的整个要点是将密钥保持为对象本身。当前对象散列利用对象的WHICH方法,该方法返回每个可变对象的唯一标识符。这是对象标识运算符(===)所依赖的基石。顺序和容器在这里真的很重要,因为.keys的顺序是未定义的,一个匿名列表永远不会===到另一个。

my %intervals{Instant};
my $first-instant = now;
%intervals{ $first-instant } = "Our first milestone.";
sleep 1;
my $second-instant = now;
%intervals{ $second-instant } = "Logging this Instant for spurious raisins.";
say ($first-instant, $second-instant) ~~ %intervals.keys;       # OUTPUT: «False␤»
say ($first-instant, $second-instant) ~~ %intervals.keys.sort;  # OUTPUT: «False␤»
say ($first-instant, $second-instant) === %intervals.keys.sort; # OUTPUT: «False␤»
say $first-instant === %intervals.keys.sort[0];                 # OUTPUT: «True␤»

由于Instant定义了自己的比较方法,因此在我们的示例中,根据cmp的排序将始终提供最早的即时对象作为它返回的List中的第一个元素。

如果您想接受哈希中的任何对象,可以使用Any!

my %h{Any};
%h{(now)} = "This is an Instant";
%h{(DateTime.now)} = "This is a DateTime, which is not an Instant";
%h{"completely different"} = "Monty Python references are neither DateTimes nor Instants";

有一种更简洁的语法,它使用绑定。

my %h := :{ (now) => "Instant", (DateTime.now) => "DateTime" };

绑定是必要的,因为对象哈希是关于非常可靠的特定对象,这是绑定在跟踪哪些任务并不关心哪些很好的事情。

42.6. 约束值类型

在声明符和名称之间放置一个类型对象,以约束哈希值的所有值的类型。使用具有where子句的约束的子集。

subset Powerful of Int where * > 9000;
my Powerful %h{Str};
put %h<Goku>   = 9001;
try {
    %h<Vegeta> = 900;
    CATCH { when X::TypeCheck::Binding { .message.put } }
}

# OUTPUT:
# 9001
# Type check failed in binding assignval; expected Powerful but got Int (900)

42.7. 循环哈希键和值

处理散列中元素的常用习惯是循环键和值,例如,

my %vowels = 'a' => 1, 'e' => 2, 'i' => 3, 'o' => 4, 'u' => 5;
for %vowels.kv -> $vowel, $index {
  "$vowel: $index".say;
}

给出与此类似的输出:

a: 1
e: 2
o: 4
u: 5
i: 3

我们使用kv方法从散列中提取键及其各自的值,以便我们可以将这些值传递给循环。

请注意,不能依赖打印的键和值的顺序;对于同一程序的不同运行,散列的元素并不总是以相同的方式存储在内存中。事实上,从版本2018.05开始,订单在每次调用时都保证不同。有时人们希望处理排序的元素,例如哈希的键。如果有人希望按字母顺序打印元音列表,那么就会写一个

my %vowels = 'a' => 1, 'e' => 2, 'i' => 3, 'o' => 4, 'u' => 5;
for %vowels.sort(*.key)>>.kv -> ($vowel, $index) {
  "$vowel: $index".say;
}

打印

a: 1
e: 2
i: 3
o: 4
u: 5

按字母顺序排列。为了达到这个结果,我们按键(%vowels.sort(* .key))对元音的哈希值进行排序,然后通过将.kv方法应用于每个元素,通过一元>> superroperator生成元数据和值。键/值列表的列表。为了提取键/值,变量因此需要包含在括号中。

另一种解决方案是展平结果列表。然后可以使用与.kv相同的方式访问键/值对:

my %vowels = 'a' => 1, 'e' => 2, 'i' => 3, 'o' => 4, 'u' => 5;
for %vowels.sort(*.key)>>.kv.flat -> $vowel, $index {
  "$vowel: $index".say;
}

您还可以使用解构来循环哈希。

42.8. 就地编辑值

有时您可能希望在迭代时修改哈希值。

my %answers = illuminatus => 23, hitchhikers => 42;
# OUTPUT: «hitchhikers => 42, illuminatus => 23»
for %answers.values -> $v { $v += 10 }; # Fails
CATCH { default { put .^name, ': ', .Str } };
# OUTPUT: «X::AdHoc: Cannot assign to a readonly variable or a value␤»

传统上,这是通过发送密钥和值来完成的,如下所示。

my %answers = illuminatus => 23, hitchhikers => 42;
for %answers.kv -> $k,$v { %answers{$k} = $v + 10 };

但是,可以利用块的签名来指定您希望对值进行读写访问。

my %answers = illuminatus => 23, hitchhikers => 42;
for %answers.values -> $v is rw { $v += 10 };

但是,即使在对象哈希的情况下,也不可能进行哈希键的就地编辑。

43. 原生调用接口

43.1. 入门指南

能想象出的最简单的 NativeCall 用法应该类似于这样的东西:

use NativeCall;
sub some_argless_function() is native('something') { * }
some_argless_function();

第一行导入了各种 traits 和类型,接下来的一行看起来很像相对普通的 Raku 子例程声明 - 稍微有点变化。我们使用native这个 trait ` 是为了指定这个 sub 子例程实际上被定义在原生库中。Raku 会给你添加特定平台的扩展名(比如 `.so 或者 .dll)还有任何惯常的前缀(例如: 'lib')。

当你第一次调用 “some_argless_function” 时,“libsomething” 将会被加载,然后会在 libsomething 库中定位到 “some_argless_function” 函数,接下来将会进行一次调用。之后的调用将会更快,因为符号句柄会被保留。

当然,大部分的函数都会接受参数或者返回值 - 但是你可以做的其他事情只是增加了这个声明Raku sub的简单模式

但是一切你需要做的就是增加这个简单的模式,通过声明一个 Raku 的过程、在符号后面指出你想要调用的名字,并且使用 “native” trait。

43.2. 改变名字

有时你想要 Raku 子例程的名字和加载库中使用的名字不同,可能这个名字很长, 或者有不同的大小写或者在你想要创建的模块的上下文中, 这个名字很繁琐。

NativeCall 为你提供了一个 symbol trait 以指定库中原生子例程的名字, 这个名字和你的 Raku 子例程名字不同。

module Foo;
use NativeCall;
our sub init() is native('foo') is symbol('FOO_INIT') { * }

libfoo 库里面有一个子例程叫 FOO_INIT,因为我们创建了一个模块叫做 Foo,我们更愿意使用 Foo::init 调用子例程,我们使用 symbol trait 来指定在 libfoo 库名字符号的名字,然后以任何我们想要的方式调用这个子例程(这里是 “init”)。

43.3. 传递值和返回值

普通的 Raku 签名和 returns trait 的使用是为了传送原生函数期望的参数类型以及返回的东西,下面有个例子:

sub add(int32, int32) returns int32 is native('calculator') { * }

在这里,我们声明该函数接受两个32位整数,返回一个32位整数。你可以在原生类型页面中找到可以传递的其他类型。 请注意,缺少 returns trait 用于指示 void 返回类型。 除指针参数化外,不要在任何地方使用 void 类型。

对于字符串,还有一个额外的 encoded trait,可以提供一些关于如何进行编组的额外提示。

use NativeCall;
sub message_box(Str is encoded('utf8')) is native('gui') { * }

为了指定如何对返回类型进行编组,只需在子例程自身应用这个 trait 即可。

use NativeCall;
sub input_box() returns Str is encoded('utf8') is native('gui') { * }

注意, 可以通过传递 Str 类型对象来传递 NULL 字符串指针; NULL 返回也将由类型对象表示。

如果 C 函数要求字符串的生命周期超过函数调用,则必须手动编码该参数并将其作为 CArray[uint8] 传递:

use NativeCall;
# C prototype is void set_foo(const char *)
sub set_foo(CArray[uint8]) is native('foo') { * }
# C prototype is void use_foo(void)
sub use_foo() is native('foo') { * } # will use pointer stored by set_foo()

my $string = "FOO";
# The lifetime of this variable must be equal to the required lifetime of
# the data passed to the C function.
my $array = CArray[uint8].new($string.encode.list);

set_foo($array);
# ...
use_foo();
# It's fine if $array goes out of scope starting from here.

43.4. 指定原生表示

使用原生函数时,有时需要指定要使用的原生数据结构类型。 is repr 是用于此的术语。

use NativeCall;

class timespec is repr('CStruct') {
    has uint32 $.tv_sec;
    has long $.tv_nanosecs;
}

sub clock_gettime(uint32 $clock-id, timespec $tspec --> uint32) is native { * };

my timespec $this-time .=new;

my $result = clock_gettime( 0, $this-time);

say "$result, $this-time"; # OUTPUT: «0, timespec<65385480>␤»

我们调用的原始函数, clock_gettime 使用指向 timespec 结构的指针作为第二个参数。 我们在这里将它声明为一个,但是将其表示指定为 repr('CStruct'), 以指示它对应于 C 数据结构。 当我们创建该类的对象时,我们正在创建 clock_gettime 所期望的指针类型。 这样,数据可以无缝地传输到原生接口和从原生接口传输。

43.5. 指针的基本使用

当你的原生函数签名需要一个指向某些原生类型(int32uint32`等等)的指针时,所有你需要做的就是将参数声明为 `is rw

use NativeCall;
# C prototype is void my_version(int *major, int *minor)
sub my_version(int32 is rw, int32 is rw) is native('foo') { * }
my_version(my int32 $major, my int32 $minor); # Pass a pointer to

有的时候你需要获取一个从 C 库返回的指针(比如一个库句柄),你不关心它指向什么 - 你只需要保存它就可以了,Pointer 类型就是为此而生的:

use NativeCall;
sub Foo_init() returns Pointer is native("foo") { * }
sub Foo_free(Pointer) is native("foo") { * }

这个可以正常工作,但是你可能想要使用比 Pointer 更好的类型,事实证明,任何具有表示“CPointer”的类都可以担任此角色,这意味着你可以通过编写如下类来暴露工作在句柄上的库:

use NativeCall;

class FooHandle is repr('CPointer') {
    # Here are the actual NativeCall functions.
    sub Foo_init() returns FooHandle is native("foo") { * }
    sub Foo_free(FooHandle) is native("foo") { * }
    sub Foo_query(FooHandle, Str) returns int8 is native("foo") { * }
    sub Foo_close(FooHandle) returns int8 is native("foo") { * }

    # Here are the methods we use to expose it to the outside world.
    method new {
        Foo_init();
    }

    method query(Str $stmt) {
        Foo_query(self, $stmt);
    }

    method close {
        Foo_close(self);
    }

    # Free data when the object is garbage collected.
    submethod DESTROY {
        Foo_free(self);
    }
}

请注意,CPointer 表示只能保存 C 指针。 这意味着你的类不能有额外的属性。 但是,对于简单的库,这可能是向其暴露面向对象的接口的一种巧妙方式。

当然,你总是可以有一个空类:

class DoorHandle is repr('CPointer') { }

只需像使用 Pointer 一样使用类,但有可能提高类型安全性和更易读的代码。

同样,类型对象用于表示 NULL 指针。

43.6. 函数指针

C 库可以将指向 C 函数的指针暴露为函数的返回值和结构体的成员,例如 structs 和 unions。

使用定义所需函数参数和返回值的签名调用函数“f”返回的函数指针“$fptr”的示例:

sub f() returns Pointer is native('mylib') { * }

my $fptr    = f();
my $nfptr   = nativecast(:(Str, size_t --> int32), $fptr);

say $nfptr("test", 4);

43.7. 数组

NativeCall 对数组有一些支持。 它受限于使用机器大小的整数,双精度和字符串,定型的数字类型,指针数组,结构体数组和数组的数组。

Raku 数组支持懒惰,在内存中以与 C 数组完全不同的方式布局。 因此,NativeCall 库提供了更原始的 CArray 类型,如果使用 C 数组,则必须使用该类型。

这是传递 C 数组的示例。

sub RenderBarChart(Str, int32, CArray[Str], CArray[num64]) is native("chart") { * }
my @titles := CArray[Str].new;
@titles[0]  = 'Me';
@titles[1]  = 'You';
@titles[2]  = 'Hagrid';
my @values := CArray[num64].new;
@values[0]  = 59.5e0;
@values[1]  = 61.2e0;
@values[2]  = 180.7e0;
RenderBarChart('Weights (kg)', 3, @titles, @values);

注意我们对 @titles 使用了绑定,而不是赋值,如果你使用赋值,则会把值放进 Raku 数组,然后它就不会工作了。如果这令你抓狂,忘记你所知道的关于 @ 符号的事情,使用 NativeCall 的时候直接使用 $ 吧。

use NativeCall;
my $titles = CArray[Str].new;
$titles[0] = 'Me';
$titles[1] = 'You';
$titles[2] = 'Hagrid';

获取数组的返回值也是一样的。

某些库 API 可能会将数组作为缓冲区,将由 C 函数填充,例如,返回填充的实际项数:

use NativeCall;
sub get_n_ints(CArray[int32], int32) returns int32 is native('ints') { * }

在这些情况下,重要的是 CArray 在将其传递给原生子例程之前至少具有要填充的元素的数量,否则 C 函数可能会遍历 Perl 的内存,从而可能导致不可预测的行为:

my $number_of_ints = 10;
my $ints = CArray[int32].allocate($number_of_ints); # instantiates an array with 10 elements
my $n = get_n_ints($ints, $number_of_ints);

注意:allocate 是在 Rakudo 2018.05 中引入的。 在此之前,你必须使用此机制将数组扩展为许多元素:

my $ints = CArray[int32].new;
my $number_of_ints = 10;
$ints[$number_of_ints - 1] = 0; # extend the array to 10 items

数组的内存管理很重要。 当你自己创建一个数组时,可以根据需要为其添加元素,并根据需要为你进行扩展。 但是,这可能会导致元素在内存中移动(但是,对现有元素的赋值永远不会导致这种情况)。 这意味着如果在将数组传递给 C 库之后将数组旋转,你最好知道自己在做什么。

相比之下,当 C 库向你返回一个数组时,内存不能由 NativeCall 管理,并且它不知道数组的结束位置。 据推测,库 API 中的某些东西告诉你这一点(例如,你知道当你看到一个 null 元素时,你应该不再读取)。 请注意,NativeCall 在这里无法为您提供任何保护 - 一旦做错了,你将遇到 segfault 错误或导致内存损坏。 这不是 NativeCall 的缺点,它是原生世界的工作方式。害怕吗? 还在这里,拥抱一下。 祝好运!

43.8. CArray 方法

除了每个 Raku 实例上可用的常用方法之外,CArray 还提供了以下方法,可以从 Raku 的角度与它进行交互:

  • elems 提供数组中的元素数量;

  • AT-POS 在给定位置提供特定元素(从零开始);

  • list 提供了从原生数组迭代器构建它的数组中的元素列表

例如,请考虑以下简单的代码:

use NativeCall;

my $native-array = CArray[int32].new( 1, 2, 3, 4, 5 );
say 'Number of elements: ' ~ $native-array.elems;

# walk the array
for $native-array.list -> $elem {
    say "Current element is: $elem";
}

# get every element by its index-based position
for 0..$native-array.elems - 1 -> $position {
    say "Element at position $position is "
          ~ $native-array.AT-POS( $position );
}

产生以下输出:

Number of elements: 5
Current element is: 1
Current element is: 2
Current element is: 3
Current element is: 4
Current element is: 5
Element at position 0 is 1
Element at position 1 is 2
Element at position 2 is 3
Element at position 3 is 4
Element at position 4 is 5

43.9. 结构体

由于表示多态性,可以声明一个看起来很正常的 Raku 类,实际上,C 编译器将它们放置在类似的结构体定义中以相同的方式存储其属性。 所需要的只是快速使用“repr” trait:

class Point is repr('CStruct') {
    has num64 $.x;
    has num64 $.y;
}

声明的属性只能是 NativeCall 已知的可以转换成结构体字段的类型,目前,结构体中可以包含机器大小的整数,doubles,strings 以及其它 NativeCall 对象(CArrays,还有 CPointer 以及 CStruct reprs)。除此之外,你可以做一些跟类一样的常用的设置,你甚至可以让某些属性来自于角色或者从其它的类继承。当然,方法也完全没有问题,疯狂!

CStruct 对象以引用的形式传递到原生函数,并且原生函数必须返回 CStruct 对象的引用,对于这些引用的内存管理规则跟数组的内存管理规则很像,尽管更简单,因为结构体的大小是不变的。当你创建一个结构体,内存也一并为你分配好,当指向 CStruct 实例的变量的生命期结束,GC 会负责释放内存。当基于 CStruct 的类型作为原生函数的返回类型时,GC 并不帮你管理它的内存。

NativeCall 目前并不把对象成员放到容器里面,所以不能对对象进行赋(使用 =)新值。 相反,你必须将新值绑定到私有成员上:

class MyStruct is repr('CStruct') {
    has CArray[num64] $!arr;
    has Str $!str;
    has Point $!point; # Point is a user-defined class

    submethod TWEAK {
        my $arr := CArray[num64].new;
        $arr[0] = 0.9e0;
        $arr[1] = 0.2e0;
        $!arr := $arr;
        $!str := 'Raku is fun';
        $!point := Point.new;
    }
}

正如你预测的那样,空指针由结构体类型的类型对象表示的。

43.10. CUnions

同样地,我们可以声明一个 Raku 类,它的属性拥有和 C 编译器中联合体(union)的相同的内存布局,这可以使用 CUnion 表示:

use NativeCall;

class MyUnion is repr('CUnion') {
    has int32 $.flags32;
    has int64 $.flags64;
}

say nativesizeof(MyUnion.new);  # 8, ie. max(sizeof(MyUnion.flags32), sizeof(MyUnion.flags64))

43.11. 嵌套的 CStructs 和 CUnions

反过来, CStructs 和 CUnions 可以被周围的 CStruct 和 CUnion 引用,或者嵌入到其他的 CStructs 和 CUnions 里面,如果是引用我们则像往常一样使用 has 来声明,如果是嵌入则使用 HAS 代替:

class MyStruct is repr('CStruct') {
    has Point $.point;  # referenced
    has int32 $.flags;
}

say nativesizeof(MyStruct.new);  # 16, ie. sizeof(struct Point *) + sizeof(int32_t)

class MyStruct2 is repr('CStruct') {
    HAS Point $.point;  # embedded
    has int32 $.flags;
}

say nativesizeof(MyStruct2.new);  # 24, ie. sizeof(struct Point) + sizeof(int32_t)

43.11.1. 注意内存管理

分配结构体以用作结构体时,请确保在 C 函数中分配自己的内存。 如果要将结构体传递给需要提前分配的 Str/char* 的C函数,请确保在将结构体传递给函数之前为 Str 类型的变量分配容器。

在你的 Raku 代码中…​
class AStringAndAnInt is repr("CStruct") {
  has Str $.a_string;
  has int32 $.an_int32;

  sub init_struct(AStringAndAnInt is rw, Str, int32) is native('simple-struct') { * }

  submethod BUILD(:$a_string, :$an_int) {
    init_struct(self, $a_string, $an_int);
  }
}

在此代码中,我们首先设置我们的成员 $.a_string$.an_int32。 之后,我们声明 init_struct() 函数以使 init() 方法包装; 然后从 BUILD 调用此函数以在返回创建的对象之前有效地分配值。

在你的 C 代码中 …​
typedef struct a_string_and_an_int32_t_ {
  char *a_string;
  int32_t an_int32;
} a_string_and_an_int32_t;

这是结构体。 注意我们在那里有怎么得到一个 char *

void init_struct(a_string_and_an_int32_t *target, char *str, int32_t int32) {
  target->an_int32 = int32;
  target->a_string = strdup(str);

  return;
}

在这个函数中,我们通过按值分配整数并通过引用传递字符串来初始化 C 结构体。 该函数在复制字符串时将 <point * a_string> 指向的内存分配到结构中。 (注意,你还必须管理内存的释放以避免内存泄漏。)

# A long time ago in a galaxy far, far away...
my $foo = AStringAndAnInt.new(a_string => "str", an_int => 123);
say "foo is {$foo.a_string} and {$foo.an_int32}";
# OUTPUT: «foo is str and 123␤»

43.12. 类型指针

Pointer 作为参数传递时可以类型化你的 Pointer。这不但对原生类型可用,同样适用于 CArray 以及 CStruct 定义类型,NativeCall 将不会显式为他们分配内存,即使在它们身上调用 new 方法也不会。这适用于那种 C 函数返回指针或者 CStruct 中嵌入的指针情况。

use NativeCall;
sub strdup(Str $s --> Pointer[Str]) is native {*}
my Pointer[Str] $p = strdup("Success!");
say $p.deref;

原生函数返回指向元素的数组的指针是很常见的。 可以将类型化指针解引用为数组以获取单个元素。

my $n = 5;
# returns a pointer to an array of length $n
my Pointer[Point] $plot = some_other_c_routine($n);
# display the 5 elements in the array
for 1 .. $n -> $i {
    my $x = $plot[$i - 1].x;
    my $y = $plot[$i - 1].y;
    say "$i: ($x, $y)";
}

指针也可以更新以引用数组中的连续元素:

my Pointer[Point] $elem = $plot;
# show differences between successive points
for 1 ..^ $n {
    my Point $lo = $elem.deref;
    ++$elem; # equivalent to $elem = $elem.add(1);
    my Point $hi = (++$elem).deref;
    my $dx = $hi.x = $lo.x;
    my $dy = $hi.y = $lo.y;
    say "$_: delta ($dx, $dy)";
}

通过声明 Pointervoid 也可以使用 Void 指针。 有关该主题的更多信息,请参阅[原生类型文档]。

43.13. 字符串

43.13.1. 显式内存管理

43.13.2. Buffers and Blobs

43.14. 函数参数

NativeCall 也支持把函数作为原生函数的参数,一个常用的情况就是事件驱动模型中,使用函数指针作为回调。当通过 NativeCall 绑定了这些函数,只需要提供对等的 signature 作为函数参数的约束。

# void SetCallBack(int (*callback)(char const *))
my sub SetCallBack(&callback(Str --> int32)) is native('mylib') { * }

注意:原生代码负责传递给 Raku 回调的值的内存管理,换句话说,NativeCall 将不会释放传递给回调的字符串占用的内存。

43.15. 库路径以及名字

native trait 接受库的名字或者全路径:

constant LIBMYSQL = 'mysqlclient';
constant LIBFOO = '/usr/lib/libfoo.so.1';

sub mysql_affectied_rows( .. ) returns int32 is native(LIBMYSQL);
sub bar is native(LIBFOO);

你也可以使用相对路径比如'./foo',NativeCall 将会自动根据不同的平台添加对应的扩展名。 注意:native trait 和 constant 都是在编译期求值的,constant类型的变量不要依赖动态变量,比如:

constant LIBMYSQL = %*ENV<P6LIB_MYSQLCLIENT> || 'mysqlclient';

这将在编译期保持给定的值,在一个模块预编译时,LIBMYSQL将会始终保持那个值。

43.15.1. ABI/API版本

假设你写的原生库为native('foo'), 在类Unix系统下,NativeCall 将会搜索’libfoo.so'(对于OS X是libfoo.dynlib,win32是foo.dll)。在大多数的现代系统上,将会需要你或者模块的使用者安装开发环境包,因为它们总是建议支持动态库的API/ABI的版本控制,所以’libfoo.so’大多数是一个符号链接,并且只被开发包提供。

sub foo is native('foo', v1);        # 将会查找并加载 libfoo.so.1
sub foo is native('foo', v1.2.3);    # 将会查找并加载 libfoo.so.1.2.3

my List $lib = ('foo', 'v1');
sub foo is native($lib);

43.15.2. 例程

native trait 也可以接受一个Callable作为参数,允许你使用自己的方式指定将会被加载的库文件:

sub foo is native(sub { 'libfoo.so.42' } );

这个函数只会在第一个调用者访问的时候调用。

43.15.3. 调用标准库

如果你想调用一个已经被加载的,或者是标准库或者来自你自己的程序的 C 函数,你可以将 Str 类型对象作为参数传递给is native,这将会是is native(Str)。 比如说,在类UNIX操作系统下,你可以使用下面的代码打印当前用户的home目录:

use NativeCall;
my class PwStruct is repr('CStruct') {
    has Str $.pw_name;
    has Str $.pw_passwd;
    has uint32 $.pw_uid;
    has uint32 $.pw_gid;
    has Str $.pw_gecos;
    has Str $.pw_dir;
    has Str $.pw_shell;
}

sub getuid()                returns uint32         is native(Str) { * }
sub getpwuid(uint32 $uid)    returns PwStruct     is native(Str) { * }

say getpwuid(getuid());

不过,使用 $*HOME 更方便一些 :-)

43.16. 导出的变量

一个库导出的变量 — 也被叫做“全局(global)”或者 “外部(extern)”变量 — 可以使用cglobal访问。比如:

my $var := cglobal('libc.so.6', 'error', int32);

这将会为$var绑定一个新的Proxy对象,并且将对它的访问重定向到被“libc.so.6”导出的叫做errno的整数变量。

43.17. 对C++的支持

NativeCall 也支持使用来自 c` 的类以及方法,就像这个例子展示的那样(还有相关的 `c 文件),注意现阶段还不像 C 一样支持测试和开发。

43.18. Helper 函数

43.18.1. sub nativecast

43.18.2. sub cglobal

43.18.3. sub nativesizeof

43.18.4. sub explicitly-manage

43.19. 例子

一些具体示例,以及在特定平台上使用上述示例的说明。

43.19.1. PostgreSQL

DBIish 中的 PostgreSQL 示例使用 NativeCall 库,并且`原生使用` Windows 中的原生 _putenv 函数调用。

43.19.2. MySQL

注意:请记住,自 Stretch 版本以来,Debian 已经将 MySQL 替换为 MariaDB,因此如果要安装 MySQL,请使用 MySQL APT 存储库而不是默认存储库。

要在 DBIish 中使用 MySQL 示例,您需要在本地安装 MySQL 服务器; 在Debian-esque 系统上,它可以安装如下:

wget https://dev.mysql.com/get/mysql-apt-config_0.8.10-1_all.deb
sudo dpkg -i mysql-apt-config_0.8.10-1_all.deb # Don't forget to select 5.6.x
sudo apt-get update
sudo apt-get install mysql-community-server -y
sudo apt-get install libmysqlclient18 -y

在尝试示例之前,请按照这些方法准备系统:

$ mysql -u root -p
SET PASSWORD = PASSWORD('sa');
DROP DATABASE test;
CREATE DATABASE test;

43.19.3. Microsoft Windows

这是一个 Windows API 调用的例子:

use NativeCall;

sub MessageBoxA(int32, Str, Str, int32)
    returns int32
    is native('user32')
    { * }

MessageBoxA(0, "We have NativeCall", "ohai", 64);

43.19.4. 关于调用 C 函数的简明指南

这是一个调用标准函数并在 Raku 程序中使用返回信息的示例。

getaddrinfo 是 POSIX 标准函数,用于获取有关网络节点的网络信息,例如 google.com。 这是一个有趣的功能,因为它说明了 NativeCall 的许多元素。

Linux 手册提供了有关 C 可调用函数的以下信息:

int getaddrinfo(const char *node, const char *service,
       const struct addrinfo *hints,
       struct addrinfo **res);

该函数返回响应码 0 = 错误,1 = 成功。 数据是从 addrinfo 元素的链表中提取的,第一个元素由 res 指向。

从 NativeCall 类型表我们知道 intint32。 我们也知道 char * 是 C Str 的形式 C 之一,它简单地映射到 Str。 但是 addrinfo 是一个结构体,这意味着我们需要编写自己的 Type 类。 但是,函数声明很简单:

sub getaddrinfo( Str $node, Str $service, Addrinfo $hints, Pointer $res is rw )
    returns int32
    is native
    { * }

请注意,$res 将由函数写入,因此必须将其标记为 rw。 由于库是标准 POSIX,因此库名称可以是 Type 定义或 null。

我们现在必须处理结构体 Addrinfo。 Linux 手册提供了以下信息:

struct addrinfo {
               int              ai_flags;
               int              ai_family;
               int              ai_socktype;
               int              ai_protocol;
               socklen_t        ai_addrlen;
               struct sockaddr *ai_addr;
               char            *ai_canonname;
               struct addrinfo *ai_next;
           };

intchar * 部分很简单。 一些研究表明 socklen_t 可以依赖于架构,但是是一个至少32位的无符号整数。 所以 socklen_t 可以映射到 uint32 类型。

复杂的是 sockaddr,它取决于 ai_socktype 是否是未定义的,INET 还是 INET6(标准的v4 IP 地址或 v6 地址)。

所以我们创建一个 Raku 类来映射到 C struct addrinfo; 当我们在它的时候,我们还为 SockAddr 创建了另一个类。

class SockAddr is repr('CStruct') {
    has int32    $.sa_family;
    has Str      $.sa_data;
}

class Addrinfo is repr('CStruct') {
    has int32     $.ai_flags;
    has int32     $.ai_family;
    has int32     $.ai_socktype;
    has int32     $.ai_protocol;
    has int32     $.ai_addrlen;
    has SockAddr  $.ai_addr       is rw;
    has Str       $.ai_cannonname is rw;
    has Addrinfo  $.ai_next       is rw;

}

最后三个属性的 is rw 反映了这些在 C 中被定义为指针。

映射到 C Struct 的重要一点是类的状态部分的结构,即属性。 但是,类可以有方法,而 NativeCall 不会“触摸”它们以映射到C.这意味着我们可以向类添加额外的方法以更易读的方式解包属性,例如,

method flags {
    do for AddrInfo-Flags.enums { .key if $!ai_flags +& .value }
}

通过定义适当的 enumflags 将返回一串键而不是一个打包的整数。

sockaddr 结构中最有用的信息是节点的地址,它取决于 Socket 的族。 因此,我们可以将方法地址添加到 Raku 类中,该类根据族来解释地址。

为了获得人类可读的 IP 地址,有一个 C 函数 inet_ntop,它给出一个带有 addrinfo 的缓冲区的 char *

将所有这些组合在一起会产生以下程序:

use v6;
use NativeCall;

constant \INET_ADDRSTRLEN = 16;
constant \INET6_ADDRSTRLEN = 46;

enum AddrInfo-Family (
    AF_UNSPEC                   => 0;
    AF_INET                     => 2;
    AF_INET6                    => 10;
);

enum AddrInfo-Socktype (
    SOCK_STREAM                 => 1;
    SOCK_DGRAM                  => 2;
    SOCK_RAW                    => 3;
    SOCK_RDM                    => 4;
    SOCK_SEQPACKET              => 5;
    SOCK_DCCP                   => 6;
    SOCK_PACKET                 => 10;
);

enum AddrInfo-Flags (
    AI_PASSIVE                  => 0x0001;
    AI_CANONNAME                => 0x0002;
    AI_NUMERICHOST              => 0x0004;
    AI_V4MAPPED                 => 0x0008;
    AI_ALL                      => 0x0010;
    AI_ADDRCONFIG               => 0x0020;
    AI_IDN                      => 0x0040;
    AI_CANONIDN                 => 0x0080;
    AI_IDN_ALLOW_UNASSIGNED     => 0x0100;
    AI_IDN_USE_STD3_ASCII_RULES => 0x0200;
    AI_NUMERICSERV              => 0x0400;
);

sub inet_ntop(int32, Pointer, Blob, int32 --> Str)
    is native {}

class SockAddr is repr('CStruct') {
    has uint16 $.sa_family;
}

class SockAddr-in is repr('CStruct') {
    has int16 $.sin_family;
    has uint16 $.sin_port;
    has uint32 $.sin_addr;

    method address {
        my $buf = buf8.allocate(INET_ADDRSTRLEN);
        inet_ntop(AF_INET, Pointer.new(nativecast(Pointer,self)+4),
            $buf, INET_ADDRSTRLEN)
    }
}

class SockAddr-in6 is repr('CStruct') {
    has uint16 $.sin6_family;
    has uint16 $.sin6_port;
    has uint32 $.sin6_flowinfo;
    has uint64 $.sin6_addr0;
    has uint64 $.sin6_addr1;
    has uint32 $.sin6_scope_id;

    method address {
        my $buf = buf8.allocate(INET6_ADDRSTRLEN);
        inet_ntop(AF_INET6, Pointer.new(nativecast(Pointer,self)+8),
            $buf, INET6_ADDRSTRLEN)
    }
}

class Addrinfo is repr('CStruct') {
    has int32 $.ai_flags;
    has int32 $.ai_family;
    has int32 $.ai_socktype;
    has int32 $.ai_protocol;
    has uint32 $.ai_addrNativeCalllen;
    has SockAddr $.ai_addr is rw;
    has Str $.ai_cannonname is rw;
    has Addrinfo $.ai_next is rw;

    method flags {
        do for AddrInfo-Flags.enums { .key if $!ai_flags +& .value }
    }

    method family {
        AddrInfo-Family($!ai_family)
    }

    method socktype {
        AddrInfo-Socktype($!ai_socktype)
    }

    method address {
        given $.family {
            when AF_INET {
                nativecast(SockAddr-in, $!ai_addr).address
            }
            when AF_INET6 {
                nativecast(SockAddr-in6, $!ai_addr).address
            }
        }
    }
}

sub getaddrinfo(Str $node, Str $service, Addrinfo $hints,
                Pointer $res is rw --> int32)
    is native {};

sub freeaddrinfo(Pointer)
    is native {}

sub MAIN() {
    my Addrinfo $hint .= new(:ai_flags(AI_CANONNAME));
    my Pointer $res .= new;
    my $rv = getaddrinfo("google.com", Str, $hint, $res);
    say "return val: $rv";
    if ( ! $rv ) {
        my $addr = nativecast(Addrinfo, $res);
        while $addr {
            with $addr {
                say "Name: ", $_ with .ai_cannonname;
                say .family, ' ', .socktype;
                say .address;
                $addr = .ai_next;
            }
        }
    }
    freeaddrinfo($res);
}

这产生如下输出:

return val: 0
Name: google.com
AF_INET SOCK_STREAM
216.58.219.206
AF_INET SOCK_DGRAM
216.58.219.206
AF_INET SOCK_RAW
216.58.219.206
AF_INET6 SOCK_STREAM
2607:f8b0:4006:800::200e
AF_INET6 SOCK_DGRAM
2607:f8b0:4006:800::200e
AF_INET6 SOCK_RAW
2607:f8b0:4006:800::200e

44. 正则表达式

正则表达式, 简称 regexes, 是描述文本模式的字符序列。模式匹配就是将这些模式和实际的文本进行匹配的过程。

44.1. 词法约定

Raku 正则表达式有特殊的写法:

m/abc/;         # a regex that is immediately matched against $_
rx/abc/;        # a Regex object
/abc/;          # a Regex object

对于前两个例子, 分隔符还能用除了斜线之外的其它字符:

m{abc};
rx{abc};

注意, 冒号和圆括号都不能用作分隔符; 禁止使用冒号作为正则表达式分割符是因为它和副词冲突, 例如 rx:i/abc/(忽略大小写的正则表达式), 而圆括号表明函数调用。

空白符在正则表达式中通常被忽略(带有 :s:sigspace 副词的正则表达式除外)。

通常, 对于 Raku 来说, 正则表达式中的注释以 # 号开头, 直至行尾。

44.2. 字面值

正则表达式最简单的情况是匹配字符串字面值。

if 'properly' ~~ m/ perl / {
    say "'properly' contains 'perl'";
}

字母数字和下划线 _ 按字面值匹配。所有其它字符要么使用反斜线转义(例如, \: 匹配一个冒号), 要么用引号引起来:

/ 'two words' /;     # matches 'two words' including the blank
/ "a:b"       /;     # matches 'a:b' including the colon
/ '#' /;             # matches a hash character

字符串是从左往右搜索的, 所以如果只有部分字符串匹配正则表达式也足够:

if 'abcdef' ~~ / de / {
    say ~$/;            # OUTPUT: «de␤»
    say $/.prematch;    # OUTPUT: «abc␤»
    say $/.postmatch;   # OUTPUT: «f␤»
    say $/.from;        # OUTPUT: «3␤»
    say $/.to;          # OUTPUT: «5␤»
};

匹配结果存储在 $/ 变量中并且也从匹配中返回。如果匹配成功, 那么结果就是 Match 类型, 否则它就是 Nil

44.3. 通配符和字符类

44.3.1. 点号匹配任意字符: .

在正则表达式中一个未转义的点 . 匹配任意单个字符。

所以, 这些都匹配:

'perl' ~~ /per./;       # matches the whole string
'perl' ~~ / per . /;    # the same; whitespace is ignored
'perl' ~~ / pe.l /;     # the . matches the r
'speller' ~~ / pe.l/;   # the . matches the first l

下面这个不匹配:

'perl' ~~ /. per /;

因为在目标字符串中 per 前面没有要匹配的字符。

44.3.2. 反斜杠, 预定义字符类

44.3.3. Unicode properties

Raku 有 \w 形式的预定义字符类。大写形式是它的反面, \W

  • \d 和 \D

\d 匹配单个数字(Unicode 属性 N) 而 \D 匹配单个不是数字的字符。

'ab42' ~~ /\d/ and say ~$/;     # OUTPUT: «4␤»
'ab42' ~~ /\D/ and say ~$/;     # OUTPUT: «a␤»

注意, 不仅仅只有阿拉伯数字(通常用于拉丁字母表中)匹配 \d, 还有来自其它下标的数字也匹配 \d。

U+0035 5 DIGIT FIVE
U+07C2 ߂ NKO DIGIT TWO
U+0E53 ๓ THAI DIGIT THREE
U+1B56 ᭖ BALINESE DIGIT SIX
  • \h 和 \H

\h 匹配单个水平空白符。 \H 匹配单个不是水平空白符的字符。

水平空白符的例子有:

U+0020 SPACE
U+00A0 NO-BREAK SPACE
U+0009 CHARACTER TABULATION
U+2001 EM QUAD

像换行符那样的垂直空白被显式地排除了; 那些可以用 \v 来匹配, 而 \s 匹配任何类型的空白:

  • \n 和 \N

\n 匹配单个逻辑换行符。\n 也支持匹配 Windows 的 CR LF 代码点对儿; 尽管还不清楚魔法是发生在读取数据时还是在正则表达式匹配时。 \N 匹配单个非逻辑换行符。

  • \s 和 \S

\s 匹配单个空白符。 \S 匹配单个非空白符。

if 'contains a word starting with "w"' ~~ / w \S+ / {
    say ~$/;        # OUTPUT: «word␤»
}
  • \t 和 \T

\t 匹配单个 tab/制表符, U+0009。(注意这儿不包含诸如 U+000B VERTICAL TABULATION 这样奇异的制表符)。\T 匹配单个非制表符。

  • \v 和 \V

\v 匹配单个垂直空白符。 \V 匹配单个非垂直空白符。

垂直空白符的例子:

U+000A LINE FEED
U+000B VERTICAL TABULATION
U+000C FORM FEED
U+000D CARRIAGE RETURN
U+0085 NEXT LINE
U+2028 LINE SEPARATOR
U+2029 PARAGRAPH SEPARATOR

使用 \s 去匹配任意空白, 而不仅仅匹配垂直空白。

  • \w 和 \W

\w 匹配单个单词字符; 例如: 一个字母(Unicode 类别 L), 一个数字或一个下划线。\W 匹配单个非单词字符。

单词字符的例子:

0041 A LATIN CAPITAL LETTER A
0031 1 DIGIT ONE
03B4 δ GREEK SMALL LETTER DELTA
03F3 ϳ GREEK LETTER YOT
0409 Љ CYRILLIC CAPITAL LETTER LJE

预定义的 subrules:

<alnum>   \w       'alpha' plus 'digit'
<alpha>   <:L>     Alphabetic characters
<blank>   \h       Horizontal whitespace
<cntrl>            Control characters
<digit>   \d       Decimal digits
<graph>            'alnum' plus 'punct'
<lower>   <:Ll>    Lowercase characters
<print>            'graph' plus 'space', but no 'cntrl'
<punct>            Punctuation and Symbols (only Punct beyond ASCII)
<space>   \s       Whitespace
<upper>   <:Lu>    Uppercase characters
<|wb>               Word Boundary (zero-width assertion)
<ww>               Within Word (zero-width assertion)
<xdigit>           Hexadecimal digit [0-9A-Fa-f]

44.3.4. Unicode 属性

目前提到的字符类大多是为了方便; 另一种方法是使用 Unicode 字符属性。这些以 <:property> 的形式出现, 其中 property 可以是短的或长的 Unicode 一般类别名。它们使用 pair 语法。

要匹配一个 Unicode 属性:

"a".uniprop('Script');                 # OUTPUT: «Latin␤»
"a" ~~ / <:Script<Latin>> /;
"a".uniprop('Block');                  # OUTPUT: «Basic Latin␤»
"a" ~~ / <:Block('Basic Latin')> /;

下面的 Unicode 通用类别表是从 Perl 5 的 perlunicode 文档偷来的:

Short	Long
L	Letter
LC	Cased_Letter
Lu	Uppercase_Letter
Ll	Lowercase_Letter
Lt	Titlecase_Letter
Lm	Modifier_Letter
Lo	Other_Letter
M	Mark
Mn	Nonspacing_Mark
Mc	Spacing_Mark
Me	Enclosing_Mark
N	Number
Nd	Decimal_Number (also Digit)
Nl	Letter_Number
No	Other_Number
P	Punctuation (also punct)
Pc	Connector_Punctuation
Pd	Dash_Punctuation
Ps	Open_Punctuation
Pe	Close_Punctuation
Pi	Initial_Punctuation
        (may behave like Ps or Pe depending on usage)
Pf	Final_Punctuation
        (may behave like Ps or Pe depending on usage)
Po	Other_Punctuation
S	Symbol
Sm	Math_Symbol
Sc	Currency_Symbol
Sk	Modifier_Symbol
So	Other_Symbol
Z	Separator
Zs	Space_Separator
Zl	Line_Separator
Zp	Paragraph_Separator
C	Other
Cc	Control (also cntrl)
Cf	Format
Cs	Surrogate
Co	Private_Use
Cn	Unassigned

举个例子: <:Lu> 匹配单个大写字母。

它的反面是这个: <:!property>。所以, <:!Lu> 匹配单个非大写字母的字符。

类别可以使用中缀操作符组合在一起:

Operator	Meaning
+	        set union
|	        set union
&	        set intersection
-	        set difference (first minus second)
^	        symmetric set intersection / XOR

要匹配要么一个小写字母,要么一个数字, 可以写 <:Ll+:N><:Ll+:Number><+ :Lowercase_Letter + :Number>

使用圆括号将类别和一组类别分组也是可以的; 例如:

'raku' ~~ m{\w+(<:Ll+:N>)}  # OUTPUT: «0 => 「6」␤»

44.3.5. 可枚举的字符类和区间

有时候, 预先存在的通配符和字符类不够用。幸运的是, 定义你自己的字符类相当简单。在 <[]> 中, 你可以放入任何数量的单个字符和字符区间(两个端点之间有两个点号), 带有或不带有空白。

"abacabadabacaba" ~~ / <[ a .. c 1 2 3 ]> /;
# Unicode hex codepoint range
"ÀÁÂÃÄÅÆ" ~~ / <[ \x[00C0] .. \x[00C6] ]> /;
# Unicode named codepoint range
"ÀÁÂÃÄÅÆ" ~~ / <[ \c[LATIN CAPITAL LETTER A WITH GRAVE] .. \c[LATIN CAPITAL LETTER AE] ]> /;

<> 中你可以使用 +- 来添加或移除多个区间定义, 甚至混合某些上面的 unicode 属性。你还可以在 [] 之间写上反斜线形式的字符类。

/ <[\d] - [13579]> /;
# starts with \d and removes odd ASCII digits, but not quite the same as
/ <[02468]> /;
# because the first one also contains "weird" unicodey digits

解析引号分割的字符串的一个常见模式涉及到对字符类取反:

say '"in quotes"' ~~ / '"' <-[ " ]> * '"'/;

这先匹配一个引号, 然后匹配任何不是引号的字符, 再然后还是一个引号。 上面例子中的 *+ 会在量词一节中解释。

就像你可以使用 - 用于集合差集和取反单个值, 你也可以在前面显式地放上一个 +:

/ <+[123]> /  # same as <[123]>

44.4. 量词

量词使前面的原子匹配可变次数。例如, a+ 匹配一个或多个字符 a

量词比连结绑定的更紧, 所以 ab+ 匹配一个 a, 然后跟着一个或多个 b。对于引号来说, 有点不同, 所以 'ab'+ 匹配字符串 ab, abab, ababab 等等。

44.4.1. 一次 或多次 :

+ 量词使它前面的原子匹配一次或多次, 没有次数上限。

例如, 要匹配 form=value 形式的字符串, 你可以这样写正则表达式:

/ \w+ '=' \w+ /

44.4.2. 零次 或 多次: *

* 量词使它前面的原子匹配一次或多次, 没有次数上限。

例如, 要允许 ab 之间出现可选的空白, 你可以这样写:

/ a \s* b /

44.4.3. 零次 或 一次匹配: ?

? 量词使它前面的原子匹配零次或一次。

44.4.4. 常规量词: ** min..max

要限定原子匹配任意次数, 你可以写出像 a * 2..5 那样的表达式来匹配字符 *a 至少 2 次, 至多 5 次。

say so 'a' ~~ /a ** 2..5/;        # OUTPUT: «False␤»
say so  'aaa' ~~ /a ** 2..5/;     # OUTPUT: «True␤»

如果最小匹配次数和最大匹配次数相同, 那么使用单个整数: a ** 5 精确地匹配 5 次。

say so 'aaaaa' ~~ /a ** 5/;       # OUTPUT: «True␤»

也可以使用 ^ 脱字符来排除区间的端点:

say so 'a'    ~~ /a ** 1^..^6/;   # OUTPUT: «False␤» -- there are 2 to 5 'a's in a row
say so 'aaaa' ~~ /a ** 1^..^6/;   # OUTPUT: «True␤»

下面这个包含从 0 开始的数值区间:

say so 'aaa' ~~ /a ** ^6/;        # OUTPUT: «True␤» -- there are 0 to 5 'a's in a row

或使用一个 Whatever Star * 操作符来表示无限区间:

say so 'aaaa' ~~ /a ** 1^..*/;    # OUTPUT: «True␤» -- there are 2 or more 'a's in a row

44.4.5. Modified quantifier: %

为了更容易地匹配逗号分割那样的值, 你可以在以上任何一个量词后面加上一个 % 修饰符以指定某个修饰符必须出现在每一次匹配之间。例如, a+ % ',' 会匹配 a, 或 a,aa,a,a 等等, 但是不会匹配 a,a,a, 等。要连这些也要匹配, 那么使用 %% 代替 %

44.4.6. 贪婪量词 Vs. 非贪婪量词: ?

默认地, 量词要求进行贪婪匹配:

'abababa' ~~ /a .* a/ && say ~$/;   # OUTPUT: «abababa␤»

你可以给量词附加一个 ? 修饰符来开启非贪婪匹配:

'abababa' ~~ /a .*? a/ && say ~$/;   # OUTPUT: «aba␤»

你还可以使用 ! 修饰符显式地要求贪婪匹配。

44.4.7. 阻止回溯: :

你可以在正则表达式中通过为量词附加一个 : 修饰符来阻止回溯:

say so 'abababa' ~~ /a .* aba/;    # OUTPUT: «True␤»
say so 'abababa' ~~ /a .*: aba/;   # OUTPUT: «False␤»

44.5. Alternation: ||

|| 在正则表达式中表示备选分支, 在匹配由 || 分割的几个可能的备选分支之一时, 第一个匹配的备选分支胜出。例如, ini 文件有如下形式:

[section]
key = value

因此, 如果你解析单行 ini 文件, 那么它要么是一个 section, 要么是一个键值对儿。所以正则表达式可以是:

/ '[' \w+ ']' || \S+ \s* '=' \s* \S* /

即, 它要么是一个由方括号包围起来的单词, 要么是一个键值对。

44.6. Longest Alternation: |

如果正则表达式由 | 分割, 则最长的那个匹配胜出。独立于正则表达式中的词法顺序。

say ('abc' ~~ / a | .b /).Str;    # OUTPUT: «ab␤»

44.7. Anchors

正则表达式引擎尝试在字符串中从左至右地搜索来查找匹配。

say so 'properly' ~~ / perl/;   # OUTPUT: «True␤»
#          ^^^^

有时候这不是你想要的。相反, 你可能只想匹配整个字符串, 或一整行, 或精确地一个或几个完整的单词。锚或断言能帮助我们。

为了整个正则表达式能够匹配, 断言需要被成功地匹配但是断言在匹配时不消耗字符。

44.7.1. ^ , Start of String and $ , End of String

^ 断言只匹配字符串的开头:

say so 'properly' ~~ /  perl/;    # OUTPUT: «True␤»
say so 'properly' ~~ /^ perl/;    # OUTPUT: «False␤»
say so 'perly'    ~~ /^ perl/;    # OUTPUT: «True␤»
say so 'perl'     ~~ /^ perl/;    # OUTPUT: «True␤»

$ 断言只匹配字符串的结尾:

say so 'use perl' ~~ /  perl  /;   # OUTPUT: «True␤»
say so 'use perl' ~~ /  perl $/;   # OUTPUT: «True␤»
say so 'perly'    ~~ /  perl $/;   # OUTPUT: «False␤»

你可以把这两个断言组合起来:

say so 'use perl' ~~ /^ perl $/;   # OUTPUT: «False␤»
say so 'perl'     ~~ /^ perl $/;   # OUTPUT: «True␤»

记住, ^ 匹配字符串的开头, 而非的开头。同样地, $ 匹配字符串的结尾, 而非的结尾。

下面的是多行字符串:

my $str = q:to/EOS/;
   Keep it secret
   and keep it safe
   EOS

say so $str ~~ /safe   $/;   # OUTPUT: «True␤»  -- 'safe' is at the end of the string
say so $str ~~ /secret $/;   # OUTPUT: «False␤» -- 'secret' is at the end of a line -- not the string
say so $str ~~ /^Keep   /;   # OUTPUT: «True␤»  -- 'Keep' is at the start of the string
say so $str ~~ /^and    /;   # OUTPUT: «False␤» -- 'and' is at the start of a line -- not the string

44.7.2. ^^ , Start of Line and $$ , End of Line

^^ 断言匹配逻辑行的开头。即, 要么在字符串的开头, 要么在换行符之后。然而, 它不匹配字符串的结尾, 即使它以一个换行符结尾。

$$ 只匹配逻辑换行符的结尾, 即, 在换行符之前, 或在字符串的结尾, 当最后一个字符不是换行符时。

(为了理解下面的示例, 最好先了解 q:to/EOS/…​EOS 的 "heredoc" 语法移除了前置的缩进, 使之与 EOS 标记同级, 以至于第一行, 第二行和最后一行没有前置空格而第三行和第四行各有两个前置空格。)

my $str = q:to/EOS/;
    There was a young man of Japan
    Whose limericks never would scan.
      When asked why this was,
      He replied "It's because
    I always try to fit as many syllables into the last line as ever I possibly can."
    EOS

say so $str ~~ /^^ There/;        # OUTPUT: «True␤»  -- start of string
say so $str ~~ /^^ limericks/;    # OUTPUT: «False␤» -- not at the start of a line
say so $str ~~ /^^ I/;            # OUTPUT: «True␤»  -- start of the last line
say so $str ~~ /^^ When/;         # OUTPUT: «False␤» -- there are blanks between
                                  #                       start of line and the "When"

say so $str ~~ / Japan $$/;       # OUTPUT: «True␤»  -- end of first line
say so $str ~~ / scan $$/;        # OUTPUT: «False␤» -- there's a . between "scan"
                                  #                      and the end of line
say so $str ~~ / '."' $$/;        # OUTPUT: «True␤»  -- at the last line

44.7.3. <|w> and <!|w>, word boundary

要匹配单词边界, 使用 <|w>。这与其它语言的 \b 类似,要匹配一个非单词边界, 使用 <!|w>, 类似其它语言的 \B。这些都是零宽断言。

44.7.4. << and >> , left and right word boundary

<< 匹配左单词边界。它匹配左侧(或者字符串的开头)是非单词字符而右侧是一个单词字符的位置。

>> 匹配右单词边界。它匹配左侧有一个单词字符而右侧(或者字符串的结尾)是一个非单词字符的位置。

my $str = 'The quick brown fox';
say so $str ~~ /br/;              # OUTPUT: «True␤»
say so $str ~~ /<< br/;           # OUTPUT: «True␤»
say so $str ~~ /br >>/;           # OUTPUT: «False␤»
say so $str ~~ /own/;             # OUTPUT: «True␤»
say so $str ~~ /<< own/;          # OUTPUT: «False␤»
say so $str ~~ /own >>/;          # OUTPUT: «True␤»

你可以使用变体 «» :

my $str = 'The quick brown fox';
say so $str ~~ /« own/;          # OUTPUT: «False␤»
say so $str ~~ /own »/;          # OUTPUT: «True␤»

44.8. 分组和捕获

在普通的(非正则表达式)Raku 代码中, 你可以使用圆括号把东西组织到一块, 通常用于覆盖操作符优先级:

say 1+4*2;   # 9, parsed as 1 + (4*2)
say (1+4)*2; # 输出: 10

在正则表达式中也可以使用同样的分组工具:

/ a || b c/;   # 匹配 'a' 或 'bc'
/ (a || b) c/; # 匹配 'ac' 或 'bc'

分组可以应用在量词上:

/ a b+ /;      # 匹配一个 'a', 后面再跟着一个或多个 'b'
/ (a b)+/;     # 匹配一个或多个 'ab' 序列
/ (a || b)+ /; # 匹配一个 'a' 序列或者 'b' 序列, 至少一次

一个非量词化的捕获产生一个 Match对象。当捕获被量化(除了使用 ? 量词)之后, 该捕获就变成 Match对象的列表。

44.8.1. 捕获

圆括号不仅仅能够分组, 它们也*捕获*; 也就是说, 它们使分组中匹配到的字符串用作变量,并且还作为生成的 Match 对象的元素:

my $str = 'number 42';
if $str ~~ /'number' (\d+) / {
    say "The number is $0";    # The number is 42
    # or
    say "The number is $/[0]"; # The number is 42
}

圆括号对儿是从左到右编号的, 编号从零开始。

if 'abc' ~~ /(a) b (c)/ {
    say "0:$0; 1:$1"; # 输出: 0:a; 1:c
}

$0$1 等语法是简写的。这些捕获可以从用作列表的匹配对象 $/ 中规范地获取到, 所以, $0 实际上是 $/[0] 的语法糖。

将匹配对象强制转换为列表可以方便地以编程方式访问所有元素:

if 'abc' ~~ /(a) b (c)/ {
    say $/.list.join: ','; # 输出 a,c
}

44.8.2. 非捕获分组

正则表达式中的圆括号扮演了双重角色: 它们将内部的正则表达式元素分组, 并通过内部的子正则表达式捕获所匹配到的内容。

要仅仅获得分组行为, 可以使用方括号 […​] 代替圆括号。

if 'abc' ~~ / [a||b] (c) / {
    say ~$0;                # OUTPUT: «c␤»
}

如果您不需要捕获, 则使用非捕获分组可提供三个好处: 它们更干净地传达正则表达式; 它们使您更容易对您关心的捕获组计数; 并且它匹配比较快。

44.8.3. 捕获编号

上面已经说明,捕获从左到右编号。 原则上是真的,这也是过于简单的。

为了完整起见,列出了以下规则。 当您发现自己经常使用它们时,考虑命名捕获(可能是 subrules)是值得的。

备选分支会重置捕获计数:

/ (x) (y)  || (a) (.) (.) /
# $0  $1      $0  $1  $2

例子:

if 'abc' ~~ /(x)(y) || (a)(.)(.)/ {
    say ~$1;            # b
}

如果两个(或多个)备选分支具有不同的捕获编号,则捕获编号最多的决定了下一个捕获的索引:

$_ = 'abcd';

if / a [ b (.) || (x) (y) ] (.) / {
    #      $0     $0  $1    $2
    say ~$2;           # d
}

捕获可以嵌套,在这种情况下,它们的每一级都会编号:

if 'abc' ~~ / ( a (.) (.) ) / {
    say "Outer: $0";                # Outer: abc
    say "Inner: $0[0] and $0[1]";   # Inner: b and c
}

44.8.4. 命名捕获

除了给捕获编号,你也可以给他们起名字。 命名捕获的通用和略微冗长的方式是这样的:

if 'abc' ~~ / $<myname> = [ \w+ ] / {
    say ~$<myname>      # OUTPUT: «abc␤»
}

对命名捕获 $<myname> 的访问是将匹配对象作为哈希索引的简写,换句话说:$/{'myname'}$/<myname>

命名捕获也可以使用常规捕获分组语法进行嵌套:

if 'abc-abc-abc' ~~ / $<string>=( [ $<part>=[abc] ]* % '-' ) / {
    say ~$<string>;         # OUTPUT: «abc-abc-abc␤»
    say ~$<string><part>;   # OUTPUT: «[abc, abc, abc]␤»
}

将匹配对象强制为散列可让您轻松地以编程方式访问所有命名捕获:

if 'count=23' ~~ / $<variable>=\w+ '=' $<value>=\w+ / {
    my %h = $/.hash;
    say %h.keys.sort.join: ', ';        # OUTPUT: «value, variable␤»
    say %h.values.sort.join: ', ';      # OUTPUT: «23, count␤»

    for %h.kv -> $k, $v {
        say "Found value '$v' with key '$k'";
        # outputs two lines:
        #   Found value 'count' with key 'variable'
        #   Found value '23' with key 'value'
    }
}

在 Subrules 部分会讨论获取命名捕获的更方便的方法。

44.8.5. Capture markers: <( )>

<( token 表示匹配的整体捕捉的开始,而相应的 )> token 表示其末端。 <( 类似于其他语言的 \K 丢弃 \K 之前找到的任何匹配项。

44.9. 替换

正则表达式也可以用来替换另一个文本。 您可以使用它来解决拼写错误(例如, 用 "Pearl Jam" 替换 "Perl Jam"), 从 yyyy-mm-ddThh:mm:ssZmm-dd-yy h:m {AM,PM} 重新格式化 ISO8601 日期及其它。

就像搜索替换编辑器的对话框一样,s/// 操作符有两面,左侧和右侧。 左侧是匹配表达式的位置,右侧是您要替换的表达式。

44.9.1. 词汇约定

替换和匹配的写法类似,但替换运算符既有正则表达式匹配的区域,也有替换的文本区域:

s/replace/with/;           # a substitution that is applied to $_
$str ~~ s/replace/with/;   # a substitution applied to a scalar

替换操作法允许除了斜线之外的分隔符:

s|replace|with|;
s!replace!with!;
s,replace,with,;

注意, 冒号和诸如 {}() 的分隔符不能作为替换分割符。带有副词的冒号斜线诸如 s:i/Foo/Bar 和其它分割符有其它用途。

就像 m// 操作符一样, 通常会忽略空白。在 Raku 中, 注释以 # 号开头直到当前行的结尾。

44.9.2. 替换字符串字面值

要替换的最简单的东西就是字符串字面量。你要替换的字符串在替换运算符的左侧, 而替换它的字符串在替换操作符的右侧; 例如:

$_ = 'The Replacements';
s/Replace/Entrap/;
.say;                    # OUTPUT: «The Entrapments␤»

字母数字字符和下划线是文字匹配,就像其表哥 m// 操作符一样。 所有其他字符都必须使用反斜杠`\`转义,或包含在引号中:

$_ = 'Space: 1999';
s/Space\:/Party like it's/;
.say                        # OUTPUT: «Party like it's 1999␤»

请注意,匹配约束仅适用于替换表达式的左侧。

默认情况下,替换仅在第一匹配中完成:

$_ = 'There can be twly two';
s/tw/on/;                     # replace 'tw' with 'on' once
.say;                         # OUTPUT: «there can be only two␤»

44.9.3. 通配符和字符类

任何可以进入 m// 操作符的内容都可以进入替换操作符的左侧,包括通配符和字符类。 当您匹配的文本不是静态的时,这很方便,例如尝试匹配字符串中间的数字:

$_ = "Blake's 9";
s/\d+/7/;         # replace any sequence of digits with '7'
.say;             # OUTPUT: «Blake's 7␤»

当然,你可以使用任何`+*?` 修饰符,它们的行为就像在 m// 操作符的上下文中一样。

44.9.4. 捕获组

就像在匹配运算符中一样,捕获组在左侧被允许,匹配的内容填充 $0..$n 变量和 $/ 对象:

$_ = '2016-01-23 18:09:00';
s/ (\d+)\-(\d+)\-(\d+) /today/;   # replace YYYY-MM-DD with 'today'
.say;                             # OUTPUT: «today 18:09:00␤»
"$1-$2-$0".say;                   # OUTPUT: «01-23-2016␤»
"$/[1]-$/[2]-$/[0]".say;          # OUTPUT: «01-23-2016␤»

任何这些变量 $0$1$/ 也可以在运算符的右侧使用,所以你可以操纵你刚刚匹配的内容。 这样,您可以将日期的YYYY,MM和DD部分分开,并将其重新格式化为 MM-DD-YYYY 顺序:

$_ = '2016-01-23 18:09:00';
s/ (\d+)\-(\d+)\-(\d+) /$1-$2-$0/;    # transform YYYY-MM-DD to MM-DD-YYYY
.say;                                 # OUTPUT: «01-23-2016 18:09:00␤»

由于右侧实际上是一个常规的 Raku 内插字符串,因此可以将时间从 HH:MM 重新格式化为 `h:MM {AM,PM} 格式, 如下所示:

$_ = '18:38';
s/(\d+)\:(\d+)/{$0 % 12}\:$1 {$0 < 12 ?? 'AM' !! 'PM'}/;
.say;                                                    # OUTPUT: «6:38 PM␤»

使用上面的模数 % 运算符将样本代码保留在80个字符以下,否则就是 $0 <12 ?? $0 !! $0 - 12。 结合解析器表达式语法的强大功能,真正使您在这里看到的内容成为可能,您可以使用“正则表达式”来解析任何文本。

44.9.5. Common adverbs

44.10. Tilde for nesting structures

~ 运算符是一个帮助器,用于匹配具有特定终结符的嵌套子规则作为目标。 它被设计为放置在开口和闭合括号之间,如下所示:

/ '(' ~ ')' <expression> /

然而, 它主要忽略左侧的参数, 并且在接下来的两个原子(可以被量化)上操作。 它对下两个原子的操作是“旋转”它们,使得它们实际上以相反的顺序匹配。 因此,上面的表达式,起初是腮红,只不过是下面的简写:

/ '(' <expression> ')' /

但是除此之外,当它重写原子时,它还会插入将设置内部表达式以识别终止符的设备,并且如果内部表达式不在所需的闭合原子上终止,则产生适当的错误消息。 所以它确实也注意了左边的括号,它实际上把我们的例子改写成更像:

$<OPEN> = '(' <SETGOAL: ')'> <expression> [ $GOAL || <FAILGOAL> ]

FAILGOAL 是一种可以由用户定义的特殊方法,它将在解析失败时被调用:

grammar A { token TOP { '[' ~ ']' \w+  };
            method FAILGOAL($goal) {
                die "Cannot find $goal near position {self.pos}"
            }
}

A.parse: '[good]';  # OUTPUT: «「[good]」␤»
A.parse: '[bad';    # will throw FAILGOAL exception
CATCH { default { put .^name, ': ', .Str } };
# OUTPUT: «X::AdHoc: Cannot find ']'  near position 5␤»

请注意,即使没有开头括号,也可以使用此构造来设置闭合结构的期望值:

"3)"  ~~ / <?> ~ ')' \d+ /;  # RESULT: «「3)」»
"(3)" ~~ / <?> ~ ')' \d+ /;  # RESULT: «「3)」»

这里 <?> 在第一个空字符串中返回true。

正则表达式捕获的顺序是原始的:

"abc" ~~ /a ~ (c) (b)/;
say $0; # OUTPUT: «「c」␤»
say $1; # OUTPUT: «「b」␤»

44.11. Subrules

就像你可以把代码片段放进子例程中一样, 你同样可以把正则表达式片段放进命名规则中(named rules)。

my regex line { \N*\n }
if "abc\ndef" ~~ /<line> def/ {
    say "First line:", $<line>.chomp; # OUTPUT:«First line: abc␤»
}

命名正则可以使用 my regex_name { body here } 来声明, 并使用 <regex_name> 来调用。与此同时, 调用命名正则的时候会安装一个同名的命名捕获。

要给捕获起一个和 regex 不同的名字, 那么使用 <capture_name=regex_name> 语法。如果不想捕获, 那么使用一个前置的点号来抑制捕获: <.regex_name>

下面是一个更完善的解析 ini 文件的例子:

my regex header { \s* '[' (\w+) ']' \h* \n+ }
my regex identifier  { \w+ }
my regex kvpair { \s* <key=identifier> '=' <value=identifier> \n+ }
my regex section {
    <header>
    <kvpair>*
}

my $contents = q:to/EOI/;
    [passwords]
        jack=password1
        joy=muchmoresecure123
    [quotas]
        jack=123
        joy=42
EOI

my %config;
if $contents ~~ /<section>*/ {
    for $<section>.list -> $section {
        my %section;
        for $section<kvpair>.list -> $p {
            say $p<value>;
            %section{ $p<key> } = ~$p<value>;
        }
        %config{ $section<header>[0] } = %section;
    }
}
say %config.perl;
# OUTPUT: «("passwords" => {"jack" => "password1", "joy" => "muchmoresecure123"},␤
#          "quotas" => {"jack" => "123", "joy" => "42"}).hash»

命名正则可以规整到 gramamrs 中。S05中有一组预定义的 subrules。

44.12. 副词

副词修改正则表达式的工作方式, 并为某些类型的循环任务提供方便的快捷方式。

有两种副词: 正则表达式副词适用于定义正则表达式时, 匹配副词适用于正则表达式与字符串匹配时。

这种区别往往是模糊的, 因为匹配和声明通常是文本上关闭的, 但使用方法形式的匹配使得区分清晰一点。

'abc' ~~ /../ 大致相当于 'abc'.match(/../), 甚至可以更清楚地单独写成一行:

my $regex = /../;           # definition
if 'abc'.match($regex) {    # matching
    say "'abc' has at least two characters";
}

正则表达式副词像 :i 会进入定义行而匹配副词像 :overlap 会附加到匹配调用上:

my $regex = /:i . a/;
for 'baA'.match($regex, :overlap) -> $m {
    say ~$m;
}
# OUTPUT: «ba␤aA␤»

44.12.1. Regex Adverbs

在正则表达式声明时出现的副词是实际正则表达式的一部分, 并影响 Raku 编译器如何将正则表达式转换为二进制代码。

例如: :ignorecase (:i) 副词告诉编译器忽略大写, 小写和标题大小写字母之间的区别。

所以 'a'~~ /A/ 是假的, 但 ’a' ~~ /:i A /是一个成功的匹配。

正则表达式副词可以在正则表达式声明之前或之内, 并且仅在词法上影响其后的正则表达式部分。 请注意, 在正则表达式之前出现的正则表达式副词必须出现在将正则表达式引入解析器之后, 如 'rx' 或 'm' 或裸的 '/'。 但是这样是无效的:

my $rx1 = :i/a/;      # adverb is before the regex is recognized => exception

下面这些是等价的:

my $rx1 = rx:i/a/;      # before
my $rx2 = rx/:i a/;     # inside

而下面这两种是不等价的:

my $rx3 = rx/a :i b/;   # matches only the b case insensitively
my $rx4 = rx/:i a b/;   # matches completely case insensitively

方括号和圆括号约束副词的作用域:

/ (:i a b) c /;         # matches 'ABc' but not 'ABC'
/ [:i a b] c /;         # matches 'ABc' but not 'ABC'

44.12.2. Ratchet

:ratchet:r 副词会导致正则表达式引擎不回溯。

假如没有这个副词, 那么正则表达式的一部分将尝试不同的路径来匹配字符串, 以使正则表达式的其他部分可以匹配。 例如, 在 'abc' ~~ / \w+ ./ 中, \w+ 首先吃光整个字符串 abc, 然后 . 就失败了。 因此 \w+ 放弃一个字符, 只匹配 ab 而 . 可以成功匹配字符串 c。 放弃字符的过程(或在轮试的情况下, 尝试不同的分支)被称为回溯。

say so 'abc' ~~ / \w+ . /;        # OUTPUT: «True␤»
say so 'abc' ~~ / :r \w+ . /;     # OUTPUT: «False␤»

Ratcheting 是一种优化, 因为回溯是昂贵的。 但更重要的是, 它与人类解析文本的方式密切相关。 如果你有一个正则表达式 my regex identifier { \w+ } ` 和 `my regex keyword { if | else | endif }, 你直观地期望 identifier 吞噬整个单词,而不是放弃结束下一个规则,如果下一个 rule 失败时。

例如,你不想让单词 motif 被解析为标识符 mot 后面跟着关键字 if。 相反, 你想将 motif 解析为标识符; 并且如果解析器期望之后有一个 if, 那么最好让它失败, 而不是以你不期望的方式解析输入。

由于 ratcheting 行为在解析器中通常是需要的, 所以有一个快捷方式来声明一个 ratcheting 正则表达式:

my token thing { .... }
# short for
my regex thing { :r ... }

44.12.3. Sigspace

:sigspace:s 副词使空白在正则表达式中有意义。

say so "I used Photoshop®"   ~~ m:i/   photo shop /;      # OUTPUT: «True␤»
say so "I used a photo shop" ~~ m:i:s/ photo shop /;   # OUTPUT: «True␤»
say so "I used Photoshop®"   ~~ m:i:s/ photo shop /;   # OUTPUT: «False␤»

m:s/ photo shop / 的作用和 m/ photo <.ws> shop <.ws> / 一样。默认地, <.ws> 确保单词是分开的, 所以 a b^$ 会匹配中间的 <.ws>, 但是 ab 不会。

正则表达式中哪里的空白会被转换为 <.ws> 取决于空白前面是什么。在上面的例子中, 正则表达式开头的空白不会被转换为 <.ws>, 但是字符后面的空白会被转换为 <.ws>。通常, 规则就是, 如果某一项可能匹配某个东西, 那么它后面的空白会被转换为 <.ws>

此外, 如果空白跟在某个 term 之后, 量词(,* 或 ?)之前, 那么 `<.ws>` 会在每次 term 匹配后匹配。 所以, `foo +` 变为 `[foo <.ws>]。另一方面, 量词后面的空白和普通的空白作用一样; 例如: "foo+" 变为 `foo+<.ws>

44.12.4. Matching adverbs

和正则表达式副词对比, 其与正则表达式声明有关, 匹配副词只有在将字符串与正则表达式匹配时才有意义。

它们永远不会出现在正则表达式内部, 只能在外部 - 作为 m/…​/ 匹配的一部分或作为匹配方法的参数。

44.12.5. Continue

:continue 或短的 :c 副词接收一个参数。 这个参数是正则表达式开始搜索的位置。 默认情况下, 它从字符串的开头搜索, 但是 :c 覆盖该位置。 如果没有为 :c 指定位置, 它将默认为 0, 除非设置了 $/, 在这种情况下, 它默认为 $/.to

given 'a1xa2' {
    say ~m/a./;         # OUTPUT: «a1␤»
    say ~m:c(2)/a./;    # OUTPUT: «a2␤»
}

注意: 不同于 :pos, 使用 :continue() 的匹配将尝试在字符串中进一步匹配, 而不是马上失败:

say "abcdefg" ~~ m:c(3)/e.+/; # OUTPUT: «「efg」␤»
say "abcdefg" ~~ m:p(3)/e.+/; # OUTPUT: «False␤»

44.12.6. Exhaustive

要找到正则表达式的所有可能的匹配 - 包括重叠的 - 和几个从同一位置开始的匹配, 请使用 :exhaustive(short: ex) 副词。

given 'abracadabra' {
    for m:exhaustive/ a .* a / -> $match {
        say ' ' x $match.from, ~$match;
    }
}

上面的代码产生这样的输出:

abracadabra
abracada
abraca
abra
   acadabra
   acada
   aca
     adabra
     ada
       abra

44.12.7. Global

不是搜索一个匹配并返回一个 Match 对象, Global 搜索每个不重叠的匹配, 并将其返回到列表中。 为此, 请使用 :global 副词:

given 'several words here' {
    my @matches = m:global/\w+/;
    say @matches.elems;         # OUTPUT: «3␤»
    say ~@matches[2];           # OUTPUT: «here␤»
}

:g:global 的简写。

44.12.8. Pos

在字符串的特定位置锚定匹配:

given 'abcdef' {
    my $match = m:pos(2)/.*/;
    say $match.from;        # OUTPUT: «2␤»
    say ~$match;            # OUTPUT: «cdef␤»
}

:p:pos 的简写。

注意: 不同于 :continue, 使用 :pos() 锚定的匹配在不匹配时将立即失败, 而不是尝试进一步匹配字符串:

say "abcdefg" ~~ m:c(3)/e.+/; # OUTPUT: «「efg」␤»
say "abcdefg" ~~ m:p(3)/e.+/; # OUTPUT: «False␤»

44.12.9. Overlap

要获得多个匹配, 包括重叠的匹配, 但每个起始位置只有一个(最长的)匹配, 请指定 :overlap (short :ov) 副词:

given 'abracadabra' {
    for m:overlap/ a .* a / -> $match {
        say ' ' x $match.from, ~$match;
    }
}

产生:

abracadabra
   acadabra
     adabra
       abra

44.13. Look-around assertions

44.13.1. Lookahead assertions

要检查一个模式是否出现在另一个模式之前,请通过 before 断言使用 lookahead 断言。形式如下:

<?before pattern>

因此,要搜索字符串 foo 后面紧跟着字符串 bar, 请使用以下 regexp:

rx{ foo <?before bar> }

例如:

say "foobar" ~~ rx{ foo <?before bar> };   # OUTPUT: «foo␤»

但是,如果要搜索一个不紧随某个模式的模式, 那么您需要使用反向向前查看断言, 其形式如下:

<!before pattern>

因此,所有出现的不在 bar 之前的 foo 都会匹配:

rx{ foo <!before bar> }

44.13.2. Lookbehind assertions

要检查一个模式是否出现在另一个模式之后,请通过 after 断言使用 lookbehind 断言。 其形式如下:

<?after pattern>

因此, 要搜索字符串 foo 立即跟着的 bar 字符串, 使用如下正则表达式:

rx{ <?after foo> bar } # read as after foo is bar

例如:

say "foobar" ~~ rx{ <?after foo> bar }; #  OUTPUT: «bar␤»

但是, 如果要搜索的模式不是紧随其后的模式, 那么您需要使用反向的 lookbehind 断言, 其形式如下:

<!after pattern>

因此, bar 前面不是 foo 的所有 bar 将被匹配:

rx{ <!after foo> bar }

44.14. Best practices and gotchas

为了帮助强大的正则表达式和 Grammar, 以下是代码布局和可读性的最佳实践,实际匹配的内容,并避免常见的陷阱。

44.14.1. Code layout

没有 :sigspace 副词, 空白在 Raku 正则表达式中就是没有意义的。 在能增加可读性的地方插入空格。 此外, 必要时插入注释。

比较下面这个比较紧凑的正则表达式:

my regex float { <[+-]>?\d*'.'\d+[e<[+-]>?\d+]? }

和可读性更好的版本:

my regex float {
     <[+-]>?        # optional sign
     \d*            # leading digits, optional
     '.'
     \d+
     [              # optional exponent
        e <[+-]>?  \d+
     ]?
}

根据经验,在原子周围和分组内部使用空格; 将量词直接置于原子之后; 并垂直对齐开口和闭合方括号和圆括号。

当你在方括号或圆括号中使用一组备选分支时, 请对齐垂直条:

my regex example {
    <preamble>
    [
    || <choice_1>
    || <choice_2>
    || <choice_3>
    ]+
    <postamble>
}

44.14.2. Keep it small

正则表达式通常比常规代码更紧凑。 因为他们短小精悍, 保持正则表达式很短。

当你可以命名正则表达式的一部分时, 通常最好将其放入单独的命名正则表达式中。

例如, 您可以从前面获取 float 正则表达式:

my regex float {
     <[+-]>?        # optional sign
     \d*            # leading digits, optional
     '.'
     \d+
     [              # optional exponent
        e <[+-]>?  \d+
     ]?
}

并把它分解成几部分:

my token sign { <[+-]> }
my token decimal { \d+ }
my token exponent { 'e' <sign>? <decimal> }
my regex float {
    <sign>?
    <decimal>?
    '.'
    <decimal>
    <exponent>?
}

这很有用, 特别是当正则表达式变得更加复杂时。 例如, 你可能希望在存在指数的情况下使小数点可选。

my regex float {
    <sign>?
    [
    || <decimal>?  '.' <decimal> <exponent>?
    || <decimal> <exponent>
    ]
}

44.14.3. What to match

通常,输入数据格式没有明确的规范,或规范对编程人员来说是未知的。 那么,在你期望的时候是自由的,只要没有可能的含糊不清就行了。

例如,在 ini 文件中:

[section]
key=value

什么可以在 section 标题内? 只允许一个单词可能太限制了。 有人会写 [two words], 或用破折号等等。 而不是询问内部允许的内容, 可能这样问比较好: 什么是不允许的?

显然, 不允许使用括号,因为 [a] b] 是不明确的。 同样的论据, 应禁止开口方括号。 这让我们有了

token header { '[' <-[ \[\] ]>+ ']' }

如果你只处理一行就行了。 但是,如果你正在处理整个文件,突然间正则表达式解析到一句

[with a
newline in between]

这可能不是一个好方法。折中的方式是:

token header { '[' <-[ \[\] \n ]>+ ']' }

然后在扫尾处理中, 从 section 标题中移除前导和尾部空格和制表符。

44.14.4. Matching Whitespace

:sigspace 副词(或使用 rule 声明符, 而不是 tokenregex) 非常适用于隐式解析许多地方可能出现的空格。

回到解析 ini 文件的例子, 我们有

my regex kvpair { \s* <key=identifier> '=' <value=identifier> \n+ }

这可能不像我们想要的那样自由, 因为用户可能会在等号周围放置空格。 那么我们可以试试这个:

my regex kvpair { \s* <key=identifier> \s* '=' \s* <value=identifier> \n+ }

但这看起来很笨重, 所以我们尝试其他方式:

my rule kvpair { <key=identifier> '=' <value=identifier> \n+ }

可是等等! value 之后,隐含的空白匹配用光了所有的空白, 包括换行符, 所以 \n+ 没有什么可以匹配的(rule 也禁止回溯, 所以运气不佳)。

因此, 重要的是将隐式空白的定义重新定义为输入格式无意义的空白。

这通过重新定义 token ws; 但是,它只适用于 Grammars:

grammar IniFormat {
    token ws { <!ww> \h* }
    rule header { \s* '[' (\w+) ']' \n+ }
    token identifier  { \w+ }
    rule kvpair { \s* <key=identifier> '=' <value=identifier> \n+ }
    token section {
        <header>
        <kvpair>*
    }

    token TOP {
        <section>*
    }
}

my $contents = q:to/EOI/;
    [passwords]
        jack = password1
        joy = muchmoresecure123
    [quotas]
        jack = 123
        joy = 42
EOI
say so IniFormat.parse($contents);

除了把所有的正则表达式都放在一个 Grammar 中并把它们变成了 tokens(因为他们不需要回溯) 之外, 有趣的新花样是:

token ws { <!ww> \h* }

这被称为隐式空白解析。 当它不在两个字符之间 (<ww>, 反向的"within word" 断言)时匹配, 以及零个或多个水平空格字符。 对水平空白的限制很重要, 因为换行符(它们是垂直空白)定界记录, 不应该被隐式地匹配。

还有一些与空白有关的麻烦潜伏着。 正则表达式 \n+ 将不会匹配 \n \n 这样的字符串, 因为两个换行符之间有空白。 要允许这样的输入字符串, 用 \n\s* 代替 \n+

45. 变量

变量名以一个叫做魔符 sigil 的特殊字符开头, 后面跟着一个可选的第二个叫做 twigil 的特殊字符, 然后是一个标识符.

45.1. Sigils

符号

类型约束

默认类型

Flattens

Assignment

$

Mu (no type constraint)

Any

No

item

&

Callable

Callable

No

item

@

Positional

Array

Yes

list

%

Associative

Hash

Yes

list

例子:

my $square = 9 ** 2;
my @array  = 1, 2, 3;   # Array variable with three elements
my %hash   = London => 'UK', Berlin => 'Germany';

默认类型可以使用 is 关键字设置。

class FailHash is Hash {
    has Bool $!final = False;
    multi method AT-KEY ( ::?CLASS:D: Str:D \key ){
        fail X::OutOfRange.new(:what("Hash key"), :got(key), :range(self.keys)) if $!final && !self.EXISTS-KEY(key);
        callsame
    }

    method finalize() {
        $!final = True
    }
}

my %h is FailHash = oranges => "round", bananas => "bendy";
say %h<oranges>;
# OUTPUT «round␤»
%h.finalize;
say %h<cherry>;
CATCH { default { put .^name, ': ', .Str } }
# OUTPUT «X::OutOfRange: Hash key out of range. Is: cherry, should be in (oranges bananas)»

不带符号的变量也是可行的, 查看 无符号变量.

45.2. 项和列表赋值

有两种类型的赋值, item 赋值和 list 赋值. 两者都使用 = 号操作符. 根据 = 号左边的语法来区别 = 是 item 赋值还是 list 赋值.

Item 赋值把等号右侧的值放到左侧的变量(容器)中.

例如, 数组变量(@符号)在列表赋值时清空数组自身, 然后把等号右侧的值都放进数组自身中. 跟 Item 赋值相比, 这意味着等号左侧的变量类型始终是 Array, 不管右侧是什么类型.

赋值类型(item 或 list)取决于当前表达式或声明符看到的第一个上下文:

my $foo = 5;            # item assignment
say $foo.perl;          # 5

my @bar = 7, 9;         # list assignment
say @bar.WHAT;          # Array
say @bar.perl;          # [7, 9]

(my $baz) = 11, 13;     # list assignment
say $baz.WHAT;          # Parcel
say $baz.perl;          # (11, 13)

因此, 包含在列表赋值中的赋值行为依赖于表达式或包含表达式的声明符。 例如, 如果内部赋值是一个声明符(例如 my), 就使用 item 赋值, 它比逗号和列表赋值的优先级更高:

my @array;
@array = my $num = 42, "str";   # item assignment: uses declarator
say @array.perl;                # [42, "str"] (an Array)
say $num.perl;                  # 42 (a Num)

类似地, 如果内部赋值是一个用于声明符初始化的表达式, 则内部表达式的上下文决定赋值的类型:

my $num;
my @array = $num = 42, "str";    # item assignment: uses expression
say @array.perl;                 # [42, "str"] (an Array)
say $num.perl;                   # 42 (a Num)

my ( @foo, $bar );
@foo = ($bar) = 42, "str";       # list assignment: uses parens
say @foo.perl;                   # [42, "str"] (an Array)
say $bar.perl;                   # $(42, "str")  (a Parcel)

然而, 如果内部赋值既不是声明符又不是表达式, 而是更大的表达式的一部分, 更大的表达式的上下文决定赋值的类型:

my ( @array, $num );
@array = $num = 42, "str";    # list assignment
say @array.perl;              # [42, "str"] (an Array)
say $num.perl;                # [42, "str"] (an Array)

这是因为整个表达式是 @array = $num = 42, "str", 而 $num = 42 不是单独的表达式.

查看操作符获取关于优先级的更多详情。

45.3. 无符号变量

在 Raku 中创建不带符号的变量也是可能的:

my \degrees = pi / 180;
my \θ       = 15 * degrees;

然而, 这些无符号变量并不创建容器. 那意味着上面的 degreesθ 实际上直接代表 Nums. 为了说明, 我们定义一个无符号变量后再赋值:

θ = 3; # Dies with the error "Cannot modify an immutable Num"

无符号变量不强制上下文, 所以它们可被用于原样地传递某些东西:

sub logged(&f, |args) {
    say('Calling ' ~ &f.name ~ ' with arguments ' ~ args.perl);
    my \result = f(|args);
    #  ^^^^^^^ not enforcing any context here
    say(&f.name ~ ' returned ' ~ result.perl);
    return |result;
}

45.4. Twigils

Twigils 影响变量的`作用域`。请记住 twigils 对基本的魔符插值没有影响,那就是,如果 $a 内插, $^a, $*a, $=a, $?a, $.a, 等等也会内插. 它仅仅取决于 $.

Twigil

Scope

*

动态的

!

属性(类成员)

?

编译时变量

.

方法(并非真正的变量)

<

匹配对象索引(并非真正的变量)

^

自我声明的形式位置参数

:

自我声明的形式命名参数

=

Pod 变量

~

子语言

45.5. * Twigil

动态变量通过 caller 查找, 不是通过外部作用域。例如:

    my $lexical   = 1;
    my $*dynamic1 = 10;
    my $*dynamic2 = 100;

    sub say-all() {
        say "$lexical, $*dynamic1, $*dynamic2";
    }

    # prints 1, 10, 100
    say-all();

    {
        my $lexical   = 2;
        my $*dynamic1 = 11;
        $*dynamic2    = 101; # 注意,这儿没有使用 my 来声明

        # prints 1, 11, 101
        say-all();
    }

    # prints 1, 10, 101
    say-all();

第一次调用 &say-all 时, 就像你期望的一样, 它打印 "1, 10, 100"。可是第二次它打印 "1, 11, 101"。 这是因为 $lexical 不是在调用者的作用域内被查找, 而是在 &say-all 被定义的作用域里被查找的。这两个动态作用域变量在调用者的作用域内被查找, 所以值为 11101。第三次调用 &say-all 后, $*dynamic1 不再是 11 了. 但是 $*dynamic2 仍然是 101。这源于我们在块中声明了一个新的动态变量 $*dynamic1 的事实并且没有像我们对待 $*dynamic2 那样把值赋值给旧的变量。

动态变量与其他变量类型在引用一个未声明的动态变量上不同的是前者不是一个编译时错误,而是运行时 failure,这样一个动态变量可以在未定义时使用只要在把它用作任何其它东西的时候检查它是否定义过:

sub foo() {
    $*FOO // 'foo';
}

say foo; # -> 'foo'

my $*FOO = 'bar';

say foo; # -> 'bar'

45.6. ! Twigil

属性是变量, 存在于每个类的实例中. 通过 ! 符号它们可以从类的里面直接被访问到:

    class Point {
        has $.x;
        has $.y;

        method Str() {
            "($!x, $!y)"
        }
    }

注意属性是怎样被声明为 $.x$.y 的, 但是仍然能够通过 $!x$!y 访问到属性. 这是因为 在 Raku 中, 所有的属性都是`私有的`, 并且在类中能使用 $!attribute-name 直接访问这些属性. Raku 能自动为你生成访问方法. 关于对象、类和它们的属性和方法的详情, 请查看面向对象.

45.7. ? Twigil

编译时"常量", 可通过 ? twigil 访问. 编译器对它们很熟悉, 并且编译后不能被修改. 常用的一个例子如下:

say "$?FILE: $?LINE"; # prints "hello.pl: 23" if this is the 23 line of a
                      # file named "hello.pl".

关于这些特殊变量的列表请查看编译时变量

尽管不能在运行时改变它们, 用户可以(重新)定义这种常量.

constant $?TABSTOP = 4; # this causes leading tabs in a heredoc or in a POD
                        # block's virtual margin to be counted as 4 spaces.

45.8. . Twigil

. twigil 真的不是用于变量的. 实际上, 看下面的代码:

    class Point {
        has $.x;
        has $.y;

        method Str() {
            "($.x, $.y)" # 注意我们这次使用 . 而不是 !
        }
    }

self(自身)调用了方法 x 和方法 y, 这是自动为你生成的, 因为在你声明你的属性的时候, 你使用的是 . twigil 。 注意, 子类可能会覆盖那些方法. 如果你不想这个发生, 请使用 $!x$!y 代替。

. twigil 只是调用了一个方法也表明下面是可能的

    class SaySomething {
        method a() { say "a"; }
        method b() { $.a; }
    }

    SaySomething.b; # prints "a"

关于对象、类和它们的属性和方法的详情, 请查看面向对象.

45.9. < Twigil

`<` twigil 是 `$/<...>` 的别名, 其中,  `$/` 是匹配变量. 关于匹配变量的更多详情请查看 link:https://docs.raku.org/language/variables#The_%24%2F_Variable[$/变量]和link:https://docs.raku.org/type/Match[类型匹配]

45.10. ^ Twigil

^ twigil 为 block 块 或 子例程 声明了一个形式位置参数. 形如 $^variable 的变量是一种占位变量. 它们可用在裸代码块中来声明代码块的形式参数. 看下面代码中的块:

for ^4 {
    say "$^seconds follows $^first";
}

它打印出:

1 follows 0
3 follows 2

有两个形式参数,就是 $first$second. 注意, 尽管 $^second 在代码中出现的比 $^first 早, $^first 依然是代码块中的第一个形式参数. 这是因为占位符变量是以 Unicode 顺序排序的.

子例程也能使用占位符变量, 但是只有在子例程没有显式的参数列表时才行. 这对普通的块也适用

sub say-it    { say $^a; } # valid
sub say-it()  { say $^a; } # invalid
              { say $^a; } # valid
-> $x, $y, $x { say $^a; } # 非法, 已经有参数列表 $x,$y,$x 了

占位符变量语法上不能有类型限制. 也注意, 也不能使用单个大写字母的占位符变量, 如 $^A

45.11. : Twigil

: twigil 为块或子例程声明了一个形式命名参数。使用这种形式声明的变量也是占位符变量的一种类型。因此适用于使用 ^ twigil 声明的变量的东西在这儿也适用(除了它们不是位置的以外, 因此没有使用 Unicode 顺序排序)。所以这个:

say { $:add ?? $^a + $^b !! $^a - $^b }( 4, 5 ) :!add
# OUTPUT:
# -1

查看 https://docs.raku.org/routine/%5E获取关于占位符变量的更多细节。

45.12. = Twigil

= twigil 用于访问 Pod 变量。当前文件中的每个 Pod 块都能通过一个 Pod 对象访问到, 例如 $=data, $=SYNOPSIS=UserBlock, 即:一个和想要的块同名的变量加上一个 = twigil。

=begin Foo
...
=end Foo

#after that, $=Foo gives you all Foo-Pod-blocks

您可以通过 `$=pod`访问 Pod 树,它包含所有作为分级数据结构的Pod结构。

请注意,所有这些 $=someBlockName 都支持位置和关联角色。

45.13. ~ Twigil

注意: Slangs(俚语)在 Rakudo 中还没有被实现。 NYI = Not Yet Implemented.

~ twigil 是指子语言(称为俚语)。下面是有用的:

变量名

说 明

$~MAIN

the current main language (e.g. Perl statements)

$~Quote

the current root of quoting language

$~Quasi

the current root of quasiquoting language

$~Regex

the current root of regex language

$~Trans

the current root of transliteration language

$~P5Regex

the current root of the Perl 5 regex language

你在你当前的词法作用域中扩充这些语言。

use MONKEY-TYPING;
augment slang Regex {  # derive from $~Regex and then modify $~Regex
    token backslash:std<\Y> { YY };
}

45.14. 变量声明符和作用域

通常, 使用 my 关键字创建一个新的变量就足够了:

my $amazing-variable = "World";
say "Hello $amazing-variable!"; # Hello World!

然而, 有很多声明符能在 Twigils 的能力之外改变作用域的细节。

声明符

作用

my

作为词法作用域名字的开头

our

作为包作用域名字的开头

has

作为属性名的开头

anon

作为私有名字的开头

state

作为词法作用域但是持久名字的开头

augment

给已存在的名字添加定义

supersede

替换已存在名字的定义

还有两个类似于声明符的前缀, 但是作用于预定义变量:

前缀

作用

temp

在作用域的最后恢复变量的值

let

如果 block 成功退出就恢复变量的值

45.14.1. my 声明符

使用 my 声明一个变量给变量一个词法作用域. 这意味着变量只在当前块中存在.例如:

{
    my $foo = "bar";
    say $foo; # -> "bar"
}
say $foo; # !!! "Variable '$foo' is not declared"

它抛出异常,因为只要我们在同一个作用域内 $foo 才被定义. 此外, 词法作用域意味着变量能在新的作用域内被临时地重新定义:

my $location = "outside";

sub outer-location {
    # Not redefined:
    say $location;
}

outer-location; # -> "outside"

sub in-building {
    my $location = "inside";
    say $location;
}

in-building;    # -> "inside"
outer-location; # -> "outside"

如果变量被重新定义了, 任何引用外部变量的代码会继续引用外部变量. 所以, 在这儿, &outer-location 仍然打印外部的 $location:

sub new-location {
    my $location = "nowhere"
    outer-location;
}

new-location; # -> "outside"

为了让 new-location() 能打印 nowwhere, 需要使用 * twigil$location 变为动态变量. 对于子例程来说, my 是默认作用域, 所以 my sub x( ) { }sub x( ) { } 是一样的.

45.14.2. our 声明符

our 跟 my 的作用类似, 除了把别名引入到符号表之外:

module M {
    our $Var;
    # $Var available here
}

# Available as $M::Var here.

45.14.3. 声明一组变量

声明符 myour 接收一组扩在圆括号中的变量作为参数来一次性声明多个变量。

my (@a, $s, %h);

这可以和解构赋值结合使用。任何对这样一个列表的赋值会取得左侧列表中提供的元素数量并且从右侧列表中把对应的值赋值给它们。没有得到赋值的元素会根据变量的类型得到一个未定义值。

my (Str $a, Str $b, Int $c) = <a b>;
say [$a, $b, $c].perl;
# OUTPUT«["a", "b", Int]␤»

要把列表解构到一个单个的值中, 通过使用 ($var,) 创建一个带有一个值的列表字面值。当使用了一个变量声明符时只在单个变量周围提供一个圆括号就足够了。

sub f { 1,2,3 };
my ($a) = f;
say $a.perl;
# OUTPUT«1␤»

要跳过列表中的元素, 使用匿名状态变量 $

my ($,$a,$,%h) = ('a', 'b', [1,2,3], {:1th});
say [$a, %h].perl;
# OUTPUT«["b", {:th(1)}]␤»

45.14.4. has 声明符

has 作用在类的实例或 role 的属性上, 还有类或 roles 的方法上. has 隐式作用于方法上, 所以 has method x() {}method x() {} 做得是相同的事情。

查看面向对象获取更多文档和例子。

has method x( ) { }

等价于:

method x( ) { }

45.14.5. anon 声明符

anon 声明符阻止符号本安装在词法作用域内, 还有方法表中, 和其它任何地方. 例如, 你可以使用 anon 声明一个知道自己名字的子例程, 但是仍然不会被安装到作用域内:

my %operations =
    half   => anon sub half($x)   { $x / 2  },
    square => anon sub square($x) { $x * $x },
    ;
say %operations<square>.name;       # square
say %operations<square>(8);         # 64

45.14.6. state 声明符

state 声明词法作用域变量, 就像 my 那样。然而, 初始化只发生一次, 就在正常执行流中第一次遇见初始化的时候。因此, state 变量会在闭合块或 程序的多次执行之间保留它们的值。

因此, 下面这个子例程:

sub a {
    state @x;
    state $l = 'A';
    @x.push($l++);
};

say a for 1..6;

会持续增加 $l 并在每次被调用时把它追加到 @x 中, 所以它会打印出:

[A]
[A B]
[A B C]
[A B C D]
[A B C D E]
[A B C D E F]

This works per "clone" of the containing code object, as in this example:

({ state $i = 1; $i++.say; } xx 3).map: {$_(), $_()}; # says 1 then 2 thrice

注意,这不是一个线程安全的解构, 当同一个 block 的同一个克隆运行在多个线程中时。要知道方法只有每个类一个克隆,而不是每个对象。

至于 my,声明多个状态变量必须放置在圆括号中, 而声明一个单一变量,圆括号可以省略。

请注意,许多操作符都伴随着隐式绑定,什么会导致超距作用。使用 .clone 或强迫创建一个可以绑定的新容器。

my @a;
sub f() {
    state $i;
    $i++;
    @a.push: "k$i" => $i # <-- .clone goes here
};
f for 1..3;
dd @a; # «Array $var = $[:k1(3), :k2(3), :k3(3)]»

所有的状态变量都是线程间共享的。这个结果可能是你不希望得到的或危险的。

sub code(){ state $i = 0; say ++$i; $i };
await
    start { loop { last if code() >= 5 } },
    start { loop { last if code() >= 5 } };

# OUTPUT«1␤2␤3␤4␤4␤3␤5␤»
# OUTPUT«2␤1␤3␤4␤5␤»
# many other more or less odd variations can be produced

45.14.7. $ 变量

和显式地声明命名状态变量一样, $ 能够用作不带显式状态声明的匿名状态变量。

say "1-a 2-b 3-c".subst(:g, /\d/, {<one two three>[$++]});
# OUTPUT«one-a two-b three-c␤»

更进一步, 状态变量不需要存在于子例程中。你可以, 举个例子, 在单行程序中使用 $ 在文件中编号行号。

raku -ne 'say ++$ ~ " $_"' example.txt

实际上词法范围内每个对 $ 的引用都是是一个单独的变量。

raku -e '{ say ++$; say $++  } for ^5'
# OUTPUT«1␤0␤2␤1␤3␤2␤4␤3␤5␤4␤»

如果在作用域内你需要多次引用 $ 的值, 那么它应该被拷贝到一个新的变量中。

sub foo() {
    given ++$ {
        when 1 {
            say "one";
        }
        when 2 {
            say "two";
        }
        when 3 {
            say "three";
        }
        default {
            say "many";
        }
    }
}

foo() for ^3;
# OUTPUT«one␤two␤three␤»

45.14.8. @ 变量

$ 变量类似, 也有一个位置匿名状态变量 @

sub foo($x) {
    say (@).push($x);
}

foo($_) for ^3;

# OUTPUT:
# [0]
# [0 1]
# [0 1 2]

这里的 @ 是用圆括号括起来了以和名为 @.push 的类成员变量消除歧义。索引访问并不需要这种歧义,但你需要拷贝这个值,以便用它做任何有用的事情。

sub foo($x) {
    my $v = @;
    $v[$x] = $x;
    say $v;
}

foo($_) for ^3;

# OUTPUT:
# [0]
# [0 1]
# [0 1 2]

就和 $ 一样, 作用域中的每次提及 @ 就引入了一个新的匿名数组。

45.14.9. % 变量

最后, 还有一个关联匿名状态变量 %

sub foo($x) {
    say (%).push($x => $x);
}

foo($_) for ^3;

# OUTPUT:
# 0 => 0
# 0 => 0, 1 => 1
# 0 => 0, 1 => 1, 2 => 2

关于歧义的同样警告适用。正如你可能期望,索引访问也有可能(使用复制以使之有用)。

sub foo($x) {
    my $v = %;
    $v{$x} = $x;
    say $v;
}

foo($_) for ^3;

# OUTPUT:
# 0 => 0
# 0 => 0, 1 => 1
# 0 => 0, 1 => 1, 2 => 2

就像其它的匿名状态变量一样, 在给定作用域中每次提及 % 最终都会引入一个单独的变量。

45.14.10. augment 声明符

使用 augment, 你可以给已经存在的类或 grammars 增加属性和方法.

因为类通常使用 our 作用域, 因此是全局的, 这意味着修改全局状态, 这是强烈不鼓励的, 对于大部分情况, 有更好的方法.

# don't do this
use MONKEY-TYPING;
augment class Int {
    method is-answer { self == 42 }
}
say 42.is-answer;       # True

45.14.11. temp 前缀

像 my 一样, temp 在作用域的末尾恢复旧的变量值. 然而, temp 不创建新的变量.

my $in = 0; # temp will "entangle" the global variable with the call stack
            # that keeps the calls at the bottom in order.
sub f(*@c) {
    (temp $in)++;
     "<f>\n"
     ~ @c>>.indent($in).join("\n")
     ~ (+@c ?? "\n" !! "")
     ~ '</f>'
};
sub g(*@c) {
    (temp $in)++;
    "<g>\n"
    ~ @c>>.indent($in).join("\n")
    ~ (+@c ?? "\n" !! "")
    ~ "</g>"
};
print g(g(f(g()), g(), f()));

# OUTPUT:
# <g>
#  <g>
#   <f>
#    <g>
#    </g>
#   </f>
#   <g>
#   </g>
#   <f>
#   </f>
#  </g>
# </g>

45.14.12. let 前缀

跟 temp 类似, 如果 block 没有成功退出则恢复之前的值。成功的退出意味着该 block 返回了一个定义过的值或一个列表。

my $answer = 42;

{
    let $answer = 84;
    die if not Bool.pick;
    CATCH {
        default { say "it's been reset :(" }
    }
    say "we made it 84 sticks!";
}

say $answer;

在上面的例子中, 如果 Bool.pick 返回 true, 那么答案会保持为 84, 因为那个 block 返回了一个定义了的值(say 返回 true)。 否则那个 die 语句会让那个 block 不成功地退出, 把答案重新设置为 42。

45.15. 类型约束和初始化

变量可以有类型约束, 约束在声明符和变量名之间:

my Int $x = 42;
$x = 'a string'; # throws an X::TypeCheck::Assignment error
CATCH { default { put .^name, ': ', .Str } }
# OUTPUT: X::TypeCheck::Assignment: Type check failed in assignment to $x; expected Int but got Str ("a string")

如果一个标量有类型约束但是没有初始值, 它会使用类型约束的类型对象来初始化.

my Int $x;
say $x.^name;    # Int
say $x.defined;  # False

没有显式类型约束的标量的类型为 Mu, 但是默认会是 Any 类型的对象.

带有 @ 符号的变量会被初始化为空的数组; 带有 % 符号的变量会被初始化为空的散列.

变量的默认值可以使用 is default 特性设置, 通过把 Nil 赋值给变量来重新应用默认值:

my Real $product is default(1);
say $product;                       # 1
$produce *= 5;
say $product;                       # 5
$product = Nil;
say $product;                       # 1

45.16. 默认的有定义的变量指令

为了强制所有的变量拥有一个有定义的约束, 使用 use variables :D 指令。这个指令是词法作用域的并且可以使用 use variables :_ 指令进行切换。

use variables :D;
my Int $i;
# OUTPUT«===SORRY!=== Error while compiling <tmp>␤Variable definition of type Int:D (implicit :D by pragma) requires an initializer ...
my Int $i = 1; # that works
{ use variables :_; my Int $i; } # 在这个 block 中关掉它

请注意, 赋值 Nil 会把这个变量恢复为它的默认值。一个有定义的约束类型的默认值是类型名加上 :D(例如 Int:D)。That means a definedness contraint is no guarantee of definedness. 这只适用于变量初始化, 不适用于签名。

45.17. 特殊变量

Pre-defined lexical variables

每个代码块中都有3个特别的变量:

变量

意义

$_

特殊变量

$/

正则匹配

$!

异常

45.17.1. $_

$ 是特殊变量,在没有显式标识的代码块中,它是默认参数。所以诸如 for @array { …​ }given $var { …​ } 之类的结构会将变量绑定给 $.

for <a b c> { say $_ }  # sets $_ to 'a', 'b' and 'c' in turn
say $_ for <a b c>;     # same, even though it's not a block
given 'a'   { say $_ }  # sets $_ to 'a'
say $_ given 'a';       # same, 尽管这不是一个块

CATCH 块将 $ 设置为捕获到的异常。 ~~ 智能匹配操作符。 对 $ 调用一个方法可以省略特殊变量 $_ 的名字,从而写的更短:

.say;                   # 与 $_.say 相同

m/regex//regex/ 正则匹配 和 s/regex/subst/ 替换是作用于 $_ 上的.

say "Looking for strings with non-alphabetic characters...";
for <ab:c d$e fgh ij*> {
    .say if m/<!alpha>/;
}

输出:

Looking for strings with non-alphabetic characters...
ab:c
d$e
ij*

45.17.2. $/

$/ 是匹配变量。它存储着最近一次正则匹配的结果,通常包含 Match 类型的对象。

'abc 12' ~~ /\w+/;  # 设置 $/ 为一个Match 对象
say $/.Str;         # abc

Grammar.parse 方法会把调用者的 $/ 设置为 Match object 的结果。看下面的代码:

use XML::Grammar; # panda install XML
XML.Grammar.parse("<p>some text</p>");
say $/;

# OUTPUT:
# 「<p>some text</p>」
#  root => 「<p>some text</p>」
#   name => 「p」
#   child => 「some text」
#    text => 「some text」
#    textnode => 「some text」
#  element => 「<p>some text</p>」
#   name => 「p」
#   child => 「some text」
#    text => 「some text」
#    textnode => 「some text」

其他匹配变量是 $/ 元素的别名:

$0          # same as $/[0]
$1          # same as $/[1]
$<named>    # same as $/<named>

45.17.3. 位置属性

如果正则中有捕获分组, $/ 中会有位置属性. 它们由圆括号组成.

'abbbbbcdddddeffg' ~~ / a (b+) c (d+ef+) g /;
say $/[0]; # 「bbbbb」
say $/[1]; # 「dddddeff」

这些捕获分组也能使用 $0,$1,$2 等便捷形式取得:

say $0; # 「bbbbb」
say $1; # 「dddddeff」

要获取所有的位置属性, 使用 $/.list, @$/,@( ) 中的任意一个都可以:

say @().join; # bbbbbdddddeff

45.17.4. 命名属性

如果正则中有命名捕获分组, $/ 可以有命名属性, 或者正则调用了另一个正则:

'I.... see?' ~~ / \w+ $<punctuation>=[ <-[\w\s]>+ ] \s* $<final-word> = [ \w+ . ] /;
say $/<punctuation>; # 「....」
say $/<final-word>;  # 「see?」

这些命名捕获分组也能使用便捷形式的 $<named> 获取:

say $<punctuation>; # 「....」
say $<final-word>;  # 「see?」

要获取所有的命名属性, 使用 $/.hash, %$/, `%()`中的任何一个:

say %().join;  # "punctuation     ....final-word  see?"

45.17.5. $! 变量

$! 是错误变量. 如果 try block 或语句前缀捕获到异常, 那个异常就会被存储在 $! 中。如果没有捕获到异常, 那么 $! 会被设置为 Any 类型对象。 注意, CATCH 块不设置 $!。CATCH 在 block 中把 $_ 设置为捕获到的异常。

45.18. 编译时变量

Compile-time Variables

说明

$?FILE

所在文件

$?LINE

所在行

::?CLASS

所在类

&?ROUTINE

所在子例程

&?BLOCK

所在块

%?LANG

What is the current set of interwoven languages?

%?RESOURCES

The files associated with the "Distribution" of the current compilation unit.

for '.' {
    .Str.say when !.IO.d;
    .IO.dir()>>.&?BLOCK when .IO.d # lets recurse a little!
}

其它编译时变量:

Compile-time Variables

说明

$?PACKAGE

所在包

$?MODULE

所在模块

$?CLASS

所在类(as variable)

$?ROLE

所在角色(as variable)

$?GRAMMAR

所在 grammar

$?TABSTOP

在 heredoc 或 虚拟边距中 tab 有多少空格

$?USAGE

从 MAIN 程序的签名中生成的使用信息

$?ENC

Str.encode/Buf.decode/various IO 方法的默认编码.

45.19. 动态变量

Dynamic Variable

说明

$*ARGFILES

神奇的命令行输入句柄

@*ARGS

来自命令行的参数

$*IN

标准输入文件句柄, AKA stdin

$*OUT

标准输出文件句柄, AKA stdout

$*ERR

标准错误文件句柄, AKA stderr

%*ENV

环境变量

$*REPO

存储安装过的/加载了的模块信息的变量

$*TZ

系统的本地时区.

$*CWD

当前工作目录.

$*KERNEL

在哪个内核下运行

$*DISTRO

在哪个操作系统分发下运行

$*VM

在哪个虚拟机下运行

$*PERL

在哪个 Perl 下运行

$*PID

当前进程的进程 ID

$*PROGRAM-NAME

当前可执行程序的路径就像它通过命令行键入一样, 或 -e 如果 perl 引用了 -e 标记

$*PROGRAM

正被执行的 Perl 程序的位置( 以 IO::Path 对象的形式)

$*EXECUTABLE

当前运行的可执行 perl 的绝对路径

$*EXECUTABLE-NAME

当前运行的可执行 perl 程序的名字。(e.g. raku-p, raku-m, Niecza.exe)

$*USER

正在运行该程序的用户. 它是一个被求值为 "username (uid)" 的对象. 它只有在被当作字符串时才被求值为用户名, 如果被当作数字则被求值为数值化的用户 id。

$*GROUP

运行程序的用户的主要组. 它是被计算为 "groupname (gid)" 的对象.它只有在被当作字符串时才被求值为组名, 如果被当作数字则被求值为数值化的组 id。

$*HOME

代表当前运行程序的用户家目录的 IO::Path 对象。如果家目录不确定则为 Nil。

$*SPEC

程序运行的平台的合适的 IO::Spec 子类, 对于特定操作系统代码,使用智能匹配: say "We are on Windows!" if $*SPEC ~~ IO::Spec::Win32

$*TMPDIR

代表着 "系统临时目录" 的 IO::Path 对象

$*TOLERANCE

由 ⇐~⇒ 操作符使用并且任何依赖它的操作, 来决定两个值是否近似地相等, 默认为 1e-15。

$*THREAD

代表当前执行线程的 Thread 对象。

$*SCHEDULER

代表当前默认调度程序的 ThreadPoolScheduler 对象。

注意 $*SCHEDULER 的用法:

对于当前的 Rakudo, 这个默认在方法 .hyper.race 上采用最大 16 个线程。要更改线程的最大数量, 要么在运行 perl 之前设置环境变量 RAKUDO_MAX_THREADS 的值, 要么在使用 .hyper 或 .race 之前创建一个默认改变了的作用域的拷贝:

my $*SCHEDULER = ThreadPoolScheduler.new( max_threads => 64 );

这种行为在 spec 测试中没有被测试并且还会变化。

46. Grammar

Grammar 是一种功能强大的工具, 用于对文本进行解构。Grammar 通常返回通过解释文本而创建的数据结构。

例如, 使用 Raku 风格的 grammar 来解析和执行 Raku 自身。

对于普通 Raku 用户来说, 更实用的一个例子是 JSON::Tiny 模块, 该模块可以反序列化任何合法的 JSON 文件, 而且反序列代码只有不到 100 行, 还能扩展。

如果你在学校就不喜欢 grammar, 不要让它成为阻止你学习 grammar 的理由。Grammar 允许你像类组织方法那样组织正则表达式。

46.1. 具名正则

Grammar 的主要组成部分是 regexes。 而 Raku 的 regexes 语法不在该文档的讨论范围内, 具名正则有自己的特殊语法, 这跟子例程的定义很像:

# 普通 regex 中空格被忽略, [] 是非捕获括号
my regex number { \d+ [ \. \d+ ]? }

在这个例子中我们使用 my 关键字指定这个 regex 是本地作用域的, 因为具名正则通常用于 grammar 里面。

正则被命名的好处是能够轻松地在其它地方重用正则表达式:

say so "32.51" ~~ &number;                         # OUTPUT: «True␤»
say so "15 + 4.5" ~~ /<number>\s* '+' \s*<number>/ # OUTPUT: «True␤»

regex 不是具名正则的唯一声明符, 实际上 , regex 声明符用的最少。 大多数情况下, 使用的是 tokenrule 声明符。token 和 rule 这两个声明符都是带棘齿(ratcheing)的, 这意味着如果匹配失败, 那么匹配引擎就不会回溯并继续尝试匹配了。这通常可以满足你的需求, 但不适用于所有情况:

my regex works-but-slow { .+ q } # 可能会回溯
my token fails-but-fast { .+ q } # 不回溯
my $s = 'Tokens and rules won\'t backtrack, which makes them fail quicker!';
say so $s ~~ &works-but-slow; # OUTPUT: «True␤»
say so $s ~~ &fails-but-fast; # OUTPUT: «False␤»
                              # .+ 得到了整个字符串但不回溯

请注意, 非回溯是作用在项上的, 即, 如下面的示例所示, 如果匹配了某些内容, 则将永远不会回溯。 但是, 如果匹配失败, 但是 ||| 引入了另一个候选者, 将会重试匹配。

my token tok-a { .* d  };
my token tok-b { .* d | bd };
say so "bd" ~~ &tok-a;        # OUTPUT: «False␤»
say so "bd" ~~ &tok-b;        # OUTPUT: «True␤»

46.1.1. Rules

tokenrule 声明符的唯一区别就是 rule 声明符会让正则中的 :sigspace 修饰符起效:

my token non-space-y { 'once' 'upon' 'a' 'time' }
my rule space-y { 'once' 'upon' 'a' 'time' }
say so 'onceuponatime'    ~~ &non-space-y; # OUTPUT: «True␤»
say so 'once upon a time' ~~ &non-space-y; # OUTPUT: «False␤»
say so 'onceuponatime'    ~~ &space-y;     # OUTPUT: «False␤»
say so 'once upon a time' ~~ &space-y;     # OUTPUT: «True␤»

46.2. 创建 Grammar

Grammar 是使用 grammar 关键字而不是 class 关键字声明类时, 它们自动获得的超类。Grammar 只能用于解析文本; 如果想提取复杂的数据, 则可以在 grammar 中添加 action, 或者建议将 action 对象和 grammar 结合使用。如果未使用 action 对象, 则 .parse 返回一个 Match 对象并默认把默认的匹配对象 $/ 设置为相同的值。

46.2.1. 原型正则

Grammar 由 rule, token 和 regex 组成; 他们实际上都是方法, 因为 grammar 是类。

这些方法可以共用一个名称和功能, 因此可以使用 proto

例如, 如果你有很多备选分支(alternations), 则可能难以生成可读性好的代码或将 grammar 子类化。在下面的 Actions 类中, TOP 方法中的三元操作符并不理想, 并且当我们添加的操作越多, 它就变得越糟糕:

grammar Calculator {
    token TOP { [ <add> | <sub> ] }
    rule  add { <num> '+' <num> }
    rule  sub { <num> '-' <num> }
    token num { \d+ }
}

class Calculations {
    method TOP ($/) { make $<add> ?? $<add>.made !! $<sub>.made; }
    method add ($/) { make [+] $<num>; }
    method sub ($/) { make [-] $<num>; }
}

say Calculator.parse('2 + 3', actions => Calculations).made;

# OUTPUT: «5␤»

为了让事情变得更好, 我们可以在 token 上使用类似于 :sym<…​> 副词的原型正则表达式:

grammar Calculator {
    token TOP { <calc-op> }

    proto rule calc-op          {*}
          rule calc-op:sym<add> { <num> '+' <num> }
          rule calc-op:sym<sub> { <num> '-' <num> }

    token num { \d+ }
}

class Calculations {
    method TOP              ($/) { make $<calc-op>.made; }
    method calc-op:sym<add> ($/) { make [+] $<num>; }
    method calc-op:sym<sub> ($/) { make [-] $<num>; }
}

say Calculator.parse('2 + 3', actions => Calculations).made;

# OUTPUT: «5␤»

在这个 grammar 中, 备选分支(alternation)已经被 <calc-op> 替换掉了, 这实际上是我们将要创建的一组值的名称。为此, 我们使用 proto rule calc-op 定义了一个 rule 原型类型(prototype)。我们之前的每个备选分支都由新的 rule calc-op 定义替换, 并且备选分支的名字附加有 :sym<> 副词。

在声明 action 的类中, 我们现在摆脱了三目操作符, 仅从 $<calc-op> 匹配对象上获取 .made 值。现在各备选分支的 action 都遵循与 grammar 相同的命名模式: method calc-op:sym<add>method calc-op:sym<sub>

当你将 grammar 和 action 子类化时, 可以看到此方法的真正魅力。假设我们想为 calculator 增加一个乘法功能:

grammar BetterCalculator is Calculator {
    rule calc-op:sym<mult> { <num> '*' <num> }
}

class BetterCalculations is Calculations {
    method calc-op:sym<mult> ($/) { make [*] $<num> }
}

say BetterCalculator.parse('2 * 3', actions => BetterCalculations).made;

# OUTPUT: «6␤»

我们需要做的只是对 calc-op 组添加额外的 rule 和 action, 感谢原型正则表达式, 所有的东西都能正常工作。

46.3. 特殊的 token

46.3.1. TOP

grammar Foo {
    token TOP { \d+ }
}

TOP token 是用 grammar 解析时, 尝试匹配的默认第一个 token。请注意, 如果使用 .parse 方法进行解析, 那么 token TOP 会自动地锚定到字符串的开头和结尾。如果不想解析整个字符串, 请使用 .subparse

也可以使用 rule TOPregex TOP

.parse.subparse.parsefile 中使用 :rule 命名参数可以选择一个不同的 token 来进行首次匹配。 这些都是 Grammar 方法。

46.3.2. ws

默认的 ws 匹配零个或多个空白字符, 只要所匹配之处不在单词里面(用代码来表示就是 regex ws { <!ww> \s* }):

# First <.ws> matches word boundary at the start of the line
# and second <.ws> matches the whitespace between 'b' and 'c'
say 'ab   c' ~~ /<.ws> ab <.ws> c /; # OUTPUT: «「ab   c」␤»

# Failed match: there is neither any whitespace nor a word
# boundary between 'a' and 'b'
say 'ab' ~~ /. <.ws> b/;             # OUTPUT: «Nil␤»

# Successful match: there is a word boundary between ')' and 'b'
say ')b' ~~ /. <.ws> b/;             # OUTPUT: «「)b」␤»

请记住, 我们对空白不感兴趣, 所以我们在 ws 前面加了一个点, 以避免捕获。由于空格通常是分隔符, 因此通常会找到它。

当使用 rule 代替 token 时, 会默认启用 :sigspace, 并且将项和闭合圆括号/方括号之后的任何空白都转换为对 ws 的非捕获调用, 写为 <.ws>。 其中 . 表示不捕捉。 也就是说:

rule entry { <key> '=' <value> }

等价于:

token entry { <key> <.ws> '=' <.ws> <value> <.ws> }

你也可以重新定义自己的 ws token:

grammar Foo {
    rule TOP { \d \d }
}.parse: "4   \n\n 5"; # Succeeds

grammar Bar {
    rule TOP { \d \d }
    token ws { \h*   }
}.parse: "4   \n\n 5"; # Fails

甚至捕获空白, 但你需要显式地使用它。 请注意, 在下一个示例中, 我们使用 token 代替 rule, 因为后者会导致空白被隐式的非捕获 .ws 占用。

grammar Foo { token TOP {\d <ws> \d} };
my $parsed = Foo.parse: "3 3";
say $parsed<ws>; # OUTPUT: «「 」␤»

46.3.3. sym

<sym> token 可以在原型正则表达式中使用, 以匹配该特定正则表达式的 :sym 副词的字符串值:

grammar Foo {
    token TOP { <letter>+ }
    proto token letter {*}
    token letter:sym<P> { <sym> }
    token letter:sym<e> { <sym> }
    token letter:sym<r> { <sym> }
    token letter:sym<l> { <sym> }
    token letter:sym<*> {   .   }
}.parse("I ♥ Perl", actions => class {
    method TOP($/) { make $<letter>.grep(*.<sym>).join }
}).made.say; # OUTPUT: «Perl␤»

当你已经将原型正则表达式与要匹配的字符串区分开来时, 这会派上用场, 因为使用 <sym> token 可防止重复这些字符串。

46.3.4. 总是成功断言

<?> 是始终成功断言。 当它用作 grammar 中的 token 时, 它可以被用于触发 Action 类方法。在下面的 grammar 中, 我们查找阿拉伯数字并且使用始终成功断言定义 succ token。

在 action 类中, 我们使用对 succ 方法的调用来设置(在这个例子中, 我们在 @!numbers 中准备了一个新元素)。在 digit 方法中, 我们把阿拉伯数字转换为梵文数字并且把它添加到 @!numbers 数组的最后一个元素中。多亏了 succ, 最后一个元素总是当前正被解析的 digit 数字的数。

grammar Digifier {
    rule TOP {
        [ <.succ> <digit>+ ]+
    }
    token succ  { <?> }
    token digit { <[0..9]> }
}

class Devanagari {
    has @!numbers;
    method digit ($/) { @!numbers.tail ~= <०  १  २  ३  ४  ५  ६  ७  ८  ९>[$/] }
    method succ  ($)  { @!numbers.push: ''     }
    method TOP   ($/) { make @!numbers[^(*-1)] }
}

say Digifier.parse('255 435 777', actions => Devanagari.new).made;
# OUTPUT: «(२५५ ४३५ ७७७)␤»

46.4. Grammar 中的方法

在 grammar 中使用 method 代替 ruletoken 也是可以的, 只要它们返回一个 Match:

grammar DigitMatcher {
    method TOP (:$full-unicode) {
        $full-unicode ?? self.num-full !! self.num-basic;
    }
    token num-full  { \d+ }
    token num-basic { <[0..9]>+ }
}

上面的 grammar 会根据提供给 subparse 方法的参数尝试不同的匹配:

say +DigitMatcher.subparse: '12७१७९०९', args => \(:full-unicode);
# OUTPUT: «12717909␤»

say +DigitMatcher.subparse: '12७१७९०९', args => \(:!full-unicode);
# OUTPUT: «12␤»

46.5. Grammar 中的动态变量

可以在 token 标记中定义变量, 方法是在定义变量的代码行前面加上 :。 通过花括号, 可以将任意代码嵌入到 token 中的任何位置。 这对于保存 token 标记之间的状态很有用, 可用于更改 grammar 解析文本的方式。 在 token 中使用动态变量(带有 $, @, &*, %* twigils 的变量)可在其定义的那个变量内级联遍历此后定义的所有 token, 从而避免了将它们作为参数从 token 传递到 token。

动态变量的一种用途是匹配保护。 此示例使用守卫来解释哪个正则表达式类按字面意义解析空格:

grammar GrammarAdvice {
    rule TOP {
        :my Int $*USE-WS;
        "use" <type> "for" <significance> "whitespace by default"
    }
    token type {
        | "rules"   { $*USE-WS = 1 }
        | "tokens"  { $*USE-WS = 0 }
        | "regexes" { $*USE-WS = 0 }
    }
    token significance {
        | <?{ $*USE-WS == 1 }> "significant"
        | <?{ $*USE-WS == 0 }> "insignificant"
    }
}

在这里, 仅当提及 rules, tokens 或 regexes 所指定的状态与正确的守卫匹配时, 诸如 "use rules for significant whitespace by default" 之类的文本才会匹配:

say GrammarAdvice.subparse("use rules for significant whitespace by default");
# OUTPUT: «use rules for significant whitespace by default»

say GrammarAdvice.subparse("use tokens for insignificant whitespace by default");
# OUTPUT: «use tokens for insignificant whitespace by default»

say GrammarAdvice.subparse("use regexes for insignificant whitespace by default");
# OUTPUT: «use regexes for insignificant whitespace by default»

say GrammarAdvice.subparse("use regexes for significant whitespace by default")
# OUTPUT: #<failed match>

46.6. Grammar 中的属性

Grammar 中可以定义属性。 但是, 只能通过方法访问属性。 尝试从 token 标记中使用它们会引发异常, 因为 token 标记是 Match 的方法, 而不是 grammar 本身。 请注意, 从 token 标记中调用的方法中更改属性只会修改该 token 自己的 match 对象的属性! 如果将 grammar 属性设置为公开, 则可以在解析后返回的匹配项中访问 grammar 属性:

grammar HTTPRequest {
    has Bool $.invalid;

    token TOP {
        <type> <.ns> <path> <.ns> 'HTTP/1.1' <.crlf>
        [ <field> <.crlf> ]+
        <.crlf>
        $<body>=.*
    }

    token type {
        | [ GET | POST | OPTIONS | HEAD | PUT | DELETE | TRACE | CONNECT ] <.accept>
        | <-[\/]>+ <.error>
    }

    token path {
        | '/' [[\w+]+ % \/] [\.\w+]? <.accept>
        | '*' <.accept>
        | \S+ <.error>
    }

    token field {
        | $<name>=\w+ <.ns> ':' <.ns> $<value>=<-crlf>* <.accept>
        | <-crlf>+ <.error>
    }

    method error(--> ::?CLASS:D) {
        $!invalid = True;
        self;
    }

    method accept(--> ::?CLASS:D) {
        $!invalid = False;
        self;
    }

    token crlf { # network new line (usually seen as "\r\n")
        # Several internet protocols (such as HTTP, RF 2616) mandate
        # the use of ASCII CR+LF (0x0D 0x0A) to terminate lines at
        # the protocol level (even though, in practice, some applications
        # tolerate a single LF).
        # Raku, Raku grammars and strings (Str) adhere to Unicode
        # conformance. Thus, CR+LF cannot be expressed unambiguously
        # as \r\n in in Raku grammars or strings (Str), as Unicode
        # conformance requires \r\n to be interpreted as \n alone.
        \x[0d] \x[0a]
    }
    token ns { # network space
        # <ws> would consume, e.g., newlines, and \h (and \s) would accept
        # more codepoints than just ASCII single space and the tab character.
        [ ' ' | <[\t]> ]*
    }
}

my $crlf = "\x[0d]\x[0a]";
my $header = "GOT /index.html HTTP/1.1{$crlf}Host: docs.raku.org{$crlf}{$crlf}body";
my $m = HTTPRequest.parse($header);
say "type(\"$m.<type>\")={$m.<type>.invalid}";
# OUTPUT: type("GOT ")=True
say "path(\"$m.<path>\")={$m.<path>.invalid}";
# OUTPUT: path("/index.html")=False
say "field(\"$m.<field>[0]\")={$m.<field>[0].invalid}";
# OUTPUT: field("Host: docs.raku.org")=False

注意:如果我们想以某种方式(在此不完整示例的上下文中)严格遵守 HTTP/1.1(RFC 2616), 则 $crlf 和 token <.crlf> 是必需的。 原因是, 与 RFC 2616 相比, Raku 是 Unicode 兼容的, 并且 \r\n 需要解释为单个 \n, 从而阻止 grammar 在 HTTP 协议的某些场景中正确解析包含 \r\n 的字符串。 请注意属性 invalid 在每个组件中都是本地的(例如, 对于 <type> 其值为 True, 而对于 <path> 其 值为 False)。 还要注意我们有一个 accept 方法, 否则 invalid 属性将是未初始化的(即使存在)。

46.7. 传递参数到 Grammar 中

要将参数传递到 grammar 中, 可以在 grammar 的任何解析方法上使用 :args 命名参数。 传递的参数应在列表中。

grammar demonstrate-arguments {
    rule TOP ($word) {
    "I like" $word
    }
}

# Notice the comma after "sweets" when passed to :args to coerce it to a list
say demonstrate-arguments.parse("I like sweets", :args(("sweets",)));
# OUTPUT: «「I like sweets」␤»

一旦传入参数, 就可以在 grammar 内调用命名正则表达式。

grammar demonstrate-arguments-again {
    rule TOP ($word) {
        <phrase-stem><added-word($word)>
    }

    rule phrase-stem {
       "I like"
    }

    rule added-word($passed-word) {
       $passed-word
    }
}

say demonstrate-arguments-again.parse("I like vegetables", :args(("vegetables",)));
# OUTPUT: 「I like vegetables」␤»
# OUTPUT:  «phrase-stem => 「I like 」␤»
# OUTPUT:  «added-word => 「vegetables」␤»

另外, 你可以初始化动态变量并在 grammar 中使用任何这样的参数。

grammar demonstrate-arguments-dynamic {
   rule TOP ($*word, $*extra) {
      <phrase-stem><added-words>
   }
   rule phrase-stem {
      "I like"
   }
   rule added-words {
      $*word $*extra
   }
}

say demonstrate-arguments-dynamic.parse("I like everything else",
  :args(("everything", "else")));
# OUTPUT: «「I like everything else」␤»
# OUTPUT:  «phrase-stem => 「I like 」␤»
# OUTPUT:  «added-words => 「everything else」␤»

46.8. Action Object

一个成功的 grammar 匹配会给你一棵匹配对象的解析树, 匹配树越深入, 则 grammar 中的分支越多, 那么在匹配树中导航以获取你真正感兴趣的信息就变的越来越困难。

为了避免你在匹配树中迷失, 你可以提供一个 action 对象。Grammar 中每次成功解析具名规则(named rule)之后, 它都会尝试调用一个和该 grammar rule 同名的方法, 并给这个方法传递一个 Match 对象作为位置参数。如果不存在这样的同名方法, 就跳过。

这儿有一个例子来说明 grammar 和 action:

grammar TestGrammar {
    token TOP { \d+ }
}

class TestActions {
    method TOP($/) {
        $/.make(2 + $/);
    }
}

my $match = TestGrammar.parse('40', actions => TestActions.new);
say $match;         # OUTPUT: «「40」␤»
say $match.made;    # OUTPUT: 42

TestActions 的一个实例作为具名参数 actions 被传递给 parse 调用, 然后当 token TOP 匹配成功之后, 就会自动调用方法 TOP, 并传递匹配对象作为此方法的参数。

为了明确说明该参数是匹配对象, 该示例使用 $/ 作为 action 方法的参数名, 尽管这只是一个方便的约定, 没有内在的含义。 $match 也可以工作, 尽管使用 $/ 确实提供了将 $<capture> 作为 $/<capture> 的快捷方式的优势; 无论如何, 我们在 TOP 的 action 中使用了另一个参数。

下面是一个更有说服力的例子:

grammar KeyValuePairs {
    token TOP {
        [<pair> \v+]*
    }

    token pair {
        <key=.identifier> '=' <value=.identifier>
    }

    token identifier {
        \w+
    }
}

class KeyValuePairsActions {
    method pair      ($/) {
        $/.make: $<key>.made => $<value>.made
    }
    method identifier($/) {
        # subroutine `make` is the same as calling .make on $/
        make ~$/
    }

    method TOP ($match) {
        # can use any variable name for parameter, not just $/
        $match.make: $match<pair>».made
    }
}


my $actions = KeyValuePairsActions;
my @res = KeyValuePairs.parse(q:to/EOI/, :$actions).made;
second=b
hits=42
perl=6
EOI

for @res -> $p {
    say "Key: $p.key()\tValue: $p.value()";
}

这会输出:

Key: second     Value: b
Key: hits       Value: 42
Key: perl       Value: 6

pair 这个 rule, 解析一对由等号分割的 pair, 将对 token identifier 的两次调用起了别名以分割捕获名称, 从而使它们使用起来更容易, 更直观, 因为它们会用在对应的 action 中。对应的 action 方法构建了一个 Pair 对象, 并使用子匹配对象的 .made 属性。这(就如 TOP action 方法一样)也暴露了一个事实: 子匹配的 action 方法在那些调用正则/外部正则之前就被调用。所以 action 方法是按后续调用的。

名为 TOP 的 action 方法仅仅把由 pair 这个 rule 的多重匹配组成的所有对象收集到一块, 然后以一个列表的方式返回。

还要注意 KeyValuePairsActions 是作为类型对象传递给 parse 方法的, 这是因为 action 方法中没有使用属性(属性只能通过实例来访问)。

我们可以使用继承扩展上面的例子。

use KeyValuePairs;

unit grammar ConfigurationSets is KeyValuePairs;

token TOP {
    <configuration-element>+ %% \v
}

token configuration-element {
    <pair>+ %% \v
}

token comment {
    \s* '#' .+? $$
}

token pair {
    <key=.identifier> '=' <value=.identifier> <comment>?
}

我们将对前面的示例进行子类化(实际上是对 grammar 进行子类化)。 我们通过添加注释来重写 pair 的定义; 以前的 TOP rule 已降级为 configuration-element, 并且有一个新的 TOP 现在考虑由垂直空白分隔的配置元素集。 我们还可以通过子类化 action 类来重用 action:

use KeyValuePairs;

unit class ConfigurationSetsActions is KeyValuePairsActions;

method configuration-element($match) {
    $match.make: $match<pair>».made
}

method TOP ($match) {
    my @made-elements = gather for $match<configuration-element> {
        take $_.made
    };
    $match.make( @made-elements );

}

所有现有的 action 都可以重用, 尽管显然必须为 grammar 中的新元素(包括 TOP)编写新的 action。 这些可以和此脚本一起使用:

use ConfigurationSets;
use ConfigurationSetsActions;

my $actions = ConfigurationSetsActions;
my $sets = ConfigurationSets.parse(q:to/EOI/, :$actions).made;
second=b # Just a thing
hits=42
perl=6

third=c # New one
hits=33
EOI

for @$sets -> $set {
    say "Element→ $set";
}

这会打印:

Element→ second b hits 42 perl 6
Element→ third c hits 33

其它情况下, action 方法可能会在属性中保存状态。 那么这当然需要你传递一个实例给 parse 方法。

注意, token ws 有点特殊: 当 :sigspace 开启的时候(就是我们使用 rule`的时候), 它会替换某些空白序列。这就是为什么 `rule pair 中等号周围的空格可以正常工作的原因, 以及为什么在闭合 } 之前的空白不会吞噬 token TOP 中的换行符的原因。

47. 列表、序列和数组

列表一直是计算机的核心部分,因为之前有计算机,在这段时间里,许多恶魔占据了他们的细节。 它们实际上是 Raku 设计中最难的部分之一,但是通过坚持和耐心,Raku 已经使用了一个优雅的系统来处理它们。

47.1. Literal Lists

字面上的列表用逗号和分号不是用圆括号创建,因此:

1, 2        # This is two-element list
(1, 2)      # This is also a List, in parentheses
(1; 2)      # same List
(1)         # This is not a List, just a 1 in parentheses
(1,)        # This is a one-element List

括号可用于标记`列表`的开头和结尾,因此:

(1, 2), (1, 2) # This is a list of two lists.

多维字面上的`列表`是通过逗号和分号组合而成的。 它们可以在常规参数列表和下标中使用。

say so (1,2; 3,4) eqv ((1,2), (3,4));
# OUTPUT«True␤»
say('foo';); # a list with one element and the empty list
# OUTPUT«(foo)()␤»

单个元素可以使用下标从列表中拉出。 列表的第一个元素的索引号为零:

say (1, 2)[0];   # says 1
say (1, 2)[1];   # says 2
say (1, 2)[2];   # says Nil
say (1, 2)[-1];  # Error
say (1, 2)[*-1]; # 2

47.2. The @ sigil

Raku 中名称为 @ 符号的变量应该包含某种类似列表的对象。 当然,其他变量也可能包含这些对象,但是 @-sigiled 变量总是这样,并且期望它们作用于该部分。

默认情况下,当您将`列表`分配给 @-sigiled 变量时,您将创建一个`数组`。 这些在下面描述。 如果你想把一个真实的的 List 放到一个 @ -sigiled 变量中,你可以用 := 绑定代替。

my @a := 1, 2, 3;

将列表的列表赋值给 @-sigiled 变量不提供相同的快捷方式。 在这种情况下,外部 List 成为数组的第一个元素。

my @a = (1,2; 3,4);
say @a.flat;
# OUTPUT«((1 2) (3 4))␤»
@a := (1,2; 3,4);
say @a.flat;
# OUTPUT«((1 2 3 4)␤»

@_sigiled 变量像列表一样的方式之一是总是支持位置下标。 任何绑定到 @-sigiled 值的东西都必须支持 Positional 角色,这保证了:

my @a := 1;  # Type check failed in binding; expected Positional but got Int

# 但是
my @a := 1,; # (1)

47.3. Reset a List Container

要从 Positional 容器中删除所有元素,请将 Empty,空列表 () 或空列表的 Slip 赋值给容器。

my @a = 1, 2, 3;
@a = ();
@a = Empty;
@a = |();

47.4. Iteration

所有的列表都可以被迭代,这意味着从列表中按顺序拿出每个元素并在最后一个元素之后停止:

for 1, 2, 3 { .say } # says 1, then says 2, then says 3

47.5. Testing for Elements

要测试元素将 ListArray 转换为 Set 或使用 Set 运算符

my @a = <foo bar buzz>;
say @a.Set<bar buzz>; # (True True)
say so 'bar' ∈ @a;   # True

47.5.1. Sequences

不是所有的列表生来都充满元素。 有些只创建他们被要求的尽可能多的元素。 这些称为序列,其类型为 Seq。 因为这样发生,循环返回 Seqs

(loop { 42.say })[2] # says 42 three times

所以,在 Raku 中有无限列表是很好的,只要你从不问他们所有的元素。 在某些情况下,你可能希望避免询问它们有多长 - 如果 Raku 知道一个序列是无限的,它将尝试返回 Inf,但它不能总是知道。

虽然 Seq 类确实提供了一些位置下标,但它不提供 Positional 的完整接口,因此 @-sigiled 变量可能不会绑定到 Seq

my @s := (loop { 42.say }); # Error expected Positional but got Seq

这是因为 Seq 在使用它们之后不会保留值。 这是有用的行为,如果你有一个很长的序列,因为你可能想在使用它们之后丢弃值,以便你的程序不会填满内存。 例如,当处理一个百万行的文件时:

for 'filename'.IO.lines -> $line {
    do-something-with($line);
}

你可以确信文件的整个内容不会留在内存中,除非你明确地存储某个地方的行。

另一方面,在某些情况下,您可能希望保留旧值。 可以在`列表`中隐藏一个 Seq,它仍然是惰性的,但会记住旧的值。 这是通过调用 .list 方法完成的。 由于此`列表`完全支持 Positional,因此可以将其直接绑定到 @-sigiled 变量上。

my @s := (loop { 42 }).list;
@s[2]; # Says 42 three times
@s[1]; # does not say anything
@s[4]; # Says 42 two more times

您还可以使用 .cache 方法代替 .list,这取决于您希望处理引用的方式。 有关详细信息,请参阅 Seq 上的页面。

47.5.2. Slips

有时候你想把一个列表的元素插入到另一个列表中。 这可以通过一个称为 Slip 的特殊类型的列表来完成。

say (1, (2, 3), 4) eqv (1, 2, 3, 4);         # says False
say (1, Slip.new(2, 3), 4) eqv (1, 2, 3, 4); # says True
say (1, slip(2, 3), 4) eqv (1, 2, 3, 4);     # also says True

另一种方法是使用 | 前缀运算符。 注意,这有一个比逗号更严格的优先级,所以它只影响一个单一的值,但不像上面的选项,它会打碎标量。而 slip 不会。

say (1, |(2, 3), 4) eqv (1, 2, 3, 4);        # says True
say (1, |$(2, 3), 4) eqv (1, 2, 3, 4);       # also says True
say (1, slip($(2, 3)), 4) eqv (1, 2, 3, 4);  # says False

47.6. Lazy Lists

列表可以是惰性的,这意味着它们的值是根据需要计算的,并且存储供以后使用。 要创建惰性列表,请使用 gather/take序列运算符。 您还可以编写一个实现 Iterable 角色的类,并在调用 lazy 时返回 True。 请注意,某些方法(如 elems)可能会导致整个列表计算失败,如果列表也是无限的。无限列表没办法知道它的元素个数。

my @l = 1,2,4,8...Inf;
say @l[0..16];
# OUTPUT«(1 2 4 8 16 32 64 128 256 512 1024 2048 4096 8192 16384 32768 65536)␤»

47.7. Immutability

到目前为止我们谈论的列表(ListSeqSlip)都是不可变的。 这意味着您不能从中删除元素,或重新绑定现有元素:

(1, 2, 3)[0]:delete; # Error Can not remove elements from a List
(1, 2, 3)[0] := 0;   # Error Cannot use bind operator with this left-hand side
(1, 2, 3)[0] = 0;    # Error Cannot modify an immutable Int

但是,如果任何元素包裹在标量中,您仍然可以更改 Scalar 指向的值:

my $a = 2;
(1, $a, 3)[1] = 42;
$a.say;            # says 42

…​就是说,它只是列表结构本身 - 有多少个元素和每个元素的标识 - 是不可变的。 不变性不是通过元素的身份传染。

47.8. List Contexts

到目前为止,我们主要是在中立语境下处理列表。 实际上列表在语法层面上上下文非常敏感。

47.8.1. List Assignment Context

当一个列表出现在赋值给 @-sigiled 变量的右边时,它被“热切地”计算。 这意味着 Seq 将被迭代,直到它不能产生更多的元素。 这是你不想放置无限列表的地方之一,免得你的程序挂起,最终耗尽内存:

my $i = 3;
my @a = (loop { $i.say; last unless --$i }); # Says 3 2 1
say "take off!";

47.8.2. Flattening "Context"

当您的列表包含子列表,但您只想要一个平面列表时,可以展平该列表以生成一系列值,就像所有的括号被删除了一样。 无论括号中有多少层次嵌套,这都可以工作。

请注意,列表周围的标量将使其免于扁平化:

for (1, (2, $(3, 4)), 5).flat { .say } # says 1, then 2, then (3 4), then 5

…​但是一个 @-sigiled 变量将溢出它的元素。

my @l := 2, (3, 4);
for (1, @l, 5).flat { .say };      # says 1, then 2, then 3, then 4, then 5
my @a = 2, (3, 4);                 # Arrays are special, see below
for (1, @a, 5).flat { .say };      # says 1, then 2, then (3 4), then 5

47.8.3. Argument List (Capture) Context

当列表作为函数或方法调用的参数出现时,会使用特殊的语法规则:该列表立即转换为 CaptureCapture 本身有一个 List(.list)和一个 Hash(.hash)。 任何键没有引号的 Pair,或者没有括号的 Pair 字面量,永远不会变成 .list。 相反,它们被认为是命名参数,并且压缩为 .hash。 有关此处理的详细信息,请参阅 Capture 上的页面。

考虑从`列表`中创建新`数组`的以下方法。 这些方法将 List 放在参数列表上下文中,因此,Array 只包含 1 和 2,但不包含 Pair :c(3),它被忽略。

Array.new(1, 2, :c(3));
Array.new: 1, 2, :c(3);
new Array: 1, 2, :c(3);

相反,这些方法不会将 List 放置在参数列表上下文中,所以所有元素,甚至 Pair :c(3),都放置在`数组`中。

Array.new((1, 2, :c(3)));
(1, 2, :c(3)).Array;
my @a = 1, 2, :c(3); Array.new(@a);
my @a = 1, 2, :c(3); Array.new: @a;
my @a = 1, 2, :c(3); new Array: @a;

在参数列表上下文中,应用于 Positional 上的 | 前缀运算符总是将列表元素slip为Capture的位置参数,而应用到 Associative 上的 | 前缀运算符会把 pairs 作为具名参数 slip 进来:

my @a := 2, "c" => 3;
Array.new(1, |@a, 4);    # Array contains 1, 2, :c(3), 4
my %a = "c" => 3;
Array.new(1, |%a, 4);    # Array contains 1, 4

47.8.4. Slice Indexing Context

切片下标 中的 List 角度来看,只有一个显着的地方在于它是不可见的:因为一个切片的副词附在 ] 后面,切片的内部不是参数列表,并且没有对 pair 形式的特殊处理 。

大多数 Positional 类型将对切片索引的每个元素强制执行整数强制,因此那儿出现的 pairs 将生成错误,无论如何:

(1, 2, 3)[1, 2, :c(3)] # Method 'Int' not found for invocant of class 'Pair'

…​但是这完全取决于类型 - 如果它定义了pairs的顺序,它可以考虑 :c(3) 是有效的索引。

切片内的索引通常不会自动展平,但是子列表通常不会强制为 Int。 相反,列表结构保持不变,从而导致在结果中重复结构的嵌套 slice 操作:

say ("a", "b", "c")[(1, 2), (0, 1)] eqv (("b", "c"), ("a", "b")) # says True

47.8.5. Range as Slice

Range 是用于下边界和上边界的容器。 生成具有 Range 的切片将包括这些边界之间的任何索引,包括边界。 对于无限上限,我们同意数学家 Inf 等于 Inf-1

my @a = 1..5;
say @a[0..2];     # (1 2 3)
say @a[0..^2];    # (1 2)
say @a[0..*];     # (1 2 3 4 5)
say @a[0..^*];    # (1 2 3 4 5)
say @a[0..Inf-1]; # (1 2 3 4 5)

47.8.6. Array Constructor Context

在数组字面量中,初始化值的列表不在捕获上下文中,只是一个正常的列表。 然而,正如在赋值中一样,急切地对它求值。

[ 1, 2, :c(3) ] eqv Array.new((1, 2, :c(3))) # says True
[while $++ < 2 { 42.say; 43 }].map: *.say;   # says 42 twice then 43 twice
(while $++ < 2 { 42.say; 43 }).map: *.say;   # says "42" then "43"
                                             # then "42" then "43"

它把我们带到数组这儿来。

47.9. Arrays

数组与列表在三个主要方面不同:它们的元素可以被类型化,它们自动列出它们的元素,并且它们是可变的。 否则,它们是列表,并且在列表所在的位置被接受。

say Array ~~ List     # says True

第四种更微妙的方式是,当使用数组时,有时可能更难以维持惰性或使用无限序列。

47.9.1. Typing

数组可以被类型化,使得它们的槽在被赋值时执行类型检查。 只允许分配 Int 值的数组是 Array[Int] 类型,可以使用 Array[Int].new 创建一个数组。 如果你打算仅仅为了这个目的使用 @-sigiled 变量,你可以在声明它时通过指定元素的类型来改变它的类型:

my Int @a = 1, 2, 3;              # An Array that contains only Ints
my @b := Array[Int].new(1, 2, 3); # Same thing, but the variable is not typed
say @b eqv @a;                    # says True.
my @c = 1, 2, 3;                  # An Array that can contain anything
say @b eqv @c;                    # says False because types do not match
say @c eqv (1, 2, 3);             # says False because one is a List
say @b eq @c;                     # says True, because eq only checks values
say @b eq (1, 2, 3);              # says True, because eq only checks values

@a[0] = 42;                       # fine
@a[0] = "foo";                    # error: Type check failed in assignment

在上面的例子中,我们将一个类型化的 Array 对象绑定到一个没有指定类型的 @-sigil 变量上。 另一种方法不工作 - 你不能绑定一个类型错误的数组到一个类型化的 @-sigiled 变量上:

my @a := Array[Int].new(1, 2, 3);     # fine
@a := Array[Str].new("a", "b");       # fine, can be re-bound
my Int @b := Array[Int].new(1, 2, 3); # fine
@b := Array.new(1, 2, 3);             # error: Type check failed in binding

当使用类型化数组时,重要的是要记住它们是名义类型的。 这意味着数组的声明类型是重要的。 给定以下子声明:

sub mean(Int @a) {
    @a.sum / @a.elems
}

传递 Array[Int] 的调用将成功:

my Int @b = 1, 3, 5;
say mean(@b);                       # @b is Array[Int]
say mean(Array[Int].new(1, 3, 5));  # Anonymous Array[Int]
say mean(my Int @ = 1, 3, 5);       # Another anonymous Array[Int]

但是,由于传递一个无类型的数组,下面的调用将全部失败,即使该数组在传递时恰好包含 Int 值:

my @c = 1, 3, 5;
say mean(@c);                       # Fails, passing untyped Array
say mean([1, 3, 5]);                # Same
say mean(Array.new(1, 3, 5));       # Same again

请注意,在任何给定的编译器中,可能有一些奇怪的,底层的方法来绕过数组上的类型检查,因此在处理不受信任的输入时,执行额外的类型检查是一个很好的做法,

for @a -> Int $i { $_++.say };

然而,只要你坚持在一个信任的代码区域内的正常赋值操作,这不会是一个问题,并且typecheck错误将在分配到数组时发生,如果他们不能在编译时捕获。 在Raku中提供的用于操作列表的核心功能不应该产生一个类型化的数组。

不存在的元素(当索引时)或已分配Nil的元素将采用默认值。 可以使用 is default 特征在逐个变量的基础上调整此默认值。 请注意,无类型的@ -sigiled变量的元素类型为 Mu,但其默认值为未定义的 Any

my @a;
@a.of.perl.say;                 # says "Mu"
@a.default.perl.say;            # says "Any"
@a[0].say;                      # says "(Any)"
my Numeric @n is default(Real);
@n.of.perl.say;                 # says "Numeric"
@n.default.perl.say;            # says "Real"
@n[0].say;                      # says "(Real)"

47.9.2. Fixed Size Arrays

要限制`阵列`的尺寸,请提供由 ,; 在数组容器的名称后面的括号中。 这样一个`数组`的值将默认为 Any。 形状可以在运行时通过 shape 方法访问。

my @a[2,2];
dd @a;
# OUTPUT«Array.new(:shape(2, 2), [Any, Any], [Any, Any])␤»
say @a.shape;
# OUTPUT«(2 2)␤»

赋值到固定大小的数组将把一个列表的列表提升为数组的数组。

my @a[2;2] = (1,2; 3,4);
@a[1;1] = 42;
dd @a;
# OUTPUT«Array.new(:shape(2, 2), [1, 2], [3, 42])␤»

47.9.3. Itemization

对于大多数用途,数组由多个槽组成,每个槽包含正确类型的`标量`。 每个这样的`标量`,反过来,包含该类型的值。 当数组被初始化,赋值或构造时,Raku 将自动进行类型检查值并创建标量来包含它们。

这实际上是 Raku 列表处理中最棘手的部分之一,以获得牢固的理解。

首先,请注意,因为假设数组中的项目化,它本质上意味着 $(…​) 被放置在您分配给数组的所有内容,如果你不把它们放在那里。 另一方面,Array.perl 不会将$显式地显示标量,与 List.perl 不同:

((1, 2), $(3, 4)).perl.say; # says "((1, 2), $(3, 4))"
[(1, 2), $(3, 4)].perl.say; # says "[(1, 2), (3, 4)]"
                            # ...but actually means: "[$(1, 2), $(3, 4)]"

它决定所有这些额外的美元符号和括号更多的眼睛疼痛比对用户的好处。 基本上,当你看到一个方括号,记住隐形美元符号。

第二,记住这些看不见的美元符号也防止扁平化,所以你不能真正地扁平化一个数组内的元素与正常调用 flat.flat

((1, 2), $(3, 4)).flat.perl.say; # (1, 2, $(3, 4)).Seq
[(1, 2), $(3, 4)].flat.perl.say; # ($(1, 2), $(3, 4)).Seq

由于方括号本身不会防止展平,因此您仍然可以使用平面将数组中的元素溢出到周围的列表中。

(0, [(1, 2), $(3, 4)], 5).flat.perl.say; # (0, $(1, 2), $(3, 4), 5).Seq

…​元素本身,但是,留在一块。

这可以阻止用户提供的数据,如果你有深嵌套的数组他们想要平面数据。 目前,他们必须手动地深度地映射结构以撤消嵌套:

say gather [0, [(1, 2), [3, 4]], $(5, 6)].deepmap: *.take; # (1 2 3 4 5 6)

…​未来版本的 Raku 可能会找到一种使这更容易的方法。 但是,当 non-itemized 列表足够时,不从函数返回数组或 itemized 列表,这是一个应该考虑作为好意给他们的用户:

  • 当您总是想要与周围列表合并时使用 Slips

  • 使用 non-itemized 列表,当你想让用户容易展平时

  • 使用 itemized 列表来保护用户可能不想展平的东西

  • 使用数组作为 non-itemized 列表的 non-itemized 列表,如果合适

  • 如果用户想要改变结果而不首先复制结果,请使用数组。

事实上,数组的所有元素(在`Scalar`容器中)是一个绅士的协议,而不是一个普遍强制的规则,并且在类型数组中的类型检查不太好。 请参阅下面有关绑定到阵列插槽的部分。

47.9.4. Literal Arrays

字面数组是用方括号内的 List 构造的。 列表被热切地迭代(如果可能,在编译时),并且列表中的值每个都进行类型检查和itemized。 在展平时, 方括号本身会将元素放入周围的列表中,但是元素本身不会因为 itemization 化而溢出。

47.9.5. Mutability

与列表不同,数组是可变的。 元素可以删除,添加或更改。

my @a = "a", "b", "c";
@a.say;                  # [a b c]
@a.pop.say;              # says "c"
@a.say;                  # says "[a b]"
@a.push("d");
@a.say;                  # says "[a b d]"
@a[1, 3] = "c", "c";
@a.say;                  # says "[a c d c]"

47.9.6. Assigning

列表到数组的分配是急切的。 该列表将被完全求值,并且数组不应该是无限的否则程序可能挂起。 类似地,对阵列的分片的分配是急切的,但是仅仅达到所请求数量的元素,其可以是有限的:

my @a;
@a[0, 1, 2] = (loop { 42 });
@a.say;                     # says "[42 42 42]"

在赋值期间,每个值都将进行类型检查,以确保它是 Array 允许的类型。 任何`标量`将从每个值中剥离,一个新的`标量`将被包裹。

47.9.7. Binding

单个数组槽可以以相同的方式绑定 $-sigiled 变量:

my $b = "foo";
my @a = 1, 2, 3;
@a[2] := $b;
@a.say;          # says '[1 2 "foo"]'
$b = "bar";
@a.say;          # says '[1 2 "bar"]'

…​但强烈不建议将 Array 槽直接绑定到值。 如果你这样做,预期内置函数的惊喜。 只有当需要知道值和Scalar-Wrapped值之间的差异的可变容器时,或者对于不能使用本地类型数组的非常大的Arrays,才需要执行此操作。 这样的生物永远不应该被传递回不知情的用户。

48. Raku 中的函数

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

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

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

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

48.1. 定义/创建/使用函数

48.1.1. 子例程

创建子例程的基本方法是使用 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

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

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

或者甚至:

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

48.1.2. Block 和 Lambda

每当你看到类似

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

或这样的句子时:

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

那么这就是语法。-> 也被认为是块的一部分。if, for, while 等语句后面都有这类块。

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

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

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

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

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

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

48.1.3. 签名

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

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

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

48.1.4. 自动签名

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

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

48.1.5. 参数

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

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.

48.1.6. 返回值

任何块或例程将把它的最后一个表达式作为返回值提供给调用者。如果 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")␤»

48.1.7. 返回类型约束

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

48.1.8. 多重分派

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

48.1.9. 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

48.2. 约定和惯用法

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

48.2.1. 吞噬约定

也许最重要的是处理 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

48.3. 函数是一等对象

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

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

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

48.3.1. 中缀形式

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

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

48.3.2. 闭包

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 一样易于使用和强大。

48.3.3. 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␤»

48.3.4. 定义操作符

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

这既适用于向现有运算符添加多个候选项, 也适用于定义新的运算符。在后一种情况下, 新子例程的定义自动将新运算符安装到 语法(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␤»

48.3.5. 优先级

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)

48.3.6. 结合性

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

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)

48.3.7. 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

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

48.3.8. 重新分派

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

幸运的是, 我们有一系列的重新分派工具, 可以帮助我们轻松完成。

例程 callsame

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
例程 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-many( Associative $a ) {
    say "Associative $a ";
    my $calling = callwith( 1 => $a );
    return $calling;
}

multi how-many( Pair $a ) {
    say "Pair $a ";
    return "There is $a "

}

multi how-many( Hash $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 )); 的输出可以看出。

例程 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
例程 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

samewith 用用户提供的参数再次调用当前候选函数, 并返回当前候选函数新实例的返回值。

proto a(|) { * }

multi a( Int $x ) {
    return 1 unless $x > 1;
    return $x * samewith( $x - 1 );
}

say (a 10);

#  output:
# 3628800
例程 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, 因为它会试图对 Promise 的块进行 nextsame, 而不是我们原来的例程。

48.3.9. 包裹后的例程

除了上面已经提到的那些, 重新分派在更多的情况下也有帮助。例如, 对于分派到 wrap 后的例程:

# 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);     # OUTPUT: «2␤»
say square-root(-4);    # OUTPUT: «0+2i␤»

48.3.10. 父类的例程

另一个用例是对来自父类的方法进行重新分派。

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

48.3.11. 强制类型

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

如果参数不能转换为更严格的类型, 就会产生一个类型检查错误。

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, 则可以将强制 Int(Any) 简略化, 省略 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 $x, Int $y --> Bool(Int) ) { $x - $y };

for (2,4) X (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 循环中打印出来(放到一个字符串上下文中)。

48.4. 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;
}

48.5. 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
}

49. 编译指令

在 Raku 中,pragma 是用于识别要使用的 Raku 的特定版本或以某种方式修改编译器的正常行为的指令。use 关键字开启编译指示(类似于你怎么 use 一个模块)。要禁用 pragma,请使用 no 关键字:

use v6.c;   # use 6.c language version
no worries; # don't issue compile time warnings

以下是一个编译指令列表,其中包含每个编译指令意图的简短描述或指向其使用的更多详细信息的链接。(注意:标记为“[NYI]”的编译指令尚未实现,标记为“[TBD]”的编号将在稍后定义。)

49.1. v6.x

该编译指令 声明了将要使用的编译器的版本,如果它们是可选的,则开启它的功能。

use v6;   # Load latest supported version (non-PREVIEW).
          # Also, useful for producing better errors when accidentally
          # executing the program with `perl` instead of `raku`
use v6.c;         # Use the "Christmas" version of Raku
use v6.d;         # Use the "Diwali" version of Raku
use v6.d.PREVIEW; # On 6.d-capable compilers, enables 6.d features,
                  # otherwise enables the available experimental
                  # preview features for 6.d language
                  # This will only work on releases previous to 6.d.

由于这些编译指令是在编译器版本上开启的,所以它们应该是文件中的第一个语句(前面的注释和 Pod 都没问题)。

49.2. MONKEY-GUTS

该编译指令目前不是任何 Raku 规范的一部分,但作为 use nqp 的同义词存在于 Rakudo 中(见下文)。

49.3. MONKEY-SEE-NO-EVAL

49.4. MONKEY-TYPING

49.5. MONKEY

use MONKEY;

打开所有可用的 MONKEY 编译指令,目前有上面的三个; 因此,它等同于:

use MONKEY-TYPING;
use MONKEY-SEE-NO-EVAL;
use MONKEY-GUTS;

49.6. experimental

允许使用实验性功能

49.7. fatal

一个词法编译指令,使得Failures从例程致命错误中返回。例如,Str上的 + 前缀将其强制转换为Numeric,但如果字符串包含非数字字符,则返回Failure。在变量中保存该Failure可以防止它被下沉,因此下面的第一个代码块到达 say $x.^name; 行并在输出中打印 Failure

在第二个块中,use fatal 编译指定开启了,因此 say 永远不会到达该行,因为从前缀 + 返回的 Failure 中包含的 Exception 被抛出并且 CATCH 块被运行,打印出 Caught…​ 行。请注意,这两个块都是相同的程序,use fatal 只会影响它所使用的词法块:Caught…​use fatal

{
    my $x = +"a";
    say $x.^name;
    CATCH { default { say "Caught {.^name}" } }
} # OUTPUT: «Failure»

{
    use fatal;
    my $x = +"a";
    say $x.^name;
    CATCH { default { say "Caught {.^name}" } }
} # OUTPUT: «Caught X::Str::Numeric»

try 块内部,默认开启 fatal 编译指令,您可以使用 no fatal *禁用*它:

try {
    my $x = +"a";
    say $x.^name;
    CATCH { default { say "Caught {.^name}" } }
} # OUTPUT: «Caught X::Str::Numeric»

try {
    no fatal;
    my $x = +"a";
    say $x.^name;
    CATCH { default { say "Caught {.^name}" } }
} # OUTPUT: «Failure»

49.8. internals

invocant

isms

[2018.09 and later]

允许被认为是正常 Raku 编程中的警告和/或错误的陷阱的一些其他语言结构。目前,Perl5C++ 是被允许的。

sub abs() { say "foo" }
abs;
# Unsupported use of bare "abs"; in Raku please use .abs if you meant
# to call it as a method on $_, or use an explicit invocant or argument,
# or use &abs to refer to the function as a noun

在这种情况下,提供一个不带任何参数的 abs sub,并没有使编译错误消失。

use isms <Perl5>;
sub abs() { say "foo" }
abs;   # foo

有了这个,编译器将允许违规的 Perl 5 构造,允许实际执行代码。

如果未指定任何语言,则允许使用所有已知语言结构。

use isms;   # allow for Perl5 and C++ isms

49.9. lib

该编译指令将子目录添加到库搜索路径,以便解释器可以找到模块

use lib <lib /opt/lib /usr/local/lib>;

这将搜索列表中传递的目录。有关更多示例,请查看模块文档

49.10. newline

在调用的作用域内设置$?NL常量的值。可能的值有 :lf(默认值,表示换行),:crlf(表示回车,换行)和 :cr(表示回车)。

49.11. nqp

使用风险由您自己承担。

这是一个 Rakudo 特有的编译指令。有了它,Rakudo 可以访问顶级命名空间中的nqp操作码

use nqp;
nqp::say("hello world");

这使用底层的 nqp say 操作码而不是 Raku 例程。这个编译指示可能会使您的代码依赖于特定版本的 nqp,并且由于该代码不是 Raku 规范的一部分,因此不能保证它是稳定的。您可能会在 Rakudo 核心中找到大量用法,这些用法用于尽可能快地实现核心功能。Rakudo 代码生成的未来优化可能会废弃这些用法。

49.12. parameters

precompilation

默认允许预编译源代码,特别是在模块中使用时。如果由于某种原因您不希望预编译(模块的)代码,您可以使用 no precompilation。这将阻止整个编译单元(通常是文件)被预编译。

49.13. soft

49.14. strict

strict 是默认行为,并要求您在使用变量之前声明变量。你可以用 no 放松这个限制。

no strict; $x = 42; # OK

49.15. trace

use trace 被激活时,执行的代码的任何行将被写入 stderr。您可以使用 no trace 关闭该功能,因此这仅适用于某些代码段。

49.16. v6

49.17. variables

49.18. worries

词法地控制是否显示编译器生成的编译时警告。默认情况下启用。

$ raku -e 'say :foo<>.Pair'
Potential difficulties:
  Pair with <> really means an empty list, not null string; use :foo('') to represent the null string,
    or :foo() to represent the empty list more accurately
  at -e:1
  ------> say :foo<>⏏.Pair
foo => Nil

$ raku -e 'no worries; say :foo<>.Pair'
foo => Nil

50. 上下文和上下文化

在许多情况下,需要上下文来解释容器的值。在 Raku 中,我们将使用 context 将容器的值强制转换为某种类型或类,或者决定如何处理它,就像接收器(sink)上下文的情况一样。

50.1. Sink 上下文

Sink 相当于 void 上下文,也就是说,我们抛出(在接收器下面)操作的结果或块的返回值的上下文。通常,当语句不知道如何处理该值时,将在警告和错误中调用此上下文。

my $sub = -> $a { return $a² };
$sub; # OUTPUT: «WARNINGS:
Useless use of $sub in sink context (line 1)»

您可以使用 sink-all 方法在 Iterator 上强制使用该接收器上下文。Proc也可以通过 sink 方法沉没,迫使它们引发异常而不返回任何东西。

通常,如果在 sink 上下文中进行计算,则块将发出警告; 但是,在 sink 上下文中 gather/take 块是显式计算的,并使用 take 显式返回值。

在 sink 上下文中,对象将调用其 sink 方法(如果存在):

sub foo {
    return [<a b c>] does role {
        method sink { say "sink called" }
    }
}
foo
# OUTPUT: sink called

50.2. Number 上下文

这个上下文,可能除了上面的所有内容之外,都是*转换*或*解释*上下文,因为它们接收无类型或类型化的变量,并将其类型化为执行操作所需的任何内容。在某些情况下,这意味着转换(例如从 StrNumeric); 在其他情况下只是一种解释(IntStr 将被解释为 IntStr)。

每当我们需要对变量应用数值运算时,就会调用*数字上下文*。

my $not-a-string="1                 ";
my $neither-a-string="3                        ";
say $not-a-string+$neither-a-string; # OUTPUT: «4»

在上面的代码中,只要只有几个数字而没有其他字符,字符串将在数字上下文中解释。但是,它可以具有任意数量的前导或尾随空格。

可以使用算术运算符强制数字上下文,例如 +-。在该上下文中,Numeric 将调用该方法(如果可用),并将返回的值用作对象的数值。

my $t = True;
my $f = False;
say $t+$f;      # OUTPUT: «1»
say $t.Numeric; # OUTPUT: «1»
say $f.Numeric; # OUTPUT: «0»
my $list= <a b c>;
say True+$list; # OUTPUT: «4»

在*列表*那样的东西的情况下,数值通常等于 .elems; 在某些情况下,像Thread 一样,它将返回唯一的线程标识符。

50.3. String 上下文

在*字符串上下文中*,值可以作为字符串进行操作。例如,此上下文用于强制非字符串值,以便可以将它们打印到标准输出。

put $very-complicated-and-hairy-object; # OUTPUT: something meaningful

或者当智能匹配正则表达式时:

put 333444777 ~~ /(3+)/; # OUTPUT: «「333」 0 => 「333」»

通常,将在变量上调用 Str 例程以将其上下文化; 因为这个方法是从 Mu 继承的,所以它始终存在,但并不总能保证工作。在某些核心类中,它会发出警告。

是(一元)字符串上下文化器。作为运算符,它连接字符串,但作为前缀运算符,它成为字符串上下文运算符。

my @array = [ [1,2,3], [4,5,6]];
say ~@array; # OUTPUT: «1 2 3 4 5 6»

~ 应用于列表时,这也将在 [reduction] 上下文中发生:

say [~] [ 3, 5+6i, Set(<a b c>), [1,2,3] ]; # OUTPUT: «35+6ic a b1 2 3»

在那个情况下, 空列表或其它容器会字符串化为一个空字符串:

say [~] [] ; # OUTPUT: «␤»

由于 ~ 也作为缓冲区连接运算符,因此必须检查每个元素是否为空,因为字符串上下文中的单个空缓冲区将表现为字符串,从而产生错误。

say [~] Buf.new(0x3,0x33), Buf.new(0x2,0x22);
# OUTPUT: «Buf:0x<03 33 02 22>»

然而,

my $non-empty = Buf.new(0x3, 0x33);
my $empty = [];
my $non-empty-also = Buf.new(0x2,0x22);
say [~] $non-empty, $empty, $non-empty-also;
# OUTPUT: «Cannot use a Buf as a string, but you called the Stringy method on it

由于 ~ 将字符串上下文放入此列表的第二个元素,~ 将使用适用于字符串的第二个形式,从而产生所显示的错误。只需确保连接的所有内容都是缓冲区即可避免此问题。

my $non-empty = Buf.new(0x3, 0x33);
my $empty = Buf.new();
my $non-empty-also = Buf.new(0x2,0x22);
say [~] $non-empty, $empty, $non-empty-also; # OUTPUT: «Buf:0x<03 33 02 22>»

通常,上下文会通过调用 contextualizer 将变量强制转换为特定类型; 在 mixins 的情况下,如果混合了上下文类,它将以这种方式运行。

my $described-number = 1i but 'Unity in complex plane';
put $described-number; # OUTPUT: «Unity in complex plane»

but 创建一个 mixin,它使用 Str 方法赋予复数。put 将它 Str 上下文化为一个字符串,即它调用字符串上下文,使用上面显示的结果。

51. 枚举

在 Raku 中,枚举(enum)类型比其他语言复杂得多,详细信息可在此处的类型描述中找到。

这个简短的文档将给出一个简单的使用示例,就像在 C 语言中一样。

假设我们有一个需要写入各种目录的程序; 我们想要一个函数,给定一个目录名,测试它(1)是否存在(2)它是否可以被该程序的用户写入; 这意味着从用户的角度来看有三种可能的状态:要么你可以写(CanWrite),要么没有目录(NoDir)或者目录存在,但你不能写(NoWrite)。 测试结果将决定程序接下来要采取的操作。

enum DirStat <CanWrite NoDir NoWrite>;
sub check-dir-status($dir --> DirStat) {
    if $dir.IO.d {
        # dir exists, can the program user write to it?
        my $f = "$dir/.tmp";
        spurt $f, "some text";
        CATCH {
            # unable to write for some reason
            return NoWrite;
        }
        # if we get here we must have successfully written to the dir
        unlink $f;
        return CanWrite;
    }
    # if we get here the dir must not exist
    return NoDir;
}

# test each of three directories by a non-root user
my $dirs =
    '/tmp',  # normally writable by any user
    '/',     # writable only by root
    '~/tmp'; # a non-existent dir in the user's home dir
for $dirs -> $dir {
    my $stat = check-dir-status $dir;
    say "status of dir '$dir': $stat";
    if $stat ~~ CanWrite {
        say "  user can write to dir: $dir";
    }
}
# output
#   status of dir '/tmp': CanWrite
#     user can write to dir: /tmp
#   status of dir '/': NoWrite
#   status of dir '~/tmp': NoDir

52. 控制语句

52.1. 语句

Raku 程序由一个或多个语句组成。简单语句由分号分隔。以下程序将打印 “Hello”, 然后在下一行打印“World”。

say "Hello";
say "World";

在语句中出现空白的大多数地方, 且在分号之前, 语句可能会分成许多行。此外, 多个语句可能出现在同一行。这会很尴尬, 但上面的内容也可以写成:

say
"Hello"; say "World";

52.2. 块儿

与许多语言一样, Raku 使用 {}blocks 括起来以将多个语句转换为单个语句。可以省略块中最后一个语句和闭合 } 之间的分号。

{ say "Hello"; say "World" }

当块单独作为一个语句存在时, 它将在前一个语句完成后立即进入, 并且其中的语句将被执行。

say 1;                    # OUTPUT: «1»
{ say 2; say 3 };         # OUTPUT: «23»
say 4;                    # OUTPUT: «4»

除非它作为一个语句单独存在, 否则一个块只会创建一个闭包。内部的语句不会立即执行。闭包是另一个主题, 如何使用它们在别处有解释。现在, 了解块何时运行以及何时不运行是非常重要的:

say "We get here"; { say "then here." }; { say "not here"; 0; } or die;

在上面的示例中, 在运行第一个语句之后, 第一个块独立作为第二个语句, 因此我们运行里面的语句。第二个块不是单独作为一个语句, 所以相反, 它创建了一个 Block 类型的对象, 但不运行它。对象实例通常被认为是 true, 因此代码不会死掉, 即使该块被计算为 0, 它是否被执行。该示例没有说明如何处理`Block`对象, 因此它会被丢弃。

下面介绍的大多数流控制结构只是告诉 Raku 何时, 如何以及多少次进入像第二个块那样的块。

在我们深入这些之前, 关于语法的一个重要的注意事项: 如果在通常放置分号的结束大括号之后的行上没有任何内容(或者只有注释), 则不需要分号:

# All three of these lines can appear as a group, as is, in a program
{ 42.say }                # OUTPUT: «42»
{ 43.say }                # OUTPUT: «43»
{ 42.say }; { 43.say }    # OUTPUT: «42 43»

…​但是:

{ 42.say }  { 43.say }    # Syntax error
{ 42.say; } { 43.say }    # Also a syntax error, of course

因此, 在换行编辑器中退格时要小心:

{ "Without semicolons line-wrapping can be a bit treacherous.".say } \
{ 43.say } # Syntax error

无论如何, 在大多数语言中你必须注意这一点, 以防止代码意外被注释掉。为清楚起见, 下面的许多示例可能包含不必要的分号。

对于任何顶级表达式, 类主体的行为类似于简单的块; 这同样适用于角色和其他包, 如语法(实际上是类)或模块。

class C {
    say "I live";
    die "I will never live!"
};
my $c = C.new;                              │
# OUTPUT: Fails and writes «I live␤II will never live!␤I

该块首先运行第一个语句, 然后 die 打印第二个语句。$c 永远不会得到值。

52.3. Phasers

块可能有 phasers: 即将他们的执行分解成特别阶段运行阶段的特殊标记块。有关详细信息, 请参阅页面 phasers

52.4. do

块不能是独立的语句, 运行这样一个块的最简单方法是在它前面写上一个 do:

# This dies half of the time
do { say "Heads I win, tails I die."; Bool.pick } or die; say "I win.";

请注意, 您需要在 do 和块之间留一个空格。

整个 do {…​} 计算为块儿的最终值。当需要该值时, 将运行该块以计算表达式的剩余部分。所以:

False and do { 42.say };

…​不会打印 42。但是, 每次计算包含它的表达式时, 只会计算一次:

# This says "(..1 ..2 ..3)" not "(..1 ...2 ....3)"
my $f = "."; say do { $f ~= "." } X~ 1, 2, 3;

换句话说, 它遵循与其他所有东西相同的具体规则。

从技术上讲, do 是一个只运行一次迭代的循环。

do 也可以用在一个裸语句上(没有花括号)但这主要是为了避免需要用圆括号扩住语句的语法, 如果它是表达式中的最后一个:

3, do if 1 { 2 }  ; # OUTPUT: «(3, 2)»
3,   (if 1 { 2 }) ; # OUTPUT: «(3, 2)»
3,    if 1 { 2 }  ; # Syntax error

52.5. start

异步运行块的最简单方法是在它之前写上一个 start:

start { sleep 1; say "done" }
say "working";
# working, done

请注意, 您需要在 start 和块儿之间留一个空格。在上面的示例中, start 块处于 sink 上下文中, 因为它未赋值给变量。从版本 6.d 开始, 这种块儿附加了一个异常处理程序:

start { die "We're dead"; }
say "working";
sleep 10;

此代码将在版本 6.d 中打印 Unhandled exception in code scheduled on thread 4 We’re dead, 而在版本 6.c 中等待 10 秒后它将立即退出。

如果你对块儿的结果不感兴趣, start {…​} 会立即返回一个可被安全忽略的 Promise。如果你对块儿的最终值兴趣, 你可以调用返回的 promise 上的 .result 方法。所以:

my $promise = start { sleep 10; 42 }
# ... do other stuff
say "The result is $promise.result()";

如果块内的代码尚未完成, 则 .result 调用将等待直到完成。

start 也可用于裸语句(不带花括号)。这主要用于, 当在对象上调用子例程/方法是异步执行的唯一事情时。

52.6. if

要有条件地运行代码块, 请使用 if 后跟条件。条件, 表达式, 将在 if 完成之前的语句之后立即进行计算。只有在条件被强转为 Bool 为真时, 才会计算附加到条件的块。与某些语言不同, 条件不必用圆括号括起来, 而块周围的 {} 是必需的:

if 1 { "1 is true".say }  ; # says "1 is true"
if 1   "1 is true".say    ; # syntax error, missing block
if 0 { "0 is true".say }  ; # does not say anything, because 0 is false
if 42.say and 0 { 43.say }; # says "42" but does not say "43"

还有一种“语句修饰符”的形式的 if。在这种情况下, if 和 then 条件在您想要有条件地运行的代码之后。请注意, 仍然始终首先计算条件:

43.say if 42.say and 0;     # says "42" but does not say "43"
43.say if 42.say and 1;     # says "42" and then says "43"
say "It is easier to read code when 'if's are kept on left of screen"
    if True;                # says the above, because it is true
{ 43.say } if True;         # says "43" as well

语句修饰符形式最好谨慎使用。

if 语句本身要么 slip 我们一个空列表, 如果它不运行块, 否则就会返回该块产生的值:

my $d = 0; say (1, (if 0 { $d += 42; 2; }), 3, $d); # says "(1 3 0)"
my $c = 0; say (1, (if 1 { $c += 42; 2; }), 3, $c); # says "(1 2 3 42)"
say (1, (if 1 { 2, 2 }), 3);         # does not slip, says "(1 (2 2) 3)"

对于语句修饰符, 是一样的, 除非你有语句的值而不是块:

say (1, (42 if True) , 2); # says "(1 42 2)"
say (1, (42 if False), 2); # says "(1 2)"
say (1,  42 if False , 2); # says "(1 42)" because "if False, 2" is true

if 默认不改变主题变量($_)。为了访问条件表达式生成的值, 您必须更强烈地要求它:

$_ = 1; if 42 { $_.say }                ; # says "1"
$_ = 1; if 42 -> $_ { $_.say }          ; # says "42"
$_ = 1; if 42 -> $a { $_.say;  $a.say } ; # says "1" then says "42"
$_ = 1; if 42       { $_.say; $^a.say } ; # says "1" then says "42"

52.6.1. else/elsif

组合条件可以通过用 else 跟在 if 条件后面来产生, 以提供一个备选块, 当条件表达式为假来运行:

if 0 { say "no" } else { say "yes" }   ; # says "yes"
if 0 { say "no" } else{ say "yes" }    ; # says "yes", space is not required

else 不能用分号将条件语句分开, 但作为一个特例, 换行符是可行的。

if 0 { say "no" }; else { say "yes" }  ; # syntax error
if 0 { say "no" }
else { say "yes" }                     ; # says "yes"

使用 elsif, 额外的条件可以被夹在 ifelse 之间。只有在前面的所有条件都为假的情况下才会计算额外条件, 并且只运行第一个真实条件旁边的块。如果你愿意, 你可以以一个 elsif 而不是一个 else 结束。

if 0 { say "no" } elsif False { say "NO" } else { say "yes" } # says "yes"
if 0 { say "no" } elsif True { say "YES" } else { say "yes" } # says "YES"

if 0 { say "no" } elsif False { say "NO" } # does not say anything

sub right { "Right!".say; True }
sub wrong { "Wrong!".say; False }
if wrong() { say "no" } elsif right() { say "yes" } else { say "maybe" }
# The above says "Wrong!" then says "Right!" then says "yes"

您不能将语句修饰符形式用于 elseelsif:

42.say if 0 else { 43.say }            # syntax error

对于分号和换行, 所有相同的规则都适用, 始终如一。

if 0 { say 0 }; elsif 1 { say 1 }  else { say "how?" } ; # syntax error
if 0 { say 0 }  elsif 1 { say 1 }; else { say "how?" } ; # syntax error
if 0 { say 0 }  elsif 1 { say 1 }  else { say "how?" } ; # says "1"
if 0 { say 0 } elsif 1 { say 1 }
else { say "how?" }                                    ; # says "1"

if 0 { say 0 }
elsif 1 { say 1 } else { say "how?" }                  ; # says "1"

if        0 { say "no" }
elsif False { say "NO" }
else        { say "yes" }                              ; # says "yes"

整个东西要么slips我们一个空列表(如果没有运行块)或者返回由运行的块产生的值:

my $d = 0; say (1,
                (if 0 { $d += 42; "two"; } elsif False { $d += 43; 2; }),
                3, $d); # says "(1 3 0)"
my $c = 0; say (1,
                (if 0 { $c += 42; "two"; } else { $c += 43; 2; }),
                3, $c); # says "(1 2 3 43)"

可以在 else 中获取前一个表达式的值, 它可以来自 if 或者最后一个 elsif, 如果存在的话:

$_ = 1; if 0     { } else -> $a { "$_ $a".say } ; # says "1 0"
$_ = 1; if False { } else -> $a { "$_ $a".say } ; # says "1 False"

if False { } elsif 0 { } else -> $a { $a.say }  ; # says "0"

52.6.2. unless

当你厌倦了输入 “if not (X)” 时, 你可能会用 unless 来反转条件语句的意义。你不能使用把 elseelsifunless 用在一起。因为那最终会让人感到困惑。除了这两个不同, unless 的工作方式和 if 相同:

unless 1 { "1 is false".say }  ; # does not say anything, since 1 is true
unless 1   "1 is false".say    ; # syntax error, missing block
unless 0 { "0 is false".say }  ; # says "0 is false"
unless 42.say and 1 { 43.say } ; # says "42" but does not say "43"
43.say unless 42.say and 0;      # says "42" and then says "43"
43.say unless 42.say and 1;      # says "42" but does not say "43"

$_ = 1; unless 0 { $_.say }           ; # says "1"
$_ = 1; unless 0 -> $_ { $_.say }     ; # says "0"
$_ = 1; unless False -> $a { $a.say } ; # says "False"

my $c = 0; say (1, (unless 0 { $c += 42; 2; }), 3, $c); # says "(1 2 3 42)"
my $d = 0; say (1, (unless 1 { $d += 42; 2; }), 3, $d); # says "(1 3 0)"

52.6.3. with, orwith, without

with 语句像 if 一样, 但它测试 definedness 而不是真假。此外, 它在条件上主题化, 很像 given:

with "abc".index("a") { .say }      # prints 0

代替 elsif, orwith 可用于链定义性测试:

# The below code says "Found a at 0"
my $s = "abc";
with   $s.index("a") { say "Found a at $_" }
orwith $s.index("b") { say "Found b at $_" }
orwith $s.index("c") { say "Found c at $_" }
else                 { say "Didn't find a, b or c" }

您可以混合基于 if 和基于 with 的子句。

# This says "Yes"
if 0 { say "No" } orwith Nil { say "No" } orwith 0 { say "Yes" };

unless 一样, 您可以使用 without 检查 undefinedness, 但是您可能不会添加一个 else 子句:

my $answer = Any;
without $answer { warn "Got: {$_.perl}" }

也有 withwithout 语句修饰符:

my $answer = (Any, True).roll;
say 42 with $answer;
warn "undefined answer" without $answer;

52.7. when

when 块类似于 if 块, 它们中的一个或两个都可以用在外部块中; 他们也都有一个“语句修饰符”形式。但是如何处理外部块中的相同代码是有区别的: 当 when 块执行时, 控制被传递到封闭块并忽略后面的语句; 但是当 if 块执行时, 后面的语句会被执行。[1]以下例子应说明 ifwhen 块的默认行为, 假设没有特殊出口或其他副作用的语句被包括在 ifwhen 块中:

{
    if X {...} # if X is true in boolean context, block is executed
    # following statements are executed regardless
}
{
    when X {...} # if X is true in boolean context, block is executed
                 # and control passes to the outer block
    # following statements are NOT executed
}

如果以上 ifwhen 块出现在文件作用域内, 则在每种情况下都会执行后面的语句。

还有另外一个功能, when 有而 if 没有的: when 的布尔上下文测试默认为 $_ ~~, 而 if 的不是。这会影响如何在没有 $_ (在这种情况下是 Any。 并且 Any 智能匹配`True`: Any ~~ True 产生 True)值的 when 块儿中使用X。请看以下代码:

{
    my $a = 1;
    my $b = True;
    when $a    { say 'a' }; # no output
    when so $a { say 'a' }  # a (in "so $a" 'so' coerces $a to Boolean context True
                            # which matches with Any)
    when $b    { say 'b' }; # no output (this statement won't be run)
}

最后, when 语句修饰符形式不影响在另一个块内部或外部执行以下语句:

say "foo" when X; # if X is true statement is executed
                  # following statements are not affected

由于成功匹配将退出块, 这段代码的行为:

$_ = True;
my $a;
{
    $a = do when .so { "foo" }
};
say $a; # OUTPUT: «(Any)»

解释了, 因为在存储或处理任何值之前放弃了 do 块。但是, 在这种情况下:

$_ = False;
my $a;
{
    $a = do when .so { "foo" }
};
say $a; # OUTPUT: «False»

因为比较是假的, 所以不会放弃该块, 因此 $a 实际上会得到一个值。

52.8. for

for 循环迭代一个列表, 每次迭代, 运行中的语句一次, 。如果块接受参数, 则列表元素作为参数提供。

my @foo = 1..3;
for @foo { $_.print } # prints each value contained in @foo
for @foo { .print }   # same thing, because .print implies a $_ argument
for @foo { 42.print } # prints 42 as many times as @foo has elements

当然, 尖括号语法或占位符可用于命名参数。

my @foo = 1..3;
for @foo -> $item { print $item }
for @foo { print $^item }            # same thing

可以声明多个参数, 在这种情况下, 迭代器在运行块之前根据需要从列表中获取尽可能多的元素。

my @foo = 1..3;
for @foo.kv -> $idx, $val { say "$idx: $val" }
my %hash = <a b c> Z=> 1,2,3;
for %hash.kv -> $key, $val { say "$key => $val" }
for 1, 1.1, 2, 2.1 { say "$^x < $^y" }  # says "1 < 1.1" then says "2 < 2.1"

尖块的参数可以具有默认值, 允许处理缺少元素的列表。

my @list = 1,2,3,4;
for @list -> $a, $b = 'N/A', $c = 'N/A' {
    say "$a $b $c"
}
# OUTPUT: «1 2 3
4 N/A N/A»

如果使用 for 的后缀形式, 则不需要块, 并且为语句列表设置主题。

say „I $_ butterflies!“ for <♥ ♥ ♥>;
# OUTPUT«I ♥ butterflies!
I ♥ butterflies!
I ♥ butterflies!»

for 可以在惰性列表上使用 - 只在需要时从列表中取元素, 因此要逐行读取文件, 您可以使用:

for $*IN.lines -> $line { .say }

迭代变量总是有词法的, 因此您无需使用 my 来为它们提供适当的作用域。此外, 它们是只读别名。如果您需要它们进行读写, 请使用 <-> 而不是 ->。如果需要 $_ 在 for 循环中进行读写, 请明确执行此操作。

my @foo = 1..3;
for @foo <-> $_ { $_++ }

for 循环可以生成每个附加块运行产生的值的 List。要捕获这些值, 请将 for 循环放在括号中或将它们赋值给数组:

(for 1, 2, 3 { $_ * 2 }).say;              # OUTPUT «(2 4 6)»
my @a = do for 1, 2, 3 { $_ * 2 }; @a.say; # OUTPUT «[2 4 6]»
my @b = (for 1, 2, 3 { $_ * 2 }); @b.say;  # OUTPUT: «[2 4 6]»

52.9. gather/take

gather 是一个返回值的序列的语句或块前缀。该值来自在 gather 块的动态作用域的 take 调用。

my @a = gather {
    take 1;
    take 5;
    take 42;
}
say join ', ', @a;          # OUTPUT: «1, 5, 42»

gather/take 可以懒惰地生成值, 具体取决于上下文。如果要强制延迟计算 , 请使用 lazy 子例程或方法。绑定到标量或无符号的容器也会导致懒惰。

例如:

my @vals = lazy gather {
    take 1;
    say "Produced a value";
    take 2;
}
say @vals[0];
say 'between consumption of two values';
say @vals[1];

# OUTPUT:
# 1
# between consumption of two values
# Produced a value
# 2

gather/take 是动态作用域的, 因此您可以从 gather 里面的 subs 或方法内部调用 take:

sub weird(@elems, :$direction = 'forward') {
    my %direction = (
        forward  => sub { take $_ for @elems },
        backward => sub { take $_ for @elems.reverse },
        random   => sub { take $_ for @elems.pick(*) },
    );
    return gather %direction{$direction}();
}

say weird(<a b c>, :direction<backward> );          # OUTPUT: «(c b a)»

如果值需要在调用方可变, 请使用 take-rw

请注意, gather/take 也适用于哈希。返回值仍然是一个 Seq 但在以下示例中对散列的赋值使其成为散列。

my %h = gather { take "foo" => 1; take "bar" => 2};
say %h;                                             # OUTPUT: «{bar => 2, foo => 1}»

52.10. supply/emit

将调用者发射到闭合的 supply 中:

my $supply = supply {
    emit $_ for "foo", 42, .5;
}
$supply.tap: {
    say "received {.^name} ($_)";
}

# OUTPUT:
# received Str (foo)
# received Int (42)
# received Rat (0.5)

52.11. given

given 语句是 Raku 中的 topicalizing 关键字, 类似于 C 语言中的 switch。换句话说, given 设置后面跟着的块里面的 $_。单独用例的关键词是 whendefault。通常的惯用法看起来像这样:

my $var = (Any, 21, any <answer lie>).pick;
given $var {
    when 21    { say $_ * 2    }
    when 'lie' { .say          }
    default    { say 'default' }
}

given 语句通常单独使用:

given 42 { .say; .Numeric; }

这比下面的写法更容易理解:

{ .say; .Numeric; }(42)

52.11.1. default 和 when

default 语句后面的 sub-block 离开时, 包含 default 语句的块立马离开。好像跳过了块中的其余语句。

given 42 {
    "This says".say;
    $_ == 42 and ( default { "This says, too".say; 43; } );
    "This never says".say;
}
# The above block evaluates to 43

when 语句也将这样做(但 when 语句修饰符将不会。)

此外, when 语句针对提供的表达式和 topic($_) 进行 智能匹配, 以便在指定匹配时可以检查值, 正则表达式和类型。

for 42, 43, "foo", 44, "bar" {
    when Int { .say }
    when /:i ^Bar/ { .say }
    default  { say "Not an Int or a Bar" }
}
# OUTPUT: «42
43
Not an Int or a Bar
44
Bar»

在这种形式中, given/when 结构的行为很像一组 if/elsif/else 语句。注意 when 语句的顺序。下面的代码打印 "Int" 而不是 42

given 42 {
    when Int { say "Int" }
    when 42  { say 42 }
    default  { say "huh?" }
}
# OUTPUT: «Int»

when 语句或 default 语句导致外部块返回时, 嵌套 whendefault 块不计为外部块, 因此只要不打开新块, 就可以嵌套这些语句并仍然在同一个“开关”(switch)中:

given 42 {
    when Int {
      when 42  { say 42 }
      say "Int"
    }
    default  { say "huh?" }
}
# OUTPUT: «42»

when 语句可以智能匹配签名

52.11.2. proceed

52.11.3. succeed

proceedsucceed 意在仅用于 whendefault 块的内部。

proceed 语句将立即离开 whendefault 块, 跳过其余的语句, 并在块后重新开始。这可以防止 whendefault 退出外部块。

given * {
    default {
        proceed;
        "This never says".say
    }
}
"This says".say;

这通常用于进入多个 when 块。proceed 在成功匹配后将恢复匹配, 如下:

given 42 {
    when Int   { say "Int"; proceed }
    when 42    { say 42 }
    when 40..* { say "greater than 40" }
    default    { say "huh?" }
}
# OUTPUT: «Int»
# OUTPUT: «42»

请注意, when 40..* 匹配未发生。为了匹配这样的情况, 人们需要在 when 42 块中添加 proceed

这不像 Cswitch 语句, 因为 proceed 不仅仅是进入直接跟随的块, 它还会再次尝试匹配 given 值, 请看以下代码:

given 42 {
    when Int { "Int".say; proceed }
    when 43  { 43.say }
    when 42  { 42.say }
    default  { "got change for an existential answer?".say }
}
# OUTPUT: «Int»
# OUTPUT: «42»

…​匹配 Int, 跳过 43, 因为值不匹配, 匹配 42, 因为这是下一个真实的匹配, 但不进入 default 块, 因为该 when 42 块不包含 proceed

相反, succeed 关键字短路执行并在此时退出整个 given 块。它也可能需要参数来指定块的最终值。

given 42 {
    when Int {
        say "Int";
        succeed "Found";
        say "never this!";
    }
    when 42 { say 42 }
    default { say "dunno?" }
}
# OUTPUT: «Int»

如果您不在 whendefault 块中, 则尝试使用 proceedsucceed 是错误的。还要记住, when 语句修饰符形式不会导致任何块被丢弃, 并且这样的语句中的任何 succeedproceed 都应用于周围的子句, 如果有的话:

given 42 {
    { say "This says" } when Int;
    "This says too".say;
    when * > 41 {
       { "And this says".say; proceed } when * > 41;
       "This never says".say;
    }
    "This also says".say;
}

52.11.4. given 作为语句

given 可以跟在语句后面, 以在给它所跟的语句中设置主题(topic)。

.say given "foo";
# OUTPUT: «foo»

printf "%s %02i.%02i.%i",
        <Mo Tu We Th Fr Sa Su>[.day-of-week - 1],
        .day,
        .month,
        .year
    given DateTime.now;
# OUTPUT: «Sa 03.06.2016»

52.12. loop

loop 语句接收 3 个参数, 分别是初始化, 条件和增量, 它们在元括号中用 ; 分隔。初始化执行一次, 任何变量声明都将溢出到周围的块中。每次迭代执行一次条件并将其强转为 Bool, 如果为 False 则循环停止。每次迭代执行一次增量器。

loop (my $i = 0; $i < 10; $i++) {
    say $i;
}

无限循环不需要圆括号。

loop { say 'forever' }

loop 如果出现在列表中, 则该语句可用于从附加块的每次运行结果中生成值:

(loop ( my $i = 0; $i++ < 3;) { $i * 2 }).say;               # OUTPUT: «(2 4 6)»
my @a = (loop ( my $j = 0; $j++ < 3;) { $j * 2 }); @a.say;   # OUTPUT: «[2 4 6]»
my @b = do loop ( my $k = 0; $k++ < 3;) { $k * 2 }; @b.say;  # same thing

for 循环不同, 不应该依赖于返回的值是否是惰性生成的。最好使用 eager 来保证循环的返回值真实运行:

sub heads-in-a-row {
    (eager loop (; 2.rand < 1;) { "heads".say })
}

52.13. while, until

只要条件为真, while 语句就会执行该块。所以

my $x = 1;
while $x < 4 {
    print $x++;
}
print "\n";

# OUTPUT: «123»

类似地, 只要表达式为 false , until 语句就会执行该块。

my $x = 1;
until $x > 3 {
    print $x++;
}
print "\n";

# OUTPUT: «123»

whileuntil 的条件可以用括号括起来, 但关键字和条件的左括号之间必须有空格。

whileuntil 两者可作为语句修饰符。例如:

my $x = 42;
$x-- while $x > 12

另见 repeat/while 和下面的 repeat/until

所有这些形式都可以以和 loop 相同的方式产生返回值。

52.14. repeat/while, repeat/until

*至少*执行*一次*该块, 如果条件允许, 则重复执行该块。这与 while/until 的不同之处在于, 即使条件出现在前面, 也会在循环结束时计算条件。

my $x = -42;
repeat {
    $x++;
} while $x < 5;
$x.say; # OUTPUT: «5»

repeat {
    $x++;
} while $x < 5;
$x.say; # OUTPUT: «6»

repeat while $x < 10 {
    $x++;
}
$x.say; # OUTPUT: «10»

repeat while $x < 10 {
    $x++;
}
$x.say; # OUTPUT: «11»

repeat {
    $x++;
} until $x >= 15;
$x.say; # OUTPUT: «15»

repeat {
    $x++;
} until $x >= 15;
$x.say; # OUTPUT: «16»

repeat until $x >= 20 {
    $x++;
}
$x.say; # OUTPUT: «20»

repeat until $x >= 20 {
    $x++;
}
$x.say; # OUTPUT: «21»

所有这些形式都可以以和 loop 相同的方式产生返回值。

52.15. return

sub return 将停止子程序或方法的执行, 运行所有相关的 phasers, 并提供给定的返回值给调用者。默认返回值是 Nil。如果提供了返回值类型约束, 则将检查它, 除非返回值为 Nil。如果类型检查失败, 则抛出异常 X::TypeCheck::Return。如果它通过了, 则发生控制异常, 可以通过 CONTROL 捕获。

无论嵌套有多深, 块中的任何 return 都与该块外部词法作用域中的第一个 Routine 绑定。请注意, 包的根目录中的 return 将在运行时失败。块中被惰性计算(例如在 map 里面)的`return` 可能发现外部词法例程在块执行时消失了。几乎在任何情况下 last 都是更好的选择。有关如何处理和生成返回值的更多信息, 请查看函数文档

52.16. return-rw

sub return 将返回值, 而不是容器。这些是不可变的, 并且在尝试可变(mutated)时会导致运行时错误。

sub s(){ my $a = 41; return $a };
say ++s();
CATCH { default { say .^name, ': ', .Str } };
# OUTPUT: «X::Multi::NoMatch.new(dispatcher …

要返回可变容器, 请使用 return-rw

sub s(){ my $a = 41; return-rw $a };
say ++s();
# OUTPUT: «42»

return 适用于关于 phasers 和控制异常的规则。

52.17. fail

在执行所有相关的 phasers之后, 离开例程并返回提供的 Exception 或包含在 Failure 里面的 Str 。如果调用者通过编译指令 use fatal; 激活致命异常, 则抛出异常而不是作为 Failure 返回。

sub f { fail "WELP!" };
say f;
CATCH { default { say .^name, ': ', .Str } }
# OUTPUT: «X::AdHoc: WELP!»

52.18. once

带有前缀 once 的块即使放在循环或递归例程中, 也只执行一次。

my $guard = 3;
loop {
    last if $guard-- <= 0;
    once { put 'once' };
    print 'many'
} # OUTPUT: «once
manymanymany»

这适用于包含代码对象的每个“克隆”, 因此:

({ once 42.say } xx 3).map: {$_(), $_()}; # says 42 thrice

请注意, 当多个线程运行同一个块儿的同一克隆时, 这不是线程安全的构造。还要记住, 方法每个类只有一个克隆, 而不是每个对象。

52.19. quietly

quietly 块将抑制其生成的所有警告。

quietly { warn 'kaput!' };
warn 'still kaput!';
# OUTPUT: «still kaput! [...]»

从块内调用的任何例程生成的任何警告也将被抑制:

sub told-you { warn 'hey...' };
quietly { told-you; warn 'kaput!' };
warn 'Only telling you now!'
# OUTPUT: «Only telling you now!
 [...] »

52.20. LABELs

while, until, loopfor 循环都可以带一个标签, 它可以用来标识 next, lastredo 。支持嵌套循环, 例如:

OUTAHERE: while True  {
    for 1,2,3 -> $n {
        last OUTAHERE if $n == 2;
    }
}

标签也可以在嵌套循环中用于命名每个循环, 例如:

OUTAHERE:
loop ( my $i = 1; True; $i++ ) {
  OUTFOR:
    for 1,2,3 -> $n {
      # exits the for loop before its natural end
      last OUTFOR if $n == 2;
  }

  # exits the infinite loop
  last OUTAHERE if $i >= 2;
}

52.21. next

next 命令启动循环的下一次迭代。所以代码:

my @x = 1, 2, 3, 4, 5;
for @x -> $x {
    next if $x == 3;
    print $x;
}

打印 “1245”。

如果存在NEXT phaser, 它将在下一次迭代之前运行:

my Int $i = 0;
while ($i < 10) {
  if ($i % 2 == 0) {
    next;
  }

  say "$i is odd.";

  NEXT {
    $i++;
  }
}
# OUTPUT: «1 is odd.
3 is odd.
5 is odd.
7 is odd.
9 is odd.»

从版本 6.d 开始, 对于它们运行的迭代, 循环中收集其最后一个语句值的 next 命令将返回 Empty

52.22. last

last 命令立即退出当前循环。

my @x = 1, 2, 3, 4, 5;
for @x -> $x {
    last if $x == 3;
    print $x;
}

打印 “12”。

如果存在LAST phaser, 则在退出循环之前运行:

my Int $i = 1;
while ($i < 10) {
  if ($i % 5 == 0) {
    last;
  }

  LAST {
    say "The last number was $i.";
  }
  NEXT {
    $i++;
  }
}
# OUTPUT: «The last number was 5.»

从版本 6.d 开始, 对于它们运行的迭代, 循环中收集其最后一个语句值的 last 命令将返回 Empty

52.23. redo

redo 命令重新启动循环块, 而不再计算条件。

loop {
    my $x = prompt("Enter a number");
    redo unless $x ~~ /\d+/;
    last;
}

53. 日期和时间函数

Raku 包括几个处理时态信息的类:DateDateTimeInstantDuration。前三个是 dateish,所以它们混合了 Dateish 角色,它定义了处理日期的类应该采用的所有方法和属性。它还包括以 X::Temporal 为根的异常的类层次结构。

我们将尝试在下一个(稍微扩展)的示例中说明这些类,这个示例可用于处理目录中的所有文件(默认情况下)。在目录中使用特定扩展名(默认为 .p6),根据他们的年龄对其进行排序,并计算每月创建的文件数量,以及在几个月的范围内表示的特定时期内修改的文件数量:

use v6;

sub MAIN( $path = ".", $extension = "p6" ) {
    my DateTime $right = DateTime.now;
    my %metadata;
    my %files-month;
    my %files-period;
    for dir($path).grep( / \.$extension $/ ) -> $file {
        CATCH {
            when X::Temporal { say "Date-related problem", .payload }
            when X::IO { say "File-related problem", .payload }
            default { .payload.say }
        }
        my Instant $modified = $file.modified;
        my Instant $accessed = $file.accessed;
        my Duration $duration = $accessed - $modified;
        my $age = $right - DateTime($accessed);
        my $time-of-day = $file.changed.DateTime.hh-mm-ss but Dateish;
        my $file-changed-date =  $file.changed.Date;
        %metadata{$file} = %( modified => $modified,
                              accessed => $accessed,
                              age => $age,
                              difference => $duration,
                              changed-tod => $time-of-day,
                              changed-date => $file-changed-date);
        %files-month{$file-changed-date.month}++;
        given $file-changed-date {
            when Date.new("2018-01-01")..^Date.new("2018-04-01") { %files-period<pre-grant>++}
            when Date.new("2018-04-01")..Date.new("2018-05-31") { %files-period<grant>++}
            default { %files-period<post-grant>++};
        }
    }

    %metadata.sort( { $^a.value<age> <=> $^b.value<age> } ).map: {
        say $^x.key, ", ",
        $^x.value<accessed modified age difference changed-tod changed-date>.join(", ");
    };
    %files-month.keys.sort.map: {
        say "Month $^x → %files-month{$^x}"
    };

    %files-period.keys.map: {
        say "Period $^x → %files-period{$^x}"
    };
}

第 6 行使用 DateTime 来包含现在返回的当前日期和时间。

CATCH phaser 在第 11 到 15 行中声明。其主要任务是区分与 DateTime 相关的异常和其他类型。这种异常可能来自无效格式时区冲突。除非文件属性有些损坏,否则两者都是不可能的,但无论如何它们都应该被捕获并与其他类型的异常分开。

我们使用第 16-17 行中的 Instants 来表示访问和修改文件的时刻。 Instant 是以原子秒为单位测量的,是对时间事件的非常低级别的描述;但是,第 18 行中声明的持续时间代表两个不同实例之间转换的时间,我们将使用它来表示年龄。

对于某些变量,我们可能有兴趣用一些日期特征来处理它们。 $time-of-day 包含文件更改日期的时间; changed 将返回一个 Instant,但它将转换为日期(Instant 而不是 Dateish),然后从中提取时间。 $time-of-day 将有 «Str+{Dateish}␤» 类型。

我们将使用此变量中的日期来查找文件更改的时间段。

Date.new("2018-01-01")..^Date.new("2018-04-01")

创建一个日期范围$file-changed-date 与它进行智能匹配。日期可以这样使用;在这种情况下,它会创建一个排除其最后一个元素的 Range

这个变量也用于计算修改文件的一年中的月份。日期 显然是 Dateish,然后有月份方法从中提取该属性。

可以比较持续时间对象。这用于

     %metadata.sort({
         $^a.value<age> <=> $^b.value<age>
     });

按年龄对文件进行排序。

54. 输入和输出全解

54.1. 基础知识

绝大多数常见的 IO 工作都是由IO::Path类型完成的。如果您想以某种形式或形状读取或写入文件,这就是您想要的类。它抽象出文件句柄(或“文件描述符”)的细节,因此你甚至不必考虑它们。

在幕后,IO::PathIO::Handle 一起使用 ; 一个你可以直接使用的类,如果你需要比 IO::Path 提供的更多控制。当与其他进程,例如通过 ProcProc::Async类型,您还可以处理IO::Handle 的*子类*:在IO::Pipe

最后,你有 IO::CatHandle,以及 IO::Spec 及其子类,你很少直接使用它们。这些类为您提供了高级功能,例如将多个文件作为一个句柄进行操作,或者进行低级路径操作。

除了所有这些类之外,Raku 还提供了几个子程序,可以让您间接使用这些类。如果您喜欢函数式编程风格或 Raku 单行程序,这些就派上用场了。

虽然 IO::Socket 及其子类也与输入和输出有关,但本指南并未涵盖它们。

54.2. 导航路径

54.2.1. What’s an IO::Path anyway?

要将路径表示为文件或目录,请使用 IO::Path 类型。获取该类型对象的最简单方法是通过在它身上调用 .IO 方法强制将 Str 类型转为路径类型:

say 'my-file.txt'.IO; # OUTPUT: «"my-file.txt".IO␤»

看起来这里似乎缺少某些东西 - 没有卷或绝对路径 - 但该信息实际上存在于对象中。你可以通过使用 .perl 方法看到它:

say 'my-file.txt'.IO.perl;
# OUTPUT: «IO::Path.new("my-file.txt", :SPEC(IO::Spec::Unix), :CWD("/home/camelia"))␤»

这两个额外的属性 - SPEC 和 - CWD 指定路径应该使用的操作系统语义类型以及路径的“当前工作目录”,即如果它是相对路径,则它相对于该目录。

这意味着无论你如何制作一个路径,IO::Path 对象在技术上总是指一个绝对路径。这就是它的 .absolute.relative 方法返回 Str 对象的原因,它们是字符串化路径的正确方法。

但是,不要急于将任何东西字符串化起来。将路径作为 IO::Path 对象传递。在路径上运行的所有例程都可以处理它们,因此不需要转换它们。

54.3. Working with files

54.3.1. Writing into files

54.3.2. Writing new content

让我们制作一些文件并从中写入和读取数据!spurtslurp 程序写和读取一块儿数据。除非您正在处理难以完全存储在内存中的非常大的文件,否则这两个例程都适合您。

"my-file.txt".IO.spurt: "I ♥ Perl!";

上面的代码在当前目录中创建了一个名为 my-file.txt 的文件,然后将文本 I ♥ Perl! 写入其中。如果 Raku 是您的第一语言,请庆祝您任务完成了!尝试打开您使用其他程序创建的文件,以验证您使用程序编写的内容。如果您已经了解其他语言,您可能想知道本指南是否遗漏了处理编码或错误条件等问题。

但是,这就是您需要的所有代码。默认情况下,字符串将按 utf-8 编码进行编码,并通过 Failure 机制处理错误:这些是您可以使用常规条件处理的异常。在这种情况下,我们会让所有潜在的 Failures 在调用之后陷入沉没,因此它们包含的任何异常都将被抛出。

54.3.3. 追加内容

如果您想在我们在上一节中创建的文件中添加更多内容,您可以注意 spurt 文档中提到的 :append 参数。但是,为了更好地控制,让我们自己使用 IO::Handle 来处理:

my $fh = 'my-file.txt'.IO.open: :a;
$fh.print: "I count: ";
$fh.print: "$_ " for ^10;
$fh.close;

.open 方法调用打开我们的 IO::Path,并返回一个 IO::Handle。我们把 :a 作为参数传递,表示我们想要以追加模式打开文件。

在接下来的两行代码中,我们使用 IO::Handle 上的 .print 常用方法打印包含 11 个文本('I count: ' 字符串和 10 个数字)的文本行。请注意,Failure 机制再一次负责我们的所有错误检查。如果 .open 失败,它将返回一个 Failure,当我们尝试在其上调用 .print 方法时将抛出异常。

最后,我们通过调用它上面的 .close 方法来关闭 IO::Handle。这样*做很重要*,特别是在大型程序或处理大量文件的程序中,因为许多系统对程序可以同时打开的文件数量有限制。如果您没有关闭句柄,最终您将达到该限制并且 .open 调用将失败。请注意,与其他一些语言不同,Raku 不使用引用计数,因此当离开所定义的作用域时,文件句柄不会关闭。只有当它们被垃圾收集并且未能关闭句柄时,它们才会被关闭,这可能会导致程序在打开的句柄有机会在垃圾回收*之前*达到文件限制。

54.4. 从文件中读取

54.4.1. 使用 IO::Path

我们在前面的章节中已经看到,在文件中写东西是 Raku 中的单行代码。从它们中读取,同样容易:

say 'my-file.txt'.IO.slurp;        # OUTPUT: «I ♥ Perl!␤»
say 'my-file.txt'.IO.slurp: :bin;  # OUTPUT: «Buf[uint8]:0x<49 20 e2 99 a5 20 50 65 72 6c 21>␤»

.slurp 方法读取文件的全部内容并将其作为单个 Str 对象返回,如果请求二进制模式,则通过指定 :bin 命名参数将其作为 Buf 对象返回。

由于 slurping 将整个文件加载到内存中,因此它不适合处理大文件。

IO::Path 类型提供了另外两种方便的方法:.words.lines,这俩方法惰性地读取小块文件并返回(默认)不保留已消耗值的Seq 对象。

这是一个示例,它在文本文件中查找提及 Perl 的行并将其打印出来。尽管文件本身太大而无法容纳到可用的RAM 中,但程序运行时不会出现任何问题,因为内容是以小块的形式处理的:

.say for '500-PetaByte-File.txt'.IO.lines.grep: *.contains: 'Perl';

这是另一个打印文件中前 100 个单词的示例,没有完全加载它:

.say for '500-PetaByte-File.txt'.IO.words: 100

请注意,我们通过传递 limit 参数给 .words而不是使用列表索引操作 来完成此操作。原因是在于底层仍然使用文件句柄,并且在完全使用返回的 Seq 之前,句柄将保持打开状态。如果没有引用 Seq,最终句柄将在垃圾收集运行期间关闭,但在大型程序中使用大量文件时,最好确保所有句柄立即关闭。所以,你应该始终确保 SeqIO::Path.words.lines 方法是完全具体化 ; 而 limit 参数可以帮助你。

54.4.2. Using IO::Handle

当然,您可以使用 IO::Handle 类型从文件中读取,这样可以更好地控制您正在执行的操作:

given 'some-file.txt'.IO.open {
    say .readchars: 8;  # OUTPUT: «I ♥ Perl␤»
    .seek: 1, SeekFromCurrent;
    say .readchars: 15;  # OUTPUT: «I ♥ Programming␤»
    .close
}

IO::Handle 给你 .read.readchars.get.getc.words.lines.slurp.comb.split.Supply 方法从中读取数据。有很多选择; 当你读取完时,需要关闭句柄。

与某些语言不同,当离开定义的作用域时,句柄不会自动关闭。相反,它将保持打开,直到被垃圾回收为止。为了使关闭更容易,一些方法允许您指定 :close 参数,您还可以使用 will leave trait 或 Trait::IO 模块提供的 does auto-close trait。

54.5. 错误的做事方法

本节介绍如何不执行 Raku IO。

54.5.1. 别去管 $*SPEC

您可能听说过 $*SPEC 并看到过一些代码或书籍显示其用于拆分和连接路径片段的用法。它提供的一些例程名称甚至可能看起来与您在其他语言中使用的名称相似。

但是,除非您正在编写自己的 IO 框架,否则几乎不需要直接使用 $*SPEC$*SPEC 提供低级别的东西,它的使用不仅会使你的代码难以阅读,你可能会引入安全问题(例如空字符)!

IO::Path 类型是 Raku 世界的主力。它满足所有路径操作需求,并提供快捷例程,让您避免处理文件句柄。用它而不是 $*SPEC 这样的东西。

提示:您可以使用 / 连接路径部分并将其提供给 IO::Path 例程; 无论操作系统如何,他们仍然可以做正确的事情。

# WRONG!! TOO MUCH WORK!
my $fh = open $*SPEC.catpath: '', 'foo/bar', $file;
my $data = $fh.slurp;
$fh.close;
# RIGHT! Use IO::Path to do all the dirty work
my $data = 'foo/bar'.IO.add($file).slurp;

但是,将它用于 IO::Path 无法提供的东西是很好的。例如,.devnull 方法:

{
    temp $*OUT = open :w, $*SPEC.devnull;
    say "In space no one can hear you scream!";
}
say "Hello";

54.6. 字符串化 IO::Path

不要使用 .Str 方法对 IO::Path 对象进行字符串化,除非您只是想将它们显示在某个地方以供参考或使用。.Str 方法返回 IO::Path 实例化的任何基本路径字符串。它不考虑 $.CWD 属性的值。例如,此代码已损坏:

my $path = 'foo'.IO;
chdir 'bar';
# WRONG!! .Str DOES NOT USE $.CWD!
run <tar -cvvf archive.tar>, $path.Str;

chdir 调用更改了当前目录的值,但我们创建的 $path 是相对于该更改之前的目录。

但是,IO::Path 对象*确实*知道它相对于哪个目录。我们只需要使用 .absolute.relative 来字符串化对象。两个例程都返回一个 Str 对象; 它们不同之处在于结果是绝对路径还是相对路径。所以,我们可以像这样修复我们的代码:

my $path = 'foo'.IO;
chdir 'bar';
# RIGHT!! .absolute does consider the value of $.CWD!
run <tar -cvvf archive.tar>, $path.absolute;
# Also good:
run <tar -cvvf archive.tar>, $path.relative;

54.6.1. 注意 $*CWD

虽然通常不在视线范围内,但默认情况下,每个 IO::Path 对象都使用 $*CWD 的当前值来设置其 `$.CWD`属性。这意味着有两件事需要注意。

54.6.2. temp the $*CWD

这段代码是错误的:

# WRONG!!
my $*CWD = "foo".IO;

my $*CWD$*CWD 变为未定义的。然后 .IO coercer 继续并将其正创建的路径的$.CWD 属性设置为 undefined 的 $*CWD 字符串化版本 ; 一个空字符串。

执行此操作的正确方法是使用 temp 而不是 my。它会将 $*CWD 的更改效果本地化,就像 my 那样,但它不会使其未定义,因此 .IO coercer 仍将获得正确的旧值:

temp $*CWD = "foo".IO;

更好的是,如果要在本地化的 $*CWD 中执行某些代码,请使用该indir 例程。

55.

Packages - Organizing and referencing namespaced program elements

包是指定程序元素的嵌套命名空间。 模块,类,Grammar是包类型。 像目录中的文件一样,通常可以使用其短名称(如果它们是本地的)或使用较长的名称来消除歧义的引用具名元素。

55.1. Names

名称是作为变量名称的合法部分的任何东西(不包括sigil符号)。 这包括:

$foo                # 简单标识符
$Foo::Bar::baz      # 通过 :: 分割的组合标识符
$Foo::($bar)::baz   # 执行插值的组合标识符
$42                 # numeric names
$!                  # 某些标点符号变量

:: 用于分割嵌套的包名。

55.1.1. 包限定名

普通的包限定名像这样:

$Foo::Bar::baz  # 包 Foo::Bar 中的 $baz 变量

有时保持sigil与变量名很清晰,所以来写这个的一个替代方式是:

Foo::Bar::<$baz>

这在编译时解决,因为变量名是一个常量。

如果 :: 之前的名称部分为 null,则意味着包未指定并且必须搜索。 一般来说,这意味着跟在主sigil后面的初始 :: 是对编译时已知的名字的无操作(no-op),但 ::() 也可以用来引入插值。 另外,在没有另一个sigil的情况下,:: 可以作为它自己的sigil,表明有意使用一个尚未声明的包名。

55.2. 伪包

在名称前面保留以下伪包名称:

MY          # 当前词法作用域中的符号 (aka $?SCOPE)
OUR         # 当前包中的符号 (aka $?PACKAGE)
CORE        # 最外层词法作用域, 定义标准 Perl
GLOBAL      # Interpreter-wide package symbols, really UNIT::GLOBAL
PROCESS     # 进程相关的全局变量 (superglobals)
COMPILING   # 正在编译的作用域中的词法符号

以下相对名称也保留,但可以在名称中的任何位置使用:

CALLER      # Contextual symbols in the immediate caller's lexical scope
CALLERS     # Contextual symbols in any caller's lexical scope
DYNAMIC     # Contextual symbols in my or any caller's lexical scope
OUTER       # Symbols in the next outer lexical scope
OUTERS      # Symbols in any outer lexical scope
LEXICAL     # Contextual symbols in my or any outer's lexical scope
UNIT        # Symbols in the outermost lexical scope of compilation unit
SETTING     # Lexical symbols in the unit's DSL (usually CORE)
PARENT      # Symbols in this package's parent package (or lexical scope)
CLIENT      # The nearest CALLER that comes from a different package

文件的作用域称为 UNIT,但在对应于语言设置的外面有一个或多个词法作用域(其他文化中通常称为序幕)。 因此,SETTING 作用域等价于 UNIT::OUTERS。 对于标准的Perl程序,SETTINGCORE 相同,但是各种启动选项(如 -n-p)可以使您进入特定领域语言,在这种情况下,CORE 仍然是标准语言的作用域,而 SETTING 表示定义用作当前文件的设置的DSL的作用域。 当作为名称中间的搜索项使用时,SETTING包括其所有外部作用域,直到 CORE。 要仅获取设置的最外层作用域,请改用 UNIT::OUTER

55.3. 查找名字

55.3.1. 插值到名字中

您可以使用 ::($expr) 将字符串插入到包或变量名中,$expr 表达式中通常放置包或变量名。 该字符串允许包含额外的 :: 实例,这将被解释为包嵌套。 您只能内插整个名称,因为结构以 :: 开头,并且立即结束或用括号之外的另一个 :: 继续。 大多数符号引用使用这种记法:

$foo = "Bar";
$foobar = "Foo::Bar";
$::($foo)           # lexically-scoped $Bar
$::("MY::$foo")     # lexically-scoped $Bar
$::("OUR::$foo")    # package-scoped $Bar
$::("GLOBAL::$foo") # global $Bar
$::("PROCESS::$foo")# process $Bar
$::("PARENT::$foo") # current package's parent's $Bar
$::($foobar)        # $Foo::Bar
$::($foobar)::baz   # $Foo::Bar::baz
$::($foo)::Bar::baz # $Bar::Bar::baz
$::($foobar)baz     # ILLEGAL at compile time (no operator baz)

初始 :: 不表明全局。 这里作为插值语法的一部分,它甚至不暗示包。 ::() 组件插值之后,间接名称被查找,就像它在原始源代码中一样,优先级首先指定为前导伪包名称,然后指向词法作用域中的名称(搜索 向外扩展,以`CORE`结束)。 最后搜索当前包。

使用 MY 伪包将查找限制为当前词法作用域,OUR 将作用域限制为当前包作用域。

55.3.2. 直接查找

要在包的符号表中直接查找而不进行扫描,请将包名视为哈希:

Foo::Bar::{'&baz'}  # same as &Foo::Bar::baz
PROCESS::<$IN>      # Same as $*IN
Foo::<::Bar><::Baz> # same as Foo::Bar::Baz

不像 ::() 符号引用,这不解析`::`的参数,也不从该初始点启动命名空间扫描。 此外,对于常量下标,保证在编译时解析符号。

空伪包是与普通名称搜索有相同的搜索列表。 也就是说,以下各项在意义上是相同的:

$foo
$::{'foo'}
::{'$foo'}
$::<foo>
::<$foo>

它们中的每一个都向外扫描词法作用域,然后扫描当前的包作用域(虽然当“strict”有效时包作用域被禁止,)。

作为这些规则的结果,您可以把变量名写的很随意:

$::{'!@#$#@'}
::{'$!@#$#@'}

只要名字中没有空格, 您也可以使用 ::<> 形式。

55.3.3. 包查找

将包对象本身下标为哈希对象,其键是变量名,包括任何sigil。 包对象可以通过使用 :: 后缀从类型名中派生:

MyType::<$foo>

55.4. 全局

解释器全局变量存在于 GLOBAL 包中。 用户程序在 GLOBAL 包中启动,因此默认情况下,主线代码中的“our”声明会进入该包。 进程范围的变量存在于 PROCESS`包中。 大多数预定义的全局变量,例如 `$*UID$*PID 实际上是进程全局变量。

55.5. 版本

任何包都可以有一个 Version 对象。 这个对象可以通过 $PACKAGE.^ver 或从包 Fully::Qualified::Name.^ver 外部访问。

56. 操作符

56.1. 操作符优先级

Raku 操作符的优先性和关联性决定了表达式中操作数的计算顺序。

当两个具有不同优先级的运算符作用于同一个操作数时,涉及优先级较高的运算符的子表达式将首先被计算。例如,在表达式 1 + 2 * 3 中,用于加法的二进制 + 运算符和用于乘法的二进制 运算符都作用于操作数 2。因为 运算符比 + 运算符具有更高的优先级,所以子表达式 2*3 将首先被计算。因此,整个表达式的结果值是7而不是9。

我们也可以用"绑定"来代替"优先级":先例较高的运算符被认为与相关操作数有较紧密的绑定,而先例较低的运算符被认为有较松的绑定。在实践中,人们也可能会遇到一些术语的混合,比如说一个操作符具有较紧或较松的先决条件的声明。

当具有相同优先级的两个操作符作用于操作数时,操作符的关联性决定了哪个子表达式/操作符首先被评估。例如,在表达式 100 / 2 * 10 中,二进制除法运算符 / 和二进制乘法运算符 * 具有相同的优先级,因此它们的计算顺序由其关联性决定。由于这两个运算符是左关联性的,所以从左边开始的运算是这样分组的。(100 / 2) * 10. 因此,该表达式的计算结果是 500,而不是5。

下表总结了 Raku 提供的优先级(列标为 Level),按照优先级从高到低的顺序排列。对于每个优先级,该表还指出了分配给该级别的运算符的关联性(列标为A),以及一些示例性运算符(列标为示例)。

A

Level

Examples

N

Terms

42 3.14 "eek" qq["foo"] $x :!verbose @$array

L

方法后缀

.meth .+ .? .* .() .[] .{} .<> .«» .:: .= .^ .:

N

自增

++ — 

R

求幂

**

L

Symbolic unary

! + - ~ ?

+^ ~^ ?^ ^

L

乘法

* / % %% +& +< +> ~& ~< ~> ?& div mod gcd lcm

L

加法

+ -

+^ ~

~^ ?

?^

L

重复

x xx

X

连结

~ o ∘

X

Junctive and

& (&) (.) ∩ ⊍

X

Junctive or

^ (

) (^) (+) (-) ∪ ⊖ ⊎ ∖

L

Named unary

temp let

N

Structural infix

but does <⇒ leg cmp .. ..^ ^.. ..

C

Chaining infix

!= ≠ == < ⇐ ≤ > >= ≥ eq ne lt le gt ge ~~ === eqv !eqv =~= ≅ (elem) (cont) (<) (>) (⇐) (>=) (<) (>) ∈ ∉ ∋ ∌ ⊂ ⊄ ⊃ ⊅ ⊆ ⊈ ⊇ ⊉ ≼ ≽

X

Tight and

&&

X

Tight or

^^ // min max

R

Conditional

?? !! ff fff

R

Item assignment

= ⇒ += -= **= xx= .=

L

Loose unary

so not

X

Comma operator

, :

X

List infix

Z minmax X X~ X* Xeqv …​ … …​^ …^

R

List prefix

print push say die map substr …​ [+] [*] any Z=

X

Loose and

and andthen notandthen

X

Loose or

or xor orelse

X

Sequencer

⇐=, =⇒, <⇐=, =⇒>

N

Terminator

下面使用的两处 ! 符号一般代表任何一对儿拥有相同优先级的操作符, 上表指定的二元操作符的结合性解释如下(其中 A 代表结合性, associativities ):

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; $)

对于一元操作符, 这解释为:

A

Assoc

Meaning of !$a!

L

left

(!$a)!

R

right

!($a!)

N

non

ILLEGAL

下面描述的操作符, 默认假定为 left 结合性。

56.2. 操作符种类

操作符能出现在相对于 term 的几个位置处:

+term

prefix (后缀)

term1 + term2

infix (中缀)

term++

postfix (后缀)

(term)

circumfix (环缀)

term1[term2]

postcircumfix (后环缀)

.+(term)

method (方法)

每个操作符也可以用作子例程。 这样的子例程的名字由操作符的种类, 然后后跟一个冒号,再加上一组引号结构, 引号结构中是组成操作符的符号(s):

infix:<+>(1, 2)                 # same as 1 + 2
circumfix:«[ ]»(<a b c>);       # same as [<a b c>]

作为一种特殊情况, listop(列表操作符)既能作为 term 又能作为前缀。子例程调用是最常见的列表操作符。其它情况包括元运算中缀操作符 [+]| 1, 2, 3prefix 等 stub 操作符。

定义自定义操作符在 定义运算符函数 中有涉及。

56.3. 元运算符

元操作符可以与其他操作符或子程序进行参数化,就像函数可以接受函数作为参数一样。要使用一个子程序作为参数,请在其名称前加上 &。Raku 将在后台生成实际的组合运算符,允许该机制应用于用户定义的运算符。要解除连锁元操作符的歧义,请将内部操作符用方括号括起来。有相当多的元操作符具有不同的语义,接下来解释一下。

56.4. Substitution 运算符

每个替换操作符主要有两种形式:一种是小写的(如 s///),原地执行(即破坏性行为;另一种是大写形式(如 S///),提供非破坏性行为。

56.4.1. s/// 原地替换

my $str = 'old string';
$str ~~ s/o .+ d/new/;
say $str; # OUTPUT: «new string␤»

s/// 对 $_ 主题变量进行操作,在原地改变它。它使用给定的 Regex 来查找要替换的部分,并将其改为提供的替换字符串。将 $/ 设置为 Match 对象,如果有多个匹配对象,则设置为 Match 对象的 List。返回 $/

这个操作符通常和 ~~ 智能匹配操作符一起使用,因为它把左侧的 $_ 别名为 $/,而 s/// 使用的是 $/

Regex 捕获可以在替换部分引用;它采用与 .subst 方法相同的副词,在 s 和开头的 / 之间,用可选的空格分隔:

my $str = 'foo muCKed into the lEn';

# replace second 'o' with 'x'
$str ~~ s:2nd/o/x/;

# replace 'M' or 'L' followed by non-whitespace stuff with 'd'
# and lower-cased version of that stuff:
$str ~~ s :g :i/<[ML]> (\S+)/d{lc $0}/;

say $str; # OUTPUT: «fox ducked into the den␤»

你也可以使用不同的定界符:

my $str = 'foober';
$str ~~ s!foo!fox!;
$str ~~ s{b(.)r} = " d$0n";
say $str; # OUTPUT: «fox den␤»

非成对字符可以简单地替换原来的斜线。成对字符,如大括号,只用于匹配部分,替换由赋值给出(任何东西:一个字符串、一个例程调用等)。

56.4.2. S/// 非破坏性替换

say S/o .+ d/new/ with 'old string';      # OUTPUT: «new string␤»
S:g/« (.)/$0.uc()/.say for <foo bar ber>; # OUTPUT: «Foo␤Bar␤Ber␤»

S/// 使用与 s/// 运算符相同的语义,只是它保留原始字符串,并返回结果字符串,而不是 $/$/ 仍被设置为与 s/// 相同的值)。

注意:由于结果是以返回值的形式获得的,所以将这个操作符与 ~~ 智能匹配操作符一起使用是一个错误,会发出警告。要在一个不是这个操作符所使用的 $_ 的变量上执行替换,可以用 givenwith 或其他任何方式将其别名为 $_。或者,使用 .subst 方法。

56.4.3. tr/// 原地转写

my $str = 'old string';
$str ~~ tr/dol/wne/;
say $str; # OUTPUT: «new string␤»

tr/// 对 $_ 主题变量进行操作,并在原地改变它。它的行为类似于使用单个 Pair 参数调用的 Str.trans,其中键是匹配部分(上例中的字符 dol),值是替换部分(上例中的字符 wne)。接受与 Str.trans 相同的副词。返回 StrDistance 对象,测量原始值和结果字符串之间的距离。

my $str = 'old string';
$str ~~ tr:c:d/dol st//;
say $str; # OUTPUT: «old st␤»

56.4.4. TR/// 非破块性转写

with 'old string' {
    say TR/dol/wne/; # OUTPUT: «new string␤»
}

TR/// 的行为与 tr/// 运算符相同,不同的是它不对 $_ 的值进行处理,而是返回结果字符串。

say TR:d/dol // with 'old string'; # OUTPUT: «string␤»

56.5. 赋值运算符

Raku 有多种赋值运算符,大致可以分为简单赋值运算符和复合赋值运算符。

简单赋值运算符的符号是 =,它是"重载"的,根据使用它的上下文,它既可以表示项赋值,也可以表示列表赋值

my $x = 1;        # item assignment; $x = 1
my @x = 1,2,3;    # list assignment; @x = [1,2,3]

关于这两种类型的赋值,请参见项赋值和列表赋值一节,有更详细的比较讨论。

复合赋值运算符是元运算符:它们将简单的赋值运算符 = 与下位运算符结合起来,形成一个新的运算符,在将结果赋值给左操作数之前,执行下位运算符指定的操作。一些内置复合赋值运算符的例子是 +=-=*=/=min=`和 `~=。 下面是它们的工作原理。

my $a = 32;
$a += 10;         # $a = 42
$a -= 2;          # $a = 40

$a = 3;
$a min= 5;        # $a = 3
$a min= 2;        # $a = 2

my $s = 'a';
$s ~= 'b';        # $s = 'ab'

# And an example of a custom operator:
sub infix:<space-concat> ($a, $b) { $a ~ " " ~ $b };
my $a = 'word1';
$a space-concat= 'word2';                 # OUTPUT: «'word1 word2'␤»

简单赋值运算符和复合赋值运算符有一个共同点,那就是它们形成了所谓的赋值表达式,这些赋值表达式返回或计算到赋值。

my sub fac (Int $n) { [*] 1..$n };        # sub for calculating factorial
my @x = ( my $y = fac(100), $y*101 );     # @x = [100!, 101!]

my $i = 0;
repeat { say $i } while ($i += 1) < 10;   # OUTPUT: «0,1,2,...9␤»

在第一个例子中,赋值表达式 my $y = fac(100) 声明 $y,将值 fac(100) 赋给它,最后返回赋值 fac(100)。然后将返回的值作为构建列表的考虑因素。在第二个例子中,复赋值表达式 $i += 1 将值 $i + 1 赋给 $i,随后计算为赋值 $i+1,从而使返回的值被用于判断 while 循环条件。

在处理简单赋值运算符和复合赋值运算符时,人们很容易认为,例如下面两个语句(总是)是等价的。

expression1 += expression2;                     # compound assignment

expression1  = expression1 + expression2;       # simple assignment

然而,它们不是,原因有二。首先,复合赋值语句中的 expression1 只被评估一次,而简单赋值语句中的 expression1 被评估两次。其次,根据相关的中缀运算符,复合赋值语句可能会隐式初始化 expression1,如果它是一个未定义值的变量。在简单赋值语句中,对于 expression1 不会发生这种初始化。

上述简单赋值语句和复合赋值语句的两个区别在下文中进行了简单的阐述。

第一种差异是编程语言中常见的,大多是不言而喻的。在复合赋值中,只有一个 expression1,它被明确地指定为既是要执行的加法的术语,又是加法的结果,即总和的存储位置。因此,没有必要对它进行两次计算。相比之下,简单赋值更具有通用性,因为作为加法项的 expression1 的值不一定与定义必须存储总和的位置的 expression1 的值相同。因此,这两个表达式被分开评估。这种区别在 expression1 的评价会以改变一个或多个变量的形式产生副作用的情况下尤为重要。

my @arr = [10, 20, 30];
my $i = 0;

if rand < 1/2 {
    @arr[++$i] += 1;                # @arr = [10,21,30]
} else {
    @arr[++$i] = @arr[++$i] + 1;    # @arr = [10,31,30] (or [10,20,21]?)
}                                   # the result may be implementation-specific
say @arr;

上面指出的第二个区别与在累加器模式中使用复合赋值运算符的普遍做法有关。这种模式涉及到一个所谓的累加器:一个在循环中计算一系列值的总和或乘积的变量。为了避免显式累加器初始化的需要,Raku 的复合赋值运算符在合理的情况下默默地处理了初始化。

my @str = "Cleanliness is next to godliness".comb;
my ($len, $str);
for @str -> $c {
  $len += 1;
  $str ~= $c;
}
say "The string '$str' has $len characters.";

在这个例子中,累加器 $len$str 分别被隐式初始化为 0"",这说明初始化值是操作符特定的。在这方面还需要注意的是,并不是所有的复合赋值运算符都能合理地初始化一个未定义的左侧变量。例如,/= 运算符不会任意为红利选择一个值,相反,它会抛出一个异常。

尽管不是严格意义上的运算符,但方法的使用方式可以与复赋值运算符相同。

my $a = 3.14;
$a .= round;      # $a = $a.round; OUTPUT: «3»

56.6. 反向关系运算符

关系运算符返回 Bool 的结果可以通过前缀为 ! 来否定。为了避免与 !! 运算符混淆,你不能修改任何已经以 ! 开头的运算符。

!==!eq 有一些快捷方式,即 !=ne

my $a = True;
say so $a != True;    # OUTPUT: «False␤»
my $i = 10;

my $release = Date.new(:2015year, :12month, :24day);
my $today = Date.today;
say so $release !before $today;     # OUTPUT: «False␤»

56.7. 反转运算符

任何中缀运算符都可以通过前缀 R 将其两个参数反过来调用,操作数的关联性也是反过来的。

say 4 R/ 12;               # OUTPUT: «3␤»
say [R/] 2, 4, 16;         # OUTPUT: «2␤»
say [RZ~] <1 2 3>,<4 5 6>  # OUTPUT: «(41 52 63)␤»

56.8. 超运算符

超运算符包括 «»,以及它们的 ASCII 变体 <<>>。它们将一个给定的运算符用 « 和/或 » 封闭(或者在一元运算符的前面或后面),应用到一个或两个列表中,返回结果列表,«» 的尖号部分针对较短的列表。单个元素被转为一个列表,所以也可以使用。如果其中一个列表比另一个短,运算符将在较短的列表上循环,直到处理完较长列表的所有元素。

say (1, 2, 3) »*» 2;          # OUTPUT: «(2 4 6)␤»
say (1, 2, 3, 4) »~» <a b>;   # OUTPUT: «(1a 2b 3a 4b)␤»
say (1, 2, 3) »+« (4, 5, 6);  # OUTPUT: «(5 7 9)␤»
say (&sin, &cos, &sqrt)».(0.5);
# OUTPUT: «(0.479425538604203 0.877582561890373 0.707106781186548)␤»

最后一个例子说明了后缀运算符(在本例中是 .())如何也能被超级化(hypered)。

my @a = <1 2 3>;
my @b = <4 5 6>;
say (@a,@b)»[1]; # OUTPUT: «(2 5)␤»

在这种情况下,被 hyper 化的是 _[postcircumfix[]]。

赋值元运算符可以被 hyper 化。

my @a = 1, 2, 3;
say @a »+=» 1;    # OUTPUT: «[2 3 4]␤»
my ($a, $b, $c);
(($a, $b), $c) «=» ((1, 2), 3);
say "$a, $c";       #  OUTPUT: «1, 3␤»

一元运算符的超形式,其尖头对准运算符,钝头对准要运算的列表。

my @wisdom = True, False, True;
say !« @wisdom;     # OUTPUT: «[False True False]␤»

my @a = 1, 2, 3;
@a»++;
say @a;             # OUTPUT: «[2 3 4]␤»

超运算符是在嵌套数组上递归定义的。

say -« [[1, 2], 3]; # OUTPUT: «[[-1 -2] -3]␤»

另外,方法可以不按顺序,以并发的方式被调用。结果列表将按顺序排列。请注意,所有的超运算符都是并行的候选者,如果方法有副作用,会造成撕扯。优化器对超运算符有完全的支配权,这也是用户不能定义超运算符的原因。

class CarefulClass { method take-care {} }
my CarefulClass @objs;
my @results = @objs».take-care();

my @slops;        # May Contain Nuts
@slops».?this-method-may-not-exist();

超运算符可以与哈希一起工作。尖头方向表示在产生的哈希中是否要忽略缺失的键。封闭运算符对所有在两个哈希中都有键的值进行操作。

%foo «+» %bar;

键的交集合

%foo »+« %bar;

键的并集

%outer »+» %inner;

只有存在于 %outer 中的 %inner 键才会出现在结果中

my %outer = 1, 2, 3 Z=> <a b c>;
my %inner = 1, 2 Z=> <x z>;
say %outer «~» %inner;          # OUTPUT: «{"1" => "ax", "2" => "bz"}␤»

超运算符可以接受用户定义的运算符作为其运算符参数。

sub pretty-file-size (Int $size --> Str) {
    # rounding version of infix:</>(Int, Int)
    sub infix:<r/>(Int \i1, Int \i2) {
        round(i1 / i2, 0.1)
    }

    # we build a vector of fractions of $size and zip that with the fitting prefix
    for $size «[r/]« (2**60, 2**50, 2**40, 2**30, 2**20, 2**10)
              Z      <EB     PB     TB     GB     MB     KB> -> [\v,\suffix] {
        # starting with the biggest suffix,
        # we take the first that is 0.5 of that suffix or bigger
        return v ~ ' ' ~ suffix if v > 0.4
    }
    # this be smaller or equal then 0.4 KB
    return $size.Str;
}

for 60, 50, 40, 30, 20, 10 -> $test {
    my &a = { (2 ** $test) * (1/4, 1/2, 1, 10, 100).pick * (1..10).pick };
    print pretty-file-size(a.Int) xx 2, ' ';
}

# OUTPUT: «10 EB 4 EB 2 PB 5 PB 0.5 PB 4 TB 300 GB 4.5 GB 50 MB 200 MB 9 KB 0.6 MB␤»

超运算符是否降为子列表,取决于链的内部操作符的节点性。对于超方法调用运算符(».),目标方法的节点性很重要。

say (<a b>, <c d e>)».elems;        # OUTPUT: «(2 3)␤»
say (<a b>, <c d e>)».&{ .elems };  # OUTPUT: «((1 1) (1 1 1))␤»

你可以通过链式超运算符来解构一个列表的列表。

my $neighbors = ((-1, 0), (0, -1), (0, 1), (1, 0));
my $p = (2, 3);
say $neighbors »>>+<<» ($p, *);   # OUTPUT: «((1 3) (2 2) (2 4) (3 3))␤»

56.9. 化简元运算符

化简元运算符 [ ],用给定的中缀运算符化简一个列表。它给出的结果与 reduce 例程相同 - 详情请看这里。

# These two are equivalent:
say [+] 1, 2, 3;                # OUTPUT: «6␤»
say reduce &infix:<+>, 1, 2, 3; # OUTPUT: «6␤»

方括号和运算符之间不允许有空白。如果要包住一个函数而不是运算符,请提供额外的一层方括号。

sub plus { $^a + $^b };
say [[&plus]] 1, 2, 3;          # OUTPUT: «6␤»

对参数列表进行迭代而不进行扁平化处理。这意味着,你可以将一个嵌套的列表传递给列表导数运算符的还原形式。

say [X~] (1, 2), <a b>;         # OUTPUT: «(1a 1b 2a 2b)␤»

相当于 1, 2 X~ <a b>

默认情况下,只返回化简的最终结果。在包裝運算符號前加上一個 \,返回所有中间的惰性清单。这叫做"三角还原"。如果非元部分已经包含了一个 \,则用 [] 引用它(例如 [\[\x]])。

my @n = [\~] 1..*;
say @n[^5];         # OUTPUT: «(1 12 123 1234 12345)␤»

56.10. 交叉运算符

交叉元操作符 X,将按照交叉乘积的顺序将给定的导数操作符应用于所有列表,这样最右边的操作符变化最快。

1..3 X~ <a b> # OUTPUT: «<1a, 1b, 2a, 2b, 3a, 3b>␤»

56.11. Zip 元运算符

zip 元操作符(这和 Z 不一样)将把给定的下位操作符应用于从其参数中取一左一右的对。返回结果列表。

my @l = <a b c> Z~ 1, 2, 3;     # OUTPUT: «[a1 b2 c3]␤»

如果其中一个操作数过早地用完了元素,zip 运算符将停止。无限列表可以用来重复元素。一个最终元素为 * 的列表将无限期地重复它的最后2个元素。

my @l = <a b c d> Z~ ':' xx *;  # OUTPUT: «<a: b: c: d:>»
   @l = <a b c d> Z~ 1, 2, *;   # OUTPUT: «<a1 b2 c2 d2>»

如果没有给出后缀运算符,则默认使用 ,(逗号运算符):

my @l = 1 Z 2;  # OUTPUT: «[(1 2)]»

56.12. 序列运算符

序列元操作符 S,将抑制优化器所做的任何并发或重排序。支持大多数简单的中缀运算符。

say so 1 S& 2 S& 3;  # OUTPUT: «True␤»

56.13. 元运算符的嵌套

为了避免在链式元运算符时产生歧义,请使用方括号来帮助编译器理解你。

my @a = 1, 2, 3;
my @b = 5, 6, 7;
@a X[+=] @b;
say @a;         # OUTPUT: «[19 20 21]␤»

56.14. Term 优先级

56.14.1. term < >

引号单词结构将内容在空白处打散,并返回一个单词的列表。如果一个单词看起来像一个数字字面量或一个 Pair 字面量,它将被转换为相应的数字。

say <a b c>[1]  # b

56.14.2. term ( )

分组运算符。

空的分组 () 创建一个空列表。非空表达式周围的圆括号只是构建了表达式, 而没有额外的语义。

在参数列表中,在参数周围放上圆括号防止了参数被解释为具名参数。

multi sub p(:$a!) { say 'named'      }
multi sub p($a)   { say 'positional' }
p a => 1;       # named
p (a => 1);     # positional

56.14.3. term { }

BlockHash 构造器。

如果`{}` 里面的内容看起来像一组 pairs 并且没有 $_ 或其它占位符参数,就返回一个散列, 这个散列由逐项逐项的 pair 组成。

否则就返回一个 Block。

注意,这个结构没有重新解析内容; 而里面的内容总是被解析为一组句子(例如,像一个 block), 并且如果后面的分析表明它需要被解析成一个散列, 那么 block 就会被执行并强转为散列。

56.15. 环缀 [ ]

数组构造器。在列表上下文中返回一个不会展平的 item 化的数组。

56.16. 方法后缀优先级

56.16.1. 后环缀 [ ]

sub postcircumfix:<[ ]>(@container, **@index,
                        :$k, :$v, :$kv, :$p, :$exists, :$delete)

:$k 会创建一个 pair, 它是散列中的一个条目。 键是 k, 键值为 $kv。 所以, $k 等价于 k$k

访问 @container 中的一个或多个元素,即数组索引操作:

my @alphabet = 'a' .. 'z';
say @alphabet[0];                   #-> a
say @alphabet[1];                   #-> b
say @alphabet[*-1];                 #-> z
say @alphabet[100]:exists;          #-> False
say @alphabet[15, 4, 17, 11].join;  #-> perl
say @alphabet[23 .. *].perl;        #-> ("x", "y", "z")

@alphabet[1, 2] = "B", "C";
say @alphabet[0..3].perl            #-> ("a", "B", "C", "d")

查看 Subscripts 获取关于该操作符行为的更详细的解释, 还有怎么在自定义类型中实现对它的支持。

56.16.2. 后环缀 { }

sub postcircumfix:<{ }>(%container, **@key,
                        :$k, :$v, :$kv, :$p, :$exists, :$delete)

访问 %container 的一个或多个元素, 即散列索引操作:

my  %color = kiwi => "green", banana => "yellow", cherry => "red";
say %color{"banana"};               #-> yellow
say %color{"cherry", "kiwi"}.perl;  #-> ("red", "green")
say %color{"strawberry"}:exists;    #-> False

%color{"banana", "lime"} = "yellowish", "green";
%color{"cherry"}:delete;
say %color;  #-> banana => yellowish, kiwi => green, lime => green

查看 后环缀 < >后环缀 « » 作为便捷形式, 查看 Subscripts 获取这个操作符行为的更详细解释, 还有怎么在自定义类型中实现对它的支持。

56.16.3. 后环缀 < >

后环缀 { } 的简写形式, 它会引起它的参数。

my %color = kiwi => "green", banana => "yellow", cherry => "red";
say %color<banana>;             #-> yellow
say %color<cherry kiwi>.perl;   #-> ("red", "green")
say %color<strawberry>:exists;  #-> False

这不是一个真正的操作符, 它仅仅是一个在编译时把 < > 变成 {} 后环缀操作符的语法糖。

56.16.4. 后环缀 « »

后环缀 { } 的简写形式。它会引起它的参数,并且 « » 中能进行变量插值。

my %color = kiwi => "green", banana => "yellow", cherry => "red";
my $fruit = "kiwi";
say %color«cherry $fruit».perl;   #-> ("red", "green")

这不是一个真正的操作符, 它仅仅是一个在编译时把 « » 变成 {} 后环缀操作符的语法糖。

56.16.5. 后环缀 ( )

调用操作符。把调用者当作 Callable 并引用它,它使用圆括号之间的表达式作为参数。

注意,标识符后面直接跟着一对儿圆括号总是被解析为子例程调用。

如果你想要你的对象响应该调用操作符, 你需要实现 CALL-ME 方法。

56.16.6. postfix .

该操作符用于调用一个方法, $invocant.method

技术上讲, 这不是一个操作符,而是编译器中特殊情况下的语法。

56.16.7. postfix .=

可变的方法调用。 $invocant.=method , 脱去语法糖后就是 $invocant = $invocant.method , 这与 http://doc.raku.org/routine/op%3D 类似。

技术上讲, 这不是一个操作符,而是编译器中特殊情况下的语法。

56.16.8. postfix .^

元方法调用。 ` $invocant.^method` 在 $invocant 的元类身上调用方法。脱去语法糖后, 它就是 $invocant.HOW.method($invocant, …​) 。 查看 HOW 获取更多信息。

技术上讲, 这不是一个操作符,而是编译器中特殊情况下的语法。

56.16.9. postfix .?

有可能被调用`的方法调用。如果有名为 `method 的方法, $invocant.?method 就在 $invocant 上调用 method 方法。否则它就返回 Nil

技术上讲, 这不是一个操作符,而是编译器中特殊情况下的语法。

56.16.10. postfix .+

$invocant.+method ` 从 `$invocant 身上调用所有叫做 method 的方法, 并返回一个 Parcel 作为结果。 如果没有找到这个名字的方法, 就会死掉。

技术上讲, 这不是一个操作符,而是编译器中特殊情况下的语法。

56.16.11. postfix .*

$invocant.*method ` 从 $invocant 身上调用所有叫做 method 的方法, 并返回一个 Parcel 作为结果。 如果没有找到这个名字的方法,则返回一个空的 Parcel。

技术上讲, 这不是一个操作符,而是编译器中特殊情况下的语法。

56.16.12. postfix .postfix

大多数情况下, 可以在后缀或后环缀前面放上一个点:

@a[1, 2, 3];
@a.[1, 2, 3]; # Same

这对于视觉清晰或简洁很有帮助。例如,如果对象的属性是一个函数,在属性名后面放置一对儿圆括号会变成方法调用的一部分。 所以要么使用两对儿圆括号, 要么在圆括号前面放上一个点来阻止方法调用。

class Operation {
    has $.symbol;
    has &.function;
}
my $addition = Operation.new(:symbol<+>, :function{ $^a + $^b });
say $addition.function()(1, 2); # 3

或者

say $addition.function.(1,2); # 3
然而,如果后缀是一个标识符, 那么它会被解释为一个普通的方法调用。
1.i # No such method 'i' for invocant of type 'Int'

技术上讲, 这不是一个操作符,而是编译器中特殊情况下的语法。

56.16.13. postfix .:<prefix>

前缀能够像方法那样, 使用冒号对儿标记法来调用。例如:

my $a = 1;
say ++$a;     # 2
say $a.:<++>; # 3

技术上讲, 这不是一个操作符,而是编译器中特殊情况下的语法。

56.16.14. postfix .::

一个类限定的方法调用, 用于调用一个定义在父类或 role 中的方法, 甚至在子类中重新定义了之后。

class Bar {
    method baz { 42 }
}

class Foo is Bar {
    method baz { "nope" }
}
say Foo.Bar::baz; # 42

56.17. 自增优先级

56.17.1. prefix ++

multi sub prefix:<++>($x is rw) is assoc<none>

把它的参数增加 1, 并返回增加后的值。

my $x = 3;
say ++$x;    # 4
say $x;      # 4

它的工作原理是在它的参数身上调用 succ 方法, 这可以让自定义类型自由地实现它们自己的增量语义。

56.17.2. prefix — 

multi sub prefix:<-->($x is rw) is assoc<none>

把它的参数减少 1, 并返回减少后的值。

my $x = 3;
say --$x;       # 2
say $x;         # 2

它的工作原理是在它的参数身上调用 pred 方法, 这可以让自定义类型自由地实现它们自己的减量语义。

56.17.3. postfix ++

multi sub postfix:<++>($x is rw) is assoc<none>

把它的参数增加 1, 并返回`unincremented`的那个值。

my $x = 3;
say $x++;       # 3
say $x;         # 4

它的工作原理是在它的参数身上调用 succ 方法, 这可以让自定义类型自由地实现它们自己的增量语义。

注意这并不一定返回它的参数。 例如,对于未定义的值, 它返回 0:

my $x;
say $x++;       # 0
say $x;         # 1

56.17.4. postfix — 

multi sub postfix:<-->($x is rw) is assoc<none>

把它的参数减少 1, 并返回`undecremented`的那个值。

my $x = 3;
say $x--;       # 3
say $x;         # 2

它的工作原理是在它的参数身上调用 pred 方法, 这可以让自定义类型自由地实现它们自己的减量语义。

注意这并不一定返回它的参数。 例如,对于未定义的值, 它返回 0:

my $x;
say $x--;       # 0
say $x;         # -1

56.18. 求幂优先级

56.18.1. infix **

multi sub infix:<**>(Any, Any) returns Numeric:D is assoc<right>

求幂操作符把它的两个参数都强制转为 Numeric , 然后计算,右侧为幂。

如果 ** 右边是一个非负整数,并且左侧是任意精度类型(Int, FatRat),那么计算不会损失精度。

56.19. 象形一元操作符的优先级

56.19.1. prefix ?

multi sub prefix:<?>(Mu) returns Bool:D

布尔上下文操作符。

通过在参数身上调用 Bool 方法强制它的参数为 Bool。注意, 这会使 Junctions 失效。

56.19.2. prefix !

multi sub prefix:<!>(Mu) returns Bool:D

否定的布尔上下文操作符。

通过在参数身上调用 Bool 方法强制它的参数为 Bool, 并返回结果的否定值。注意, 这会使 Junctions 失效。

56.19.3. prefix

multi sub prefix:<+>(Any) returns Numeric:D

Numeric 上下文操作符。

通过在参数身上调用 Numeric 方法强制将参数转为 Numeric 类型。

56.19.4. prefix -

multi sub prefix:<->(Any) returns Numeric:D

否定的 Numeric 上下文操作符。

通过在参数身上调用 Numeric 方法强制将参数转为 Numeric 类型, 并返回结果的否定值。

56.19.5. prefix ~

multi sub prefix:<->(Any) returns Str:D

字符串上下文操作符。

通过在参数身上调用 Str 方法强制把参数转为 Str 类型。

56.19.6. prefix |

将 Capture, Enum, Pair, List, Parcel, EnumMap 和 Hash 展平到参数列表中。

(在 Rakudo 中,这不是作为一个合适的操作符来实现的,而是编译器中的一种特殊情况, 这意味着它只对参数列表有效,而非在任意代码中都有效。)

56.19.7. prefix +^

multi sub prefix:<+^>(Any) returns Int:D

Integer bitwise negation

整数按位取反。

将参数强转为 Int 类型并对结果按位取反, 假设两者互补。

56.19.8. prefix ?^

multi sub prefix:<?^>(Mu) returns Bool:D

布尔按位取反。

将参数强转为 Bool, 然后按位反转,这使它和 prefix:<!> 一样。

56.19.9. prefix ^

multi sub prefix:<^>(Any) returns Range:D

upto 操作符.

强制把它的参数转为 Numeric, 生成一个从 0 直到(但是排除) 参数为止的范围。

say ^5;         # 0..^5
for ^5 { }      # 5 iterations

56.20. 乘法优先级

56.20.1. infix *

multi sub infix:<*>(Any, Any) returns Numeric:D

把两边的参数都强转为 Numeric 并把它们相乘。 结果是一个更宽的类型。 查看 Numeric 获取更详细信息。

56.20.2. infix /

multi sub infix:</>(Any, Any) returns Numeric:D

把两边的参数都强制为 Numeric, 并用左边除以右边的数。整数相除返回 Rat, 否则返回"更宽类型” 的结果。

56.20.3. infix div

multi sub infix:<div>(Int:D, Int:D) returns Int:D

整除。向下取整。

56.20.4. infix %

multi sub infix:<%>($x, $y) return Numeric:D

模操作符。首先强制为 Numeric。

通常,下面的等式是成立的:

$x % $y == $x - floor($x / $y) * $y

56.20.5. infix %%

multi sub infix:<%%>($a, $b) returns Bool:D

整除操作符, 如果 ` $a % $b == 0` 则返回 True.

56.20.6. infix mod

multi sub infix:<mod>(Int:D $a, Int:D $b) returns Int:D

整数取模操作符。 返回整数取模操作的剩余部分。

56.20.7. infix +&

multi sub infix:<+&>($a, $b) returns Int:D

Numeric 按位 AND。 把两个参数都强转为 Int 并执行按位 AND 操作,假定两者是互补的。

56.20.8. infix +<

multi sub infix:<< +< >>($a, $b) returns Int:D

向左移动整数个位。

56.20.9. infix +>

multi sub infix:<< +> >>($a, $b) returns Int:D

向右移动整数个位。

56.20.10. infix gcd

multi sub infix:<gcd>($a, $b) returns Int:D

强制两个参数都为 Int 并返回最大公分母(greatest common denominator)。

56.20.11. infix lcm

multi sub infix:<lcm>($a, $b) returns Int:D

强制两个参数为 Int 并返回最小公倍数(least common multiple)

56.21. 加法优先级

56.21.1. infix

multi sub infix:<+>($a, $b) returns Numeric:D

强制两个参数为 Numeric 并把它们相加。

56.21.2. infix -

multi sub infix:<->($a, $b) returns Numeric:D

强制两个参数为 Numeric 并用第一个参数减去第二个参数。

56.21.3. infix +|

multi sub infix:<+|>($a, $b) returns Int:D

强制两个参数为 Int 并执行按位 OR(包括 OR)

56.21.4. infix +^

multi sub infix:<+^>($a, $b) returns Int:D

强制两个参数为 Int 并执行按位 XOR(不包括 OR)

56.21.5. infix ?|

multi sub infix:<?|>($a, $b) returns Bool:D

强制两个参数为 Bool 并执行逻辑 OR(不包括 OR)

56.22. 重复操作符优先级

56.22.1. infix x

proto sub infix:<x>(Any, Any) returns Str:D
multi sub infix:<x>(Any, Any)
multi sub infix:<x>(Str:D, Int:D)

$a 强转为 Str , 把 $b 强转为 Int, 并重复字符串 $b 次。 如果 $b ⇐ 0 则返回空字符串。

say 'ab' x 3;       # ababab
say 42 x 3;         # 424242

56.22.2. infix xx

multi sub infix:<xx>($a, $b) returns List:D

返回一组重复的 $a 并计算 $b 次($b 被强转为 Int)。 如果 $b ⇐ 0 ,则返回一个空列表。

每次重复都会计算左侧的值, 所以

[1, 2] xx 5

返回 5 个不同的数组(但是每次都是相同的内容)并且

rand xx 3

返回 3 个独立的伪随机数。右侧可以是 *, 这时会返回一个惰性的,无限的列表。

56.23. 连结

56.23.1. infix ~

proto sub infix:<~>(Any, Any) returns Str:D
multi sub infix:<~>(Any,   Any)
multi sub infix:<~>(Str:D, Str:D)

强制两个参数为 Str 并连结它们。

say 'ab' ~ 'c';     # abc

56.24. Junctive AND (all) 优先级

56.24.1. infix &

multi sub infix:<&>($a, $b) returns Junction:D is assoc<list>

用它的参数创建一个 all Junction。查看 Junctions 获取更多详情。

56.25. Junctive OR (any) Precedence

56.25.1. infix |

multi sub infix:<|>($a, $b) returns Junction:D is assoc<list>

用它的参数创建一个 any Junction。查看 Junctions 获取更多详情。

56.25.2. infix ^

multi sub infix:<^>($a, $b) returns Junction:D is assoc<list>

用它的参数创建一个 one Junction。查看 Junctions 获取更多详情。

56.26. Named Unary Precedence

56.26.1. prefix temp

sub prefix:<temp>(Mu $a is rw)

temporizes 传入的变量作为参数, 这意味着退出作用域后它被重置为旧值。(这和 Perl 5 中的 local 操作符类似, 除了 temp 不重置值之外。)

56.26.2. prefix let

sub prefix:<let>(Mu $a is rw)

假定重置:如果通过异常或 fail()退出当前作用域, 旧值就会被恢复。

56.27. Nonchaining Binary Precedence

56.27.1. infix does

sub infix:<does>(Mu $obj, Mu $role) is assoc<none>

在运行时把 $role 混合进 $obj 中。 要求 $obj 是可变的。

参数 $role 不一定要求是一个 role, 它可以表现的像是一个 role, 例如枚举值。

56.27.2. infix but

sub infix:<but>(Mu $obj, Mu $role) is assoc<none>

$role 混合进 $obj 并创建一个 $obj 的副本。因为 $obj 是不能修改的,但是能使用 mixins 用于创建不可变值。

参数 $role 不一定要求是一个 role, 它可以表现的像是一个 role, 例如枚举值。

56.27.3. infix cmp

proto sub infix:<cmp>(Any, Any) returns Order:D is assoc<none>
multi sub infix:<cmp>(Any,       Any)
multi sub infix:<cmp>(Real:D,    Real:D)
multi sub infix:<cmp>(Str:D,     Str:D)
multi sub infix:<cmp>(Enum:D,    Enum:D)
multi sub infix:<cmp>(Version:D, Version:D)

一般的, “智能的” 三路比较器。

比较字符串时使用字符串语义, 比较数字时使用数字语义, 比较 Pair 对象时, 先比较键, 再比较值,等等。

if $a eqv $b, then $a cmp $b always returns Order::Same.
say (a => 3) cmp (a => 4);      # Less
say 4        cmp 4.0;           # Same
say 'b'      cmp 'a';           # More

56.27.4. infix leg

proto sub infix:<leg>($a, $b) returns Order:D is assoc<none>
multi sub infix:<leg>(Any,   Any)
multi sub infix:<leg>(Str:D, Str:D)

字符串三路比较器。 leg 是 less, equal 还有 greater 的简写形式?

把两个参数都强转为 Str , 然后按照字母次序比较。

say 'a' leg 'b';        Less
say 'a' leg 'a';        Same
say 'b' leg 'a';        More

56.27.5. infix <=>

multi sub infix:«<=>»($a, $b) returns Order:D is assoc<none>

Numeric 三路比较器。

把两个参数强转为 Real, 并执行数值比较。

56.27.6. infix ..

multi sub infix:<..>($a, $b) returns Range:D is assoc<none>

由参数创建一个 Range。

56.27.7. infix ..^

multi sub infix:<..^>($a, $b) returns Range:D is assoc<none>

由参数创建一个 Range, 不包含末端。

56.27.8. infix ^..

multi sub infix:<^..>($a, $b) returns Range:D is assoc<none>

由参数创建一个 Range, 不包含开始端点。

56.27.9. infix ..

multi sub infix:<^..^>($a, $b) returns Range:D is assoc<none>

由参数创建一个 Range, 不包含开端和末端。

56.28. Chaining Binary Precedence

56.28.1. infix ==

proto sub infix:<==>($, $) returns Bool:D is assoc:<chain>
multi sub infix:<==>(Any, Any)
multi sub infix:<==>(Int:D, Int:D)
multi sub infix:<==>(Num:D, Num:D)
multi sub infix:<==>(Rational:D, Rational:D)
multi sub infix:<==>(Real:D, Real:D)
multi sub infix:<==>(Complex:D, Complex:D)
multi sub infix:<==>(Numeric:D, Numeric:D)

强转两个参数为 Numeric(如果必要), 并返回 True 如果它们相等。

56.28.2. infix !=

proto sub infix:<!=>(Mu, Mu) returns Bool:D is assoc<chain>

强转两个参数为 Numeric(如果必要), 并返回 True 如果它们不相等。

56.28.3. infix <

proto sub infix:«<»(Any, Any) returns Bool:D is assoc<chain>
multi sub infix:«<»(Int:D, Int:D)
multi sub infix:«<»(Num:D, Num:D)
multi sub infix:«<»(Real:D, Real:D)

强转两个参数为 Real (如果必要), 并返回 True 如果第一个参数小于第二个参数。

56.28.4. infix <=

proto sub infix:«<=»(Any, Any) returns Bool:D is assoc<chain>
multi sub infix:«<=»(Int:D, Int:D)
multi sub infix:«<=»(Num:D, Num:D)
multi sub infix:«<=»(Real:D, Real:D)

强转两个参数为 Real (如果必要), 并返回 True 如果第一个参数小于第二个参数。

56.28.5. infix >

proto sub infix:«>»(Any, Any) returns Bool:D is assoc<chain>
multi sub infix:«>»(Int:D, Int:D)
multi sub infix:«>»(Num:D, Num:D)
multi sub infix:«>»(Real:D, Real:D)

强转两个参数为 Real (如果必要), 并返回 True 如果第一个参数大于第二个参数。

56.28.6. infix >=

proto sub infix:«>=»(Any, Any) returns Bool:D is assoc<chain>
multi sub infix:«>=»(Int:D, Int:D)
multi sub infix:«>=»(Num:D, Num:D)
multi sub infix:«>=»(Real:D, Real:D)

强转两个参数为 Real (如果必要), 并返回 True 如果第一个参数大于或等于第二个参数。

56.28.7. infix eq

proto sub infix:<eq>(Any, Any) returns Bool:D is assoc<chain>
multi sub infix:<eq>(Any,   Any)
multi sub infix:<eq>(Str:D, Str:D)

强转两个参数为 Str(如果必要), 并返回 True 如果第一个参数等于第二个参数。

助记法: equal

56.28.8. infix ne

proto sub infix:<ne>(Mu, Mu) returns Bool:D is assoc<chain>
multi sub infix:<ne>(Mu,    Mu)
multi sub infix:<ne>(Str:D, Str:D)

强转两个参数为 Str(如果必要), 并返回 False 如果第一个参数等于第二个参数。

助记法: not equal

56.28.9. infix gt

proto sub infix:<gt>(Mu, Mu) returns Bool:D is assoc<chain>
multi sub infix:<gt>(Mu,    Mu)
multi sub infix:<gt>(Str:D, Str:D)

强转两个参数为 Str(如果必要), 并返回 True 如果第一个参数大于第二个参数。

助记法: greater than

56.28.10. infix ge

proto sub infix:<ge>(Mu, Mu) returns Bool:D is assoc<chain>
multi sub infix:<ge>(Mu,    Mu)
multi sub infix:<ge>(Str:D, Str:D)

强转两个参数为 Str(如果必要), 并返回 True 如果第一个参数大于第二个参数。

助记法: greater or equal

56.28.11. infix lt

proto sub infix:<lt>(Mu, Mu) returns Bool:D is assoc<chain>
multi sub infix:<lt>(Mu,    Mu)
multi sub infix:<lt>(Str:D, Str:D)

强转两个参数为 Str(如果必要), 并返回 True 如果第一个参数小于第二个参数。

助记法: less than

56.28.12. infix le

proto sub infix:<le>(Mu, Mu) returns Bool:D is assoc<chain>
multi sub infix:<le>(Mu,    Mu)
multi sub infix:<le>(Str:D, Str:D)

强转两个参数为 Str(如果必要), 并返回 True 如果第一个参数小于或等于第二个参数。

助记法: less or equal

56.28.13. infix before

proto sub infix:<before>(Any, Any) returns Bool:D is assoc<chain>
multi sub infix:<before>(Any,       Any)
multi sub infix:<before>(Real:D,    Real:D)
multi sub infix:<before>(Str:D,     Str:D)
multi sub infix:<before>(Enum:D,    Enum:D)
multi sub infix:<before>(Version:D, Version:D)

一般的排序, 使用和 cmp 相同的语义。 如果第一个参数小于第二个参数则返回 True。

56.28.14. infix after

proto sub infix:<after>(Any, Any) returns Bool:D is assoc<chain>
multi sub infix:<after>(Any,       Any)
multi sub infix:<after>(Real:D,    Real:D)
multi sub infix:<after>(Str:D,     Str:D)
multi sub infix:<after>(Enum:D,    Enum:D)
multi sub infix:<after>(Version:D, Version:D)

一般的排序, 使用和 cmp 相同的语义。 如果第一个参数大于第二个参数则返回 True。

56.28.15. infix eqv

proto sub infix:<eqv>(Any, Any) returns Bool:D is assoc<chain>
proto sub infix:<eqv>(Any, Any)

等值操作符。如果两个参数在结构上相同就返回 True。例如, 相同类型(并且递归)包含相同的值。

say [1, 2, 3] eqv [1, 2, 3];        # True
say Any eqv Any;                    # True
say 1 eqv 2;                        # False
say 1 eqv 1.0;                      # False

对于任意对象使用默认的 eqv 操作是不可能的。例如, eqv 不认为同一对象的两个实例在结构上是相等的:

class A {
    has $.a;
}

say A.new(a => 5) eqv A.new(a => 5);  #=> False

要得到这个类的对象相等(eqv)语义, 需要实现一个合适的中缀 eqv 操作符:

class A {
    has $.a;
}

multi infix:<eqv>(A $l, A $r) { $l.a eqv $r.a }
say A.new(a => 5) eqv A.new(a => 5);  #=> True

56.28.16. infix ===

proto sub infix:<===>(Any, Any) returns Bool:D is assoc<chain>
proto sub infix:<===>(Any, Any)

值相等。如果两个参数都是同一个对象则返回 True。

class A { };

my $a = A.new;
say $a === $a;              # True
say A.new === A.new;        # False
say A === A;                # True

对于值的类型, === 表现的和 eqv 一样:

say 'a' === 'a';            # True
say 'a' === 'b';            # False

# different types
say 1 === 1.0;              # False

=== 使用 WHICH 方法来获取对象相等, 所以所有的值类型必须重写方法 WHICH

56.28.17. infix =:=

proto sub infix:<=:=>(Mu \a, Mu \b) returns Bool:D is assoc<chain>
multi sub infix:<=:=>(Mu \a, Mu \b)

容器相等。返回 True 如果两个参数都绑定到同一个容器上。 如果它返回 True, 那通常意味着修改一个参数也会同时修改另外一个。

my ($a, $b) = (1, 3);
say $a =:= $b;      # False
$b = 2;
say $a;             # 1
$b := $a;
say $a =:= $b;      # True
$a = 5;
say $b;             # 5

56.28.18. infix ~~

智能匹配操作符。把左侧参数起别名为 $ , 然后计算右侧的值, 并在它身上调用 .ACCEPTS($) 。 匹配的语义由右侧操作数的类型决定。

这儿有一个内建智能匹配函数的摘要:

右侧      比较语义
Mu:U	  类型检查
Str	      字符串相等
Numeric	  数值相等
Regex	  正则匹配
Callable  调用的布尔结果
Any:D	  对象相等

56.29. Tight AND Precedence

56.29.1. infix &&

在布尔上下文中返回第一个求值为 False 的参数, 否则返回最后一个参数。

注意这是短路操作符,如果其中的一个参数计算为 false 值, 那么该参数右侧的值绝不会被计算。

sub a { 1 }
sub b { 0 }
sub c { die "never called" };
say a() && b() && c();      # 0

56.30. Tight OR Precedence

56.30.1. infix ||

在布尔上下文中返回第一个求值为 True 的参数, 否则返回最后一个参数。

注意这是短路操作符,如果其中的一个参数计算为 true 值, 那么该参数右侧的值绝不会被计算。

sub a { 0 }
sub b { 1 }
sub c { die "never called" };
say a() || b() || c();      # 1

56.30.2. infix ^^

返回第一个值为 true 的参数如果只有一个的话, 否则返回 Nil。只要找到两个值为 true 的参数就发生短路。

say 0 ^^ 42;                # 42
say 0 ^^ 42 ^^ 1 ^^ die 8;  # (empty line)

注意, 这个操作符的语义可能不是你假想的那样: infix ^^ 翻到它找到的第一个 true 值, 找到第二个 true 值后永远地反转为 Nil 值, 不管还有多少 true 值。(换句话说,它的语义是”找到一个真值”, 而不是布尔起奇偶校验语义)

56.30.3. infix //

Defined-or 操作符。返回第一个定义了的操作数, 否则返回最后一个操作数。短路操作符。

say Any // 0 // 42;         # 0

56.30.4. infix min

返回参数的最小值。语义由 cmp 语义决定。

$foo min= 0  # read as: $foo decreases to 0

56.30.5. infix max

返回参数的最大值。

$foo max= 0  # read as: $foo increases to 0

56.31. Conditional Operator Precedence

56.31.1. infix ?? !!

三目操作符, 条件操作符。

$condition ?? $true !! $false 计算并返回 $true 表达式,如果 $condition 为真的话。 否则计算并返回 $false 分支。

56.31.2. infix ff

sub infix:<ff>(Mu $a, Mu $b)

Flipflop operator. 触发器操作符。

把两个参数都跟 $ 进行比较(即,$ ~~ $a$_ ~~ $b)。求值为 False 直到左侧的智能匹配为真, 这时,它求值为真, 直到右侧的智能匹配为真。

实际上,左边的参数是"开始”条件, 右侧的参数是”停止” 条件。 这种结构一般用于收集只在特定区域的行。 例如:

my $excerpt = q:to/END/;
Here's some unimportant text.
=begin code
    This code block is what we're after.
    We'll use 'ff' to get it.
=end code
More unimportant text.
END

my @codelines = gather for $excerpt.lines {
    take $_ if "=begin code" ff "=end code"
}

# this will print four lines,
# starting with "=begin code" and ending with "=end code"
say @codelines.join("\n");

匹配开始条件之后,操作符会继续将停止条件与 $_ 进行匹配, 如果成功就做相应地表现。在这个例子中, 只有第一个元素被打印了:

for <AB C D B E F> {
    say $_ if /A/ ff /B/;  # prints only "AB"
}

如果你想测试开始条件, 并且没有结束条件, * 能用作 “停止” 条件。

for <A B C D E> {
    say $_ if /C/ ff *; # prints C, D, and E
}

对于 sed-like 版本, 在开始条件匹配成功之后,它不会使用停止条件与 $_ 进行匹配。

这个操作符不能被重载, 因为它被编译器特殊处理过。

56.31.3. infix ^ff

sub infix:<^ff>(Mu $a, Mu $b)

像 ff 那样工作,除了它不会在条目匹配开始条件时返回真。(包括匹配停止条件的条目)

一个比较:

my @list = <A B C>;
say $_ if /A/ ff /C/ for @list;  # prints A, B, and C
say $_ if /A/ ^ff /C/ for @list; # prints B and C

sed-like 版本 可以在 ^fff 中找到.

这个操作符不能被重载, 因为它被编译器特殊处理过。

56.31.4. infix ff^

sub infix:<ff^>(Mu $a, Mu $b)

像 ff 那样工作,除了它不会在条目匹配停止条件时返回真。(包括第一次匹配开始条件的条目)

my @list = <A B C>;
say $_ if /A/ ff /C/ for @list;  # prints A, B, and C
say $_ if /A/ ff^ /C/ for @list; # prints A and B

sed-like 版本 可以在 fff^ 中找到.

这个操作符不能被重载, 因为它被编译器特殊处理过。

56.31.5. infix ff

sub infix:<^ff^>(Mu $a, Mu $b)

像 ff 那样工作,除了它不会在条目匹配停止条件时返回真, 也不会在条目匹配开始时返回真。(或者两者)

my @list = <A B C>;
say $_ if /A/ ff /C/ for @list;  # prints A, B, and C
say $_ if /A/ ^ff^ /C/ for @list; # prints B

sed-like 版本 可以在 fff 中找到.

这个操作符不能被重载, 因为它被编译器特殊处理过。

56.31.6. infix fff

sub infix:<fff>(Mu $a, Mu $b)

执行 sed-like 那样的 flipflop 操作,在其中,它返回 False 直到左侧的参数与 $ 智能匹配, 并且在那之后返回 True 直到右侧的参数和 $ 智能匹配。

像 ff 那样工作, 除了它每次调用只尝试一个参数之外。即, 如果 $ 和左侧的参数智能匹配, fff 随后不会尝试将同一个 $ 和右侧的参数进行匹配。

for <AB C D B E F> {
    say $_ if /A/ fff /B/;  # Prints "AB", "C", "D", and "B"
}

对于 non-sed-like 版本, 查看 ff.

这个操作符不能被重载, 因为它被编译器特殊处理过。

56.31.7. infix ^fff

sub infix:<^fff>(Mu $a, Mu $b)

像 fff那样, 除了它对于左侧的匹配不返回真之外。

my @list = <A B C>;
say $_ if /A/ fff /C/ for @list;  # prints A, B, and C
say $_ if /A/ ^fff /C/ for @list; # prints B and C

对于 non-sed 版本, 查看 ^ff.

这个操作符不能被重载, 因为它被编译器特殊处理过。

56.31.8. infix fff^

sub infix:<fff^>(Mu $a, Mu $b)

像 fff 那样, 除了它对于右侧的匹配不返回真之外。

my @list = <A B C>;
say $_ if /A/ fff /C/ for @list;  # prints A, B, and C
say $_ if /A/ fff^ /C/ for @list; # prints A and B

对于 non-sed 版本, 查看 ff^.

这个操作符不能被重载, 因为它被编译器特殊处理过。

56.31.9. infix fff

sub infix:<^fff^>(Mu $a, Mu $b)

像 fff 那样, 除了它对于左侧和右侧的匹配都不返回真之外。

my @list = <A B C>;
say $_ if /A/ fff /C/ for @list;  # prints A, B, and C
say $_ if /A/ ^fff^ /C/ for @list; # prints B

对于 non-sed 版本, 查看 ff.

这个操作符不能被重载, 因为它被编译器特殊处理过。

56.32. Item Assignment Precedence

56.32.1. infix =

sub infix:<=>(Mu $a is rw, Mu $b)

Item 赋值.

把 = 号右侧的值放入左侧的容器中。 它真正的语义是由左侧的容器类型决定的。

(注意 item 赋值和列表赋值的优先级级别不同, 并且等号左侧的语法决定了等号是被解析为 item 赋值还是列表赋值操作符)。

56.32.2. infix ⇒

sub infix:«=>»($key, Mu $value) returns Pair:D

Pair 构造器.

使用左侧值作为键, 右侧值作为值,构造一个 Pair 对象。

注意 操作符是语法上的特例,在这个结构中, 它允许左侧是一个未被引起的标识符。

my $p = a => 1;
say $p.key;         # a
say $p.value;       # 1

在参数列表中,在 左侧使用未被引起的标识符构建的 Pair 会被解释为一个具名参数。

查看 Terms 语言文档了解更多创建 Pair 对象的方式。

56.33. Loose Unary Precedence

56.33.1. prefix not

multi sub prefix:<not>(Mu $x) returns Bool:D

在布尔上下文中计算它的参数(因此使 Junctions 失效), 并返回否定的结果。

56.33.2. prefix so

multi sub prefix:<so>(Mu $x) returns Bool:D

在布尔上下文中计算它的参数(因此使 Junctions 失效), 并返回结果。

56.34. 逗号操作符优先级

56.34.1. infix ,

sub infix:<,>(*@a) is assoc<list> returns Parcel:D

从它的参数宏构建一个 Parcel。也在语法构成上用作函数用的参数的分隔符。

56.34.2. infix :

就像中缀操作符 , 那样, : 用作参数分隔符, 并把它左侧的参数标记为调用者。

那会把函数调用转为方法调用。

substr('abc': 1);       # same as 'abc'.substr(1)

Infix : 只允许出现在非方法调用的第一个参数后面。 在其它位置它会是语法错误。

56.35. List Infix Precedence

56.35.1. infix Z

sub infix:<Z>(**@lists) returns List:D is assoc<chain>

Zip operator.

Z 像一个拉链那样把列表插入进来, 只要第一个输入列表耗尽就停止:

say (1, 2 Z <a b c> Z <+ ->).perl;  # ((1, "a", "+"), (2, "b", "-")).list

Z 操作符也作为元操作符存在, 此时内部的 parcels 被应用了元操作符的列表替换:

say 100, 200 Z+ 42, 23;             # 142, 223
say 1..3 Z~ <a b c> Z~ 'x' xx 3;    # 1ax 2bx 3cx

56.35.2. infix X

sub infix:<X>(**@lists) returns List:D is assoc<chain>

从所有列表创建一个外积。最右边的元素变化得最迅速。

1..3 X <a b c> X 9

# produces (1, 'a', 9), (1, 'b', 9), (1, 'c', 9),
#          (2, 'a', 9), (2, 'b', 9), (2, 'c', 9),
#          (3, 'a', 9), (3, 'b', 9), (3, 'c', 9)

X 操作符也可以作为元操作符, 此时内部的 parcels 被应用了元操作符的列表的值替换:

1..3 X~ <a b c> X~ 9

# produces '1a9', '1b9', '1c9',
#          '2a9', '2b9', '2c9',
#          '3a9', '3b9', '3c9'

56.35.3. infix …​

multi sub infix:<...>(**@) is assoc<list>
multi sub infix:<...^>(**@) is assoc<list>

序列操作符是一个用于产生惰性列表的普通操作符。

它可以有一个初始元素和一个生成器在 的左侧, 在右侧是一个端点。

序列操作符会使用尽可能多的参数来调用生成器。参数会从初始元素和已生成元素中获取。

默认的生成器是 .succ.pred , 取决于末端怎么比较:

say 1 ... 4;        # 1 2 3 4
say 4 ... 1;        # 4 3 2 1
say 'a' ... 'e';    # a b c d e
say 'e' ... 'a';    # e d c b a

(Whatever) 末端生成一个无限序列,使用的是默认的生成器 .succ

say (1 ... *)[^5];  # 1 2 3 4 5

自定义生成器是在 操作符之前的最后一个参数。下面这个自定义生成器接收两个参数, 生成了斐波纳契数。

say (1, 1, -> $a, $b { $a + $b } ... *)[^8];    # 1 1 2 3 5 8 13 21
# same but shorter
say (1, 1, *+* ... *)[^8];                      # 1 1 2 3 5 8 13 21

当然自定义生成器也能只接收一个参数。

say 5, { $_ * 2 } ... 40;                       # 5 10 20 40

生成器的参数个数至少要和初始元素的个数一样多。

如果没有生成器,并且有不止一个初始元素,所有的初始元素都是数值,那么序列操作符会尝试推导出生成器。它知道数学和几何序列。

say 2, 4, 6 ... 12;     # 2 4 6 8 10 12
say 1, 2, 4 ... 32;     # 1 2 4 8 16 32

如果末端不是 *, 它会和每个生成的元素进行智能匹配,当智能匹配成功的时候序列就被终止。对于 …​ 操作符, 会包含最后一个元素, 对于 …​^ 操作符,会排除最后的那个元素。

这允许你这样写:

say 1, 1, *+* ...^ *>= 100;

来生成所有直到 100 但不包括 100 的斐波纳契数。

…​ 操作符还会把初始值看作”已生成的元素”,所以它们也会对末端进行检查:

my $end = 4;
say 1, 2, 4, 8, 16 ... $end;
# outputs 1 2 4

56.36. List Prefix Precedence

56.36.1. infix =

列表赋值。它真正的语义是由左侧的容器类型决定的。查看 Array 和 Hash 获取普通案例。

item 赋值和列表赋值的优先级级别不同, 并且等号左侧的语法决定了等号是被解析为 item 赋值还是列表赋值操作符。

56.36.2. infix :=

绑定。 而 $x = $y 是把 $y 中的值放到 $x 里面, $x := $y 会让 $x$y 引用同一个值。

my $a = 42;
my $b = $a;
$b++;
say $a;

这会输出 42, 因为 $a 和 $b 都包含了数字 42, 但是容器是不同的。

my $a = 42;
my $b := $a;
$b++;
say $a;

这会打印 43, 因为 $b$a 都代表着`同一个对象`。

56.36.3. infix ::=

只读绑定. 查看 infix :=.

56.36.4. listop …​

这是 yada, yada, yada 操作符 或 stub 操作符。如果它在子例程或类型中是唯一的语句,它会把子例程或类型标记为 stub(这在预声明类型和组成 roles 上下文中是有意义的)

如果 …​ 语句被执行了, 它会调用 &fail , 伴随着默认的消息 stub 代码的执行。

56.36.5. listop !!!

如果它在子例程或类型中是唯一的语句,它会把子例程或类型标记为 stub(这在预声明类型和组成 roles 上下文中是有意义的)

如果 !!! 语句被执行了, 它会调用 &die , 伴随着默认的消息 stub 代码的执行。

56.36.6. listop ???

如果它在子例程或类型中是唯一的语句,它会把子例程或类型标记为 stub(这在预声明类型和组成 roles 上下文中是有意义的)

如果 ??? 语句被执行了, 它会调用 &warn , 伴随着默认的消息 stub 代码的执行。

56.37. Loose AND precedence

56.37.1. infix and

和中缀操作符 && 一样,除了优先级更宽松。

在布尔上下文中返回第一个求值为 False 的操作数, 否则返回最后一个操作数。短路操作符。

56.37.2. infix andthen

andthen 运算符在遇到第一个未定义的参数时返回,否则返回最后一个参数。最后一个参数按原样返回,根本不检查定义性。是短路运算符。左侧的结果被绑定到 $_ 中,用于右侧,如果右侧是一个 Callable,则作为参数传递,其计数必须为0或1。

这个操作符的一个方便用途是将一个例程的返回值别名为 $_,并对其进行额外的操作,如打印或返回给调用者。由于 andthen 运算符会短路,所以右边的语句不会被执行,除非左边的语句是有定义的(小贴士:从来没有定义过 Failure,所以你可以用这个运算符来处理)。

sub load-data {
    rand  > .5 or return; # simulated load data failure; return Nil
    (rand > .3 ?? 'error' !! 'good data') xx 10 # our loaded data
}
load-data.first: /good/ andthen say "$_ is good";
# OUTPUT: «(good data is good)␤»

load-data() andthen .return; # return loaded data, if it's defined
die "Failed to load data!!";

上面的例子只有当子程序返回任何匹配 /good/ 的项目时,才会打印出 good data is good,除非加载数据返回一个定义的值,否则就会死掉。别名行为让我们可以在运算符上跨管道取值。

andthen 操作符是 with 语句修饰符的近亲,有些编译器会把 with 编译成 andthen,这意味着这两行的行为是等价的:

.say with 42;
42 andthen .say;

56.37.3. infix notandthen

notandthen 操作符在遇到第一个定义的参数时返回,否则返回最后一个参数。最后一个参数按原样返回,根本不检查定义性。是短路运算符。左侧的结果被绑定到 $_ 的右侧,如果右侧是一个 Callable,则作为参数传递,其计数必须是0或1。

乍一看,notandthen 可能看起来和 orelse 运算符是一样的。区别很微妙:当遇到一个定义的项目(不是最后一个项目)时,notandthen 会返回 Empty,而 orelse 会返回那个项目。换句话说,notandthen 是在没有定义项时采取行动的一种手段,而 orelse 则是获得第一个定义项的手段。

sub all-sensors-down     { [notandthen] |@_, True             }
sub first-working-sensor { [orelse]     |@_, 'default sensor' }

all-sensors-down Nil, Nil, Nil
  and say 'OMG! All sensors are down!'; # OUTPUT:«OMG! All sensors are down!␤»
say first-working-sensor Nil, Nil, Nil; # OUTPUT:«default sensor␤»

all-sensors-down Nil, 42, Nil
  and say 'OMG! All sensors are down!'; # No output
say first-working-sensor Nil, 42, Nil;  # OUTPUT:«42␤»

notandthen 操作符是 without 语句修饰符的近亲,有些编译器将 without 编译成 notandthen,也就是说这两行具有等价的行为。

sub good-things { fail }

'boo'.say without good-things;
good-things() notandthen 'boo'.say;

56.38. Loose OR Precedence

56.38.1. infix or

和中缀操作符 || 一样,除了优先级更宽松。

在布尔上下文中返回第一个求值为 True 的参数, 否则返回最后一个参数。短路操作符。

56.38.2. infix orelse

orelse 操作符类似于 infix //,除了更宽松的优先级和 $_ 别名。

返回第一个定义的参数,否则返回最后一个参数。最后一个参数按原样返回,根本不检查定义性。是短路运算符。左侧的结果被绑定到 $_ 中,用于右侧,如果右侧是一个 Callable,则作为参数传递,其计数必须为0或1。

这个操作符对于处理例程返回的 Failure 很有用,因为预期值通常是被定义的,而 Failure 从来不是:

sub meows { ++$ < 4 ?? fail 'out of meows!' !! '🐱' }

sub meows-processor1 { meows() orelse .return } # return handled Failure
sub meows-processor2 { meows() orelse fail $_ } # return re-armed Failure
sub meows-processor3 {
    # Use non-Failure output, or else print a message that stuff's wrong
    meows() andthen .say orelse ‘something's wrong’.say;
}

say "{.^name}, {.handled}"  # OUTPUT: «Failure, True␤»
    given meows-processor1;
say "{.^name}, {.handled}"  # OUTPUT: «Failure, False␤»
    given meows-processor2;
meows-processor3;           # OUTPUT: «something's wrong␤»
meows-processor3;           # OUTPUT: «🐱␤»

56.38.3. infix xor

除了更宽松的优先级之外,与 infix ^ 相同。

如果且仅当另一个操作数在布尔上下文中评价为 False 时,返回在布尔上下文中评价为 True 的操作数。如果两个操作数都为 False,返回最后一个参数。如果两个操作数都为 True,则返回 Nil

当链式运算时,如果且仅当有一个这样的操作数时,才返回返回值为 True 的操作数。如果有一个以上的操作数为真,则在评估第二个操作数后短路,返回 Nil。如果所有操作数都为假,则返回最后一个操作数。

56.39. 序列器优先级

56.39.1. infix ==⇒

这个 feed 操作符从左边取结果,并将其作为最后一个参数传递给下一个(右边)例程。

my @array = (1, 2, 3, 4, 5);
@array ==> sum() ==> say();   # OUTPUT: «15␤»

上面这个简单的例子,就相当于写作:

my @array = (1, 2, 3, 4, 5);
say(sum(@array));             # OUTPUT: «15␤»

或者如果使用方法:

my @array = (1, 2, 3, 4, 5);
@array.sum.say;               # OUTPUT: «15␤»

优先级是非常宽松的,所以你需要用括号来分配结果,或者你甚至可以直接使用另一个 feed 操作符!如果是单参数的例程/方法,或者第一个参数是块,那么通常要求你用括号调用。如果例程/方法只取一个参数,或者第一个参数是一个块,通常要求你用括号调用(尽管最后一个例程/方法不需要这样)。

这个"传统的"结构,从下到上读,最后两行创建要处理的数据结构。

my @fractions = <TWO THREE FOUR FIVE SEVEN> »~» " " X~ <FIFTHS SIXTHS EIGHTHS>;
my @result = map { .uniparse },                    # (3) Converts to unicode
    grep { .uniparse },                            # (2) Checks if it parses
    map( {"VULGAR FRACTION " ~ $^þ }, @fractions); # (1) Adds string to input

# @result is [⅖ ⅗ ⅜ ⅘ ⅚ ⅝ ⅞]

现在我们使用带括号的 feed 运算符(从左到右),从上到下读。

my @result = (
    <TWO THREE FOUR FIVE SEVEN> »~» " " X~ <FIFTHS SIXTHS EIGHTHS> # (1) Input
    ==> map( {"VULGAR FRACTION " ~ $^þ } )                         # (2) Converts to Unicode name
    ==> grep({ .uniparse })                                        # (3) Filters only real names
    ==> map( { .uniparse} );                                       # (4) Converts to unicode
);

举例说明,方法链等价,从上到下读,使用的顺序与上述相同:

my @result = ( <TWO THREE FOUR FIVE SEVEN> »~» " " X~ <FIFTHS SIXTHS EIGHTHS>)
    .map( {"VULGAR FRACTION " ~ $^þ } )
    .grep({ .uniparse })
    .map({ .uniparse });

虽然在这种特殊情况下,结果是一样的,但 feed 操作符 =⇒ 通过箭头指向数据流的方向更清楚地表明了意图。如果要在不需要括号的情况下进行分配,可以使用另一个 feed 操作符。

my @result;
<people of earth>
    ==> map({ .tc })
    ==> grep /<[PE]>/
    ==> sort()
    ==> @result;

它可以用来捕捉部分结果,但是,与向左 feed 操作符不同的是,它需要括号或分号。

my @result;
<people of earth>
    ==> map({ .tc })
    ==> my @caps; @caps   # also could wrap in parentheses instead
    ==> grep /<[PE]>/
    ==> sort()
    ==> @result;

feed 操作符让你可以从例程和无关数据上的方法结果中构建类似方法链的模式。在方法链中,你被限制在数据上可用的方法或之前方法调用的结果。有了 feed 运算符,这种限制就没有了。由此产生的代码也可以被看作比一系列方法调用断在多行上更易读。

注意:在未来,这个操作符将看到一些变化,因为它获得了并行运行列表操作的能力。它将强制执行左操作数是可以封闭为闭包的(可以克隆并在子线程中运行)。

56.40. infix ⇐==

这个向左 feed 操作符从右边取结果,并将其作为最后一个参数传递给前一个(左)例程。这阐明了一系列列表操作函数的从右到左的数据流。

# Traditional structure, read bottom-to-top
my @result =
    sort                   # (4) Sort, result is <Earth People>
    grep { /<[PE]>/ },     # (3) Look for P or E
    map { .tc },           # (2) Capitalize the words
    <people of earth>;     # (1) Start with the input

# Feed (right-to-left) with parentheses, read bottom-to-top
my @result = (
    sort()                 # (4) Sort, result is <Earth People>
    <== grep({ /<[PE]>/ }) # (3) Look for P or E
    <== map({ .tc })       # (2) Capitalize the words
    <== <people of earth>  # (1) Start with the input
);

# To assign without parentheses, use another feed operator
my @result
    <== sort()              # (4) Sort, result is <Earth People>
    <== grep({ /<[PE]>/ })  # (3) Look for P or E
    <== map({ .tc })        # (2) Capitalize the words
    <== <people of earth>;  # (1) Start with the input

# It can be useful to capture a partial result
my @result
    <== sort()
    <== grep({ /<[PE]>/ })
    <== my @caps            # unlike ==>, there's no need for additional statement
    <== map({ .tc })
    <== <people of earth>;

与向右 feed 操作符不同,结果并不能紧密映射到方法链上。但是,与上面传统的结构相比,每个参数都用一行隔开,结果代码比逗号更具有示范性。左向 feed 操作符还允许你分解语句,并捕获一个中间结果,这对于调试或取该结果并在最终结果上创建另一个变体极为有用。

注意:在未来,这个操作符将看到一些变化,因为它获得了并行运行列表操作的能力。它将强制执行正确的操作数是可以封闭为闭包的(可以克隆并在子线程中运行)。

56.41. 同一性

一般来说,中缀运算符可以应用于单个或无元素而不产生错误,一般是在 reduce 操作的情况下。

say [-] ()  # OUTPUT: «0␤»

设计文件规定,这应该返回一个同一性值,而且必须为每个操作符指定一个同一性值。一般来说,返回的同一性元素应该是直观的。然而,这里有一个表格,规定了它在 Raku 中的操作符类是如何定义的,这与上面定义的类型和语言定义的操作符中的表格相对应。

运算符类

同一性值

Equality

Bool::True

Arithmetic

0

Arithmetic *

1

Comparison

True

Bitwise

0

Stringy

''

Sets

Empty set or equivalent

Or-like Bool

False

And-like Bool

True

例如,一个空列表的并集将返回一个空集合。

say [∪];  # OUTPUT: «set()␤»

这只适用于空或 0 总是有效操作数的运算符。例如,将其应用于除法会产生一个异常。

say [%] ();  # OUTPUT: «(exit code 1) No zero-arg meaning for infix:<%>␤»

57. Raku 中的面向对象

Raku 为面向对象编程(OOP)提供了强有力的支持。虽然 Raku 允许程序员以多种范式进行编程,但面向对象编程是该语言的核心。

Raku 提供了丰富的预定义类型,可以分为两类:常规类型和原生类型。一切可以存储在变量中的东西,要么是原生值,要么是对象。这包括字符、类型(类型对象)、代码和容器。

原生类型用于低级类型(如 uint64)。即使原生类型不具备与对象相同的功能,如果你对它们调用方法,它们也会自动被框定为正常的对象。

一切不是原生值的东西都是对象。对象确实允许继承封装

57.1. 使用对象

要调用一个对象上的方法,请在方法名后面加上一个点:

say "abc".uc;    # OUTPUT: «ABC␤»
#        ^^^ 不带参数的方法调用
my @words = $string.comb(/\w+/);
#                  ^^^^^^^^^^^^ 带一个参数的方法调用

这将调用 "abc" 上的 uc 方法,"abc" 是一个类型为 Str 的对象。要向方法提供参数,请在方法后的括号内添加参数。 另外一种方法调用的语法将方法名和参数列表用一个冒号分开(冒号紧跟方法名, 中间不能有空格):

my $formatted-text = "Fourscore and seven years ago...".indent(8);
say $formatted-text;
# OUTPUT: «        Fourscore and seven years ago...␤»

$formatted-text 现在包含上述文本,但缩进了8个空格。

多个参数用逗号分隔:

my @words = "Abe", "Lincoln";
@words.push("said", $formatted-text.comb(/\w+/));
say @words;
# OUTPUT: «[Abe Lincoln said (Fourscore and seven years ago)]␤»

同样,可以通过在方法后放置冒号并以逗号分隔参数列表来指定多个参数:

say @words.join('--').subst: 'years', 'DAYS';
# OUTPUT: «Abe--Lincoln--said--Fourscore and seven DAYS ago␤»

因为如果要传递没有括号的参数,就必须在方法后面加一个 :,所以没有冒号或括号的方法调用, 肯定是没有参数列表的方法调用。

say 4.log:   ; # OUTPUT: «1.38629436111989␤» ( natural logarithm of 4 )
say 4.log: +2; # OUTPUT: «2␤» ( base-2 logarithm of 4 )
say 4.log  +2; # OUTPUT: «3.38629436111989␤» ( natural logarithm of 4, plus 2 )

许多看起来不像方法调用的操作(例如,智能匹配或将一个对象插值到一个字符串中)可能会导致方法调用。

方法可以返回可变容器,在这种情况下,你可以对方法调用的返回值进行赋值。这就是对象的可读写属性的用法:

$*IN.nl-in = "\r\n";

这里,我们在 $*IN 对象上调用方法 nl-in,不需要参数,用 = 运算符赋值给返回的容器。

所有对象都支持来自类 Mu 的方法,它是类型层次结构的根。所有的对象都是从 Mu 派生出来的。

57.1.1. 类型对象

类型本身是对象,你可以通过写上一个类型名称来获得类型对象:

my $int-type-obj = Int;

你可以通过调用 WHAT 方法来请求任何东西的类型对象,WHAT 方法实际上是一个方法形式的宏:

my $int-type-obj = 1.WHAT;

类型对象(除 Mu外)可以用 === 恒等式运算符进行相等性比较:

sub f(Int $x) {
    if $x.WHAT === Int {
        say 'you passed an Int';
    }
    else {
        say 'you passed a subtype of Int';
    }
}

不过,在大多数情况下,使用 .isa 方法就足够了:

sub f($x) {
    if $x.isa(Int) {
        ...
    }
    ...
}

子类型检查是通过智能匹配来完成的:

if $type ~~ Real {
    say '$type contains Real or a subtype thereof';
}

57.2. 类

类是用 class 关键字来声明的,通常后面会有一个名字。

class Journey { }

这个声明的结果是一个类型对象被创建并安装在当前包和当前词法作用域内,名称为 Journey。你也可以用词法声明类:

my class Journey { }

这将它们的可见性限制在当前的词法作用域内,如果该类是嵌套在一个模块或其他类中的实现细节,这可能是有用的。

57.2.1. 属性

属性是每一个类的实例存在的变量;当实例化为一个值时,变量与其值之间的关联称为属性。它们是存储对象状态的地方。在 Raku 中,所有的属性都是私有的,这意味着它们只能由类实例本身直接访问。它们通常使用 has 声明符 和 ! twigil 来声明。

class Journey {
    has $!origin;
    has $!destination;
    has @!travellers;
    has $!notes;
}

虽然没有 public(甚至 protected)属性这种东西,但有一种方法可以让访问器方法自动生成:用 . twigil 代替 ! twigil(. 应该会提醒你一个方法调用)。

class Journey {
    has $.origin;
    has $.destination;
    has @!travellers;
    has $.notes;
}

这默认为提供一个只读访问器。为了允许修改属性,请添加 is rw 特性:

class Journey {
    has $.origin;
    has $.destination;
    has @!travellers;
    has $.notes is rw;
}

现在,在创建了一个 Journey 对象后,它的 .origin.destination.note 都可以从类外访问,但只有 .notes 可以被修改。

如果一个对象在实例化时没有某些属性,如 origindestination,我们可能不会得到想要的结果。为了防止这种情况发生,我们可以提供默认值,或者在创建对象时通过用 is required 特性标记属性来确保属性被设置。

class Journey {
    # error if origin is not provided
    has $.origin is required;
    # set the destination to Orlando as default (unless that is the origin!)
    has $.destination = self.origin eq 'Orlando' ?? 'Kampala' !! 'Orlando';
    has @!travelers;
    has $.notes is rw;
}

由于类继承了 Mu 的默认构造函数,而且我们要求为我们生成一些访问器方法,所以我们的类已经有了一定的功能。

# 创建一个新的类的实例.
my $vacation = Journey.new(
    origin      => 'Sweden',
    destination => 'Switzerland',
    notes       => 'Pack hiking gear!'
);

# 使用存取器; 这打印出 Sweden.
say $vacation.origin;

# 使用 rw 存取器来更改属性的值.
$vacation.notes = 'Pack hiking gear and sunglasses!';

请注意,虽然默认构造函数可以初始化只读属性,但它只会设置有访问方法的属性。也就是说,即使你把 travelers ⇒ ["Alex", "Betty"] 传给默认构造函数,属性 @!travelers 也不会被初始化。

57.2.2. 方法

方法是在类体内部用 method 关键字声明的。

class Journey {
    has $.origin;
    has $.destination;
    has @!travellers;
    has $.notes is rw;

    method add-traveller($name) {
        if $name ne any(@!travellers) {
            push @!travellers, $name;
        }
        else {
            warn "$name is already going on the journey!";
        }
    }

    method describe() {
        "From $!origin to $!destination"
    }
}

方法可以有一个签名,就像子例程一样。属性可以在方法中使用,并且总是可以和 ! twigil 一起使用,即使它们是用 . twigil 声明的。这是因为 . twigil 声明了一个 ! twigil,并生成了一个访问者方法。

从上面的代码来看,在方法 describe 中使用 $!origin$.origin 之间有一个微妙但重要的区别。$!origin 是一种廉价而明显的属性查找。$.origin 是一个方法调用,因此可以在子类中被重写。只有当你想允许重写时才使用 $.origin

与子例程不同,额外的命名参数不会产生编译时或运行时错误。这就允许通过重新分派进行方法的链式调用。

你可以编写你自己的访问器来覆盖任何或所有自动生成的访问器。

my $ⲧ = " " xx 4; # A tab-like thing
class Journey {
    has $.origin;
    has $.destination;
    has @.travelers;
    has Str $.notes is rw;

    multi method notes() { "$!notes\n" };
    multi method notes( Str $note ) { $!notes ~= "$note\n$ⲧ" };

    method Str { "⤷ $!origin\n$ⲧ" ~ self.notes() ~ "$!destination ⤶\n" };
}

my $trip = Journey.new( :origin<Here>, :destination<There>,
                        travelers => <þor Freya> );

$trip.notes("First steps");
notes $trip: "Almost there";
print $trip;

# OUTPUT:
#⤷ Here
#       First steps
#       Almost there
#
#There ⤶

所声明的 multi 方法 notes 覆盖了 $.notes 声明中隐式的自动生成的方法,使用不同的签名进行读写。

请注意,在 notes $trip: "Almost there" 中,我们使用的是间接调用者语法,先写方法名,再写对象,然后用冒号隔开,然后写参数: 即 method invocant: arguments。只要感觉比经典的句号和括号更自然,我们就可以使用这种语法。它的工作方式完全一样。

注意 Str 方法中对 notes 方法的调用是如何在 self 上进行的。以这种方式编写方法调用,会让方法的返回值与容器保持原样。为了将返回值容器化,你可以在 sigil 上进行方法调用,而不是在 self 上进行调用。这样就会根据用于容器化的 sigil,对方法的返回值进行各种方法调用:

Sigil

Method

$

item

@

list

%

hash

&

item

例如,JourneyStr 方法可以通过在返回的字符串中嵌入带魔符的方法调用来重写, 不使用 ~ 操作符:

method Str { "⤷ $!origin\n$ⲧ$.notes()$!destination ⤶\n" }

方法名可以在运行时用 ."" 运算符解析。

class A { has $.b };
my $name = 'b';
A.new."$name"().say;
# OUTPUT: «(Any)␤»

在本节中,更新 $.notes 的语法与之前的属性一节相比有所改变。取而代之的是赋值:

$vacation.notes = 'Pack hiking gear and sunglasses!';

我们现在进行的是方法调用:

$trip.notes("First steps");

覆盖默认的自动生成的访问器意味着它不再可以在返回时为赋值提供一个可变的容器。方法调用是为属性的更新添加计算和逻辑的首选方法。许多现代语言可以通过用 "setter" 方法重载赋值来更新一个属性。虽然 Raku 可以为此目的用 Proxy 对象重载赋值运算符,但目前不鼓励重载赋值来设置具有复杂逻辑的属性,因为面向对象的设计较弱

57.2.3. 类和继承方法

方法的签名可以有一个显式的调用者作为它的第一个参数,后面跟着一个冒号,这使得该方法可以引用它被调用的对象。

class Foo {
    method greet($me: $person) {
        say "Hi, I am $me.^name(), nice to meet you, $person";
    }
}
Foo.new.greet("Bob");    # OUTPUT: «Hi, I am Foo, nice to meet you, Bob␤»

在方法签名中提供调用者也允许通过使用类型约束将方法定义为类方法或对象方法。::?CLASS 变量可以用来在编译时提供类名,结合 :U(对于类方法)或 :D(对于实例方法)。

class Pizza {
    has $!radius = 42;
    has @.ingredients;

    # class method: construct from a list of ingredients
    method from-ingredients(::?CLASS:U $pizza: @ingredients) {
        $pizza.new( ingredients => @ingredients );
    }

    # instance method
    method get-radius(::?CLASS:D:) { $!radius }
}
my $p = Pizza.from-ingredients: <cheese pepperoni vegetables>;
say $p.ingredients;     # OUTPUT: «[cheese pepperoni vegetables]␤»
say $p.get-radius;      # OUTPUT: «42␤»
say Pizza.get-radius;   # This will fail.
CATCH { default { put .^name ~ ":\n" ~ .Str } };
# OUTPUT: «X::Parameter::InvalidConcreteness:␤
#          Invocant of method 'get-radius' must be
#          an object instance of type 'Pizza',
#          not a type object of type 'Pizza'.
#          Did you forget a '.new'?»

通过使用 multi 声明符,方法既可以是类方法,也可以是对象方法:

class C {
    multi method f(::?CLASS:U:) { say "class method"  }
    multi method f(::?CLASS:D:) { say "object method" }
}
C.f;       # OUTPUT: «class method␤»
C.new.f;   # OUTPUT: «object method␤»

57.2.4. self

在方法内部,术语 self 是可用的,并绑定到调用对象上。self 可以用来调用调用对象上的进一步方法,包括构造函数:

class Box {
  has $.data;

  method make-new-box-from() {
      self.new: data => $!data;
  }
}

self 也可以用在类或实例方法中,但要注意不要试图从另一种类型的方法中调用:

class C {
    method g()            { 42     }
    method f(::?CLASS:U:) { self.g }
    method d(::?CLASS:D:) { self.f }
}
C.f;        # OUTPUT: «42␤»
C.new.d;    # This will fail.
CATCH { default { put .^name ~ ":\n" ~ .Str } };
# OUTPUT: «X::Parameter::InvalidConcreteness:␤
#          Invocant of method 'f' must be a type object of type 'C',
#          not an object instance of type 'C'.  Did you forget a 'multi'?»

self 也可用于属性,只要它们有一个访问器。self.a 将调用声明为 has $.a 的属性的访问器。然而,self.a$.a 之间是有区别的,因为后者会 itemize;$.a 将等同于 self.a.item$(self.a)

class A {
    has Int @.numbers;
    has $.x = (1, 2, 3);

    method show-diff() { .say for self.x; .say for $.x }

    method twice  { self.times: 2 }
    method thrice { $.times: 3    }

    method times($val = 1) { @!numbers.map(* * $val).list }
};

my $obj = A.new(numbers => [1, 2, 3]);
$obj.show-diff;   # OUTPUT: «1␤2␤3␤(1 2 3)␤»
say $obj.twice;   # OUTPUT: «(2 4 6)␤»
say $obj.thrice;  # OUTPUT: «(3 6 9)␤»

对于使用 self 或快捷方式的方法调用,都支持方法参数的冒号语法,如上例中的 twicethrice 方法所示。

需要注意的是,如果 Mu 的相关方法 blessCREATE 没有重载,self 将指向这些方法中的类型对象。

另一方面,BUILDTWEAK 这两个子方法是在实例上调用的,处于初始化的不同阶段。来自子类的同名子方法还没有运行,所以你不应该依赖这些方法里面潜在的虚拟方法调用。

57.2.5. 私有方法

在方法名前带有感叹号 ! 的方法不能从定义类之外的任何地方调用;这样的方法是私有的,因为它们在声明它们的类之外是不可见的。私有方法在调用时用感叹号代替点号。

class FunMath {
    has $.value is required;
    method !do-subtraction( $num ) {
        if $num ~~ Str {
            return $!value + (-1 * $num.chars);
        }
        return $!value + (-1 * $num);
    }
    method minus( $minuend: $subtrahend ) {
        # invoking the private method on the explicit invocant
        $minuend!do-subtraction($subtrahend);
    }
}
my $five = FunMath.new(value => 5);
say $five.minus(6);         # OUTPUT: «-1␤»

say $five.do-subtraction(6);
CATCH { default { put .^name ~ ":\n" ~ .Str } }
# OUTPUT: «X::Method::NotFound:
# No such method 'do-subtraction' for invocant of type
# 'FunMath'. Did you mean '!do-subtraction'?␤»

私有方法不能被子类继承.

57.2.6. 子方法

子方法是不会被子类继承的公共方法。这个名字源于它们在语义上与子例程相似。

子方法对于对象的构造和销毁任务,以及对于特定类型的任务非常有用,以至于子类型肯定要覆盖它们。

例如,默认方法 new继承链中的每个类上调用子方法 BUILD:

class Point2D {
    has $.x;
    has $.y;

    submethod BUILD(:$!x, :$!y) {
        say "Initializing Point2D";
    }
}

class InvertiblePoint2D is Point2D {
    submethod BUILD() {
        say "Initializing InvertiblePoint2D";
    }
    method invert {
        self.new(x => - $.x, y => - $.y);
    }
}

say InvertiblePoint2D.new(x => 1, y => 2);
# OUTPUT: «Initializing Point2D␤»
# OUTPUT: «Initializing InvertiblePoint2D␤»
# OUTPUT: «InvertiblePoint2D.new(x => 1, y => 2)␤»

另见: 对象结构

57.2.7. 继承

类可以有父类:

class Child is Parent1 is Parent2 { }

如果在子类上调用了一个方法,而子类没有提供该方法,那么就会调用父类中的一个该名称的方法(如果存在的话)。父类被调用的顺序称为方法解析顺序(MRO)。Raku 使用 C3 方法解析顺序。你可以通过调用一个类型的元类来询问它的 MRO:

say List.^mro;      # ((List) (Cool) (Any) (Mu))

如果一个类没有指定父类,默认为 Any。所有的类都直接或间接地派生自 Mu,即类型层次结构的根。

所有对公共方法的调用都是 C++ 意义上的"虚方法",这意味着一个对象的实际类型决定了要调用哪个方法,而不是声明的类型:

class Parent {
    method frob {
        say "the parent class frobs"
    }
}

class Child is Parent {
    method frob {
        say "the child's somewhat more fancy frob is called"
    }
}

my Parent $test;
$test = Child.new;
$test.frob;          # calls the frob method of Child rather than Parent
# OUTPUT: «the child's somewhat more fancy frob is called␤»

如果要在孩子对象上显式调用父方法,请参考父命名空间中的全称:

$test.Parent::frob;  # calls the frob method of Parent
# OUTPUT: «the parent class frobs␤»

57.2.8. 代理

代理是一种技术,通过这种技术,一个对象的成员(被代理人)在另一个原始对象(代理人)的上下文中被求值。换句话说,所有对代理人的方法调用都被代理给被代理人。

在 Raku 中,代理是通过将 handles 特质应用于属性来指定的。提供给 trait 的参数指定了当前对象和代理对象的共同方法。可以提供一个 Pair(用于重命名)、一个 Pair 的列表、一个 Regex 或一个 Whatever,而不是一个方法名的列表。

class Book {
    has Str  $.title;
    has Str  $.author;
    has Str  $.language;
    has Cool $.publication;
}

class Product {
    has Book $.book handles('title', 'author', 'language', year => 'publication');
}

my $book = Book.new:
    :title<Dune>,
    :author('Frank Herbert'),
    :language<English>,
    :publication(1965)
;

given Product.new(:$book) {
    say .title;    # OUTPUT: «Dune␤»
    say .author;   # OUTPUT: «Frank Herbert␤»
    say .language; # OUTPUT: «English␤»
    say .year;     # OUTPUT: «1965␤»
}

在上面的例子中,Product 类定义了属性 $.book,并将其标记为 handles trait,以指定每当在 Product 类的实例对象上被调用时,这些方法将被转发到 Book 类。这里有几件事需要注意。

  • 我们没有在 Product 类内部编写任何方法来调用它的实例对象。相反,我们指示该类将任何这些方法的调用代理给 Book 类。

  • 我们已经指定了方法名 titleauthorlanguage,因为它们出现在 Book 类中。另一方面,我们通过提供适当的 Pairpublication 方法改名为 year

代理可以作为继承的替代方法,通过代理给父类,而不是继承它的所有方法。例如,下面的 Queue 类将队列的几个方法适当地代理给 Array 类,同时也为其中的几个方法提供了一个首选接口(例如,push 之于 enqueue)。

class Queue {
    has @!q handles(
        enqueue => 'push', dequeue => 'shift',
        'push', 'shift', 'head', 'tail', 'elems', 'splice'
    );

    method gist {
        '[' ~ @!q.join(', ') ~ ']'
    }
}

my Queue $q .= new;
$q.enqueue($_) for 1..5;
$q.push(6);
say $q.shift;                  # OUTPUT: «1␤»
say $q.dequeue while $q.elems; # OUTPUT: «2␤3␤4␤5␤6␤»

$q.enqueue($_) for <Perl Python Raku Ruby>;
say $q.head;                   # OUTPUT: «Perl␤»
say $q.tail;                   # OUTPUT: «Ruby␤»
say $q;                        # OUTPUT: «[Perl, Python, Raku, Ruby]␤»
$q.dequeue while $q.elems;
say $q;                        # OUTPUT: «[]␤»

57.2.9. 对象构造

对象一般是通过方法调用来创建的,可以在类型对象上创建,也可以在同类型的另一个对象上创建。

Mu 提供了一个名为 new 的构造方法,它接受命名参数,并使用它们来初始化公共属性。

class Point {
    has $.x;
    has $.y;
}
my $p = Point.new( x => 5, y => 2);
#             ^^^ inherited from class Mu
say "x: ", $p.x;
say "y: ", $p.y;
# OUTPUT: «x: 5␤»
# OUTPUT: «y: 2␤»

Mu.new 在它的调用者上调用方法 bless,传递所有的命名参数bless 创建新对象,然后按照方法解析的反向顺序(即从 Mu 到大多数派生类)走遍所有子类,并在每个类中检查是否存在一个名为 BUILD 的方法。如果该方法存在,则用 new 方法的所有命名参数来调用该方法。如果不存在,那么这个类的公共属性将从同名的命名参数中初始化。在这两种情况下,如果 BUILD 和默认机制都没有初始化属性,则应用默认值。这意味着 BUILD 可以改变一个属性,但它不能访问声明为其默认值的属性的内容;这些内容只有在 TWEAK 期间才能获得(见下文),TWEAK 可以"看到"在类的声明中初始化的属性的内容。

在调用了 BUILD 方法之后,如果存在名为 TWEAK 的方法,就会调用这些方法,再次调用传递给 new 的所有命名参数。请看下面的一个使用实例。

由于 BUILDTWEAK 子方法的默认行为,派生自 Mu 的构造函数 new 的命名参数可以直接对应于方法解析顺序中任何一个类的公共属性,或者对应于任何 BUILDTWEAK 子方法的任何命名参数。

这种对象构造方案对自定义构造函数有几个影响。首先,自定义 BUILD 方法应该始终是子方法,否则会破坏子类的属性初始化。其次,BUILD 子方法可以用来在对象构造时运行自定义代码。它们还可以用于为属性初始化创建别名。

class EncodedBuffer {
    has $.enc;
    has $.data;

    submethod BUILD(:encoding(:$enc), :$data) {
        $!enc  :=  $enc;
        $!data := $data;
    }
}
my $b1 = EncodedBuffer.new( encoding => 'UTF-8', data => [64, 65] );
my $b2 = EncodedBuffer.new( enc      => 'UTF-8', data => [64, 65] );
#  both enc and encoding are allowed now

由于向例程传递参数会将参数与参数绑定,所以如果将属性作为参数,则不需要单独的绑定步骤。因此,上面的例子也可以写成。

submethod BUILD(:encoding(:$!enc), :$!data) {
    # nothing to do here anymore, the signature binding
    # does all the work for us.
}

但是,当属性可能有特殊的类型要求时,使用这种自动绑定属性时要小心,例如 :$!id 必须是正整数。请记住,除非你特别照顾这个属性,否则将分配默认值,而这个默认值将是 Any,这将导致类型错误。

第三个含义是,如果你想要一个接受位置参数的构造函数,你必须编写自己的 new 方法。

class Point {
    has $.x;
    has $.y;
    method new($x, $y) {
        self.bless(:$x, :$y);
    }
}

然而这被认为是糟糕的做法,因为它使来自子类的对象的正确初始化变得更加困难。

另外需要注意的是,new 这个名字在 Raku 中并不特别。它只是一个普通的约定,在大多数 Raku 类中都被彻底遵循。你可以从任何方法中调用 bless,或者使用 CREATE 来摆弄低级的工作。

TWEAK 子方法允许你在对象构造后检查东西或修改属性。

class RectangleWithCachedArea {
    has ($.x1, $.x2, $.y1, $.y2);
    has $.area;
    submethod TWEAK() {
        $!area = abs( ($!x2 - $!x1) * ( $!y2 - $!y1) );
    }
}

say RectangleWithCachedArea.new( x2 => 5, x1 => 1, y2 => 1, y1 => 0).area;
# OUTPUT: «4␤»

57.2.10. 对象克隆

克隆是使用所有对象上可用的 clone 方法完成的,该方法可以浅层克隆公共属性和私有属性。公共属性的新值可以作为命名参数提供。

class Foo {
    has $.foo = 42;
    has $.bar = 100;
}

my $o1 = Foo.new;
my $o2 = $o1.clone: :bar(5000);
say $o1; # Foo.new(foo => 42, bar => 100)
say $o2; # Foo.new(foo => 42, bar => 5000)

关于非标量属性如何被克隆,以及实现你自己的自定义克隆方法的例子,请参见 clone 的文档。

57.3. 角色

角色是属性和方法的集合;但是,与类不同的是,角色只用于描述对象的部分行为;这就是为什么,一般情况下,角色是要混在类和对象中的原因。一般来说,类是用来管理对象的,而角色是用来管理对象中的行为和代码重用的。

角色在声明的角色名称前使用关键字 role。角色使用 does 关键字在被混入的角色名称前混合。

角色也可以使用 is 混合到一个类中。然而,is 一个角色的语义与 does 提供的语义有很大不同。使用 is,一个类从角色中被授权,然后被继承。因此,没有扁平化的组合,也没有 does 提供的安全保障。

constant ⲧ = " " xx 4; #Just a ⲧab
role Notable {
    has Str $.notes is rw;

    multi method notes() { "$!notes\n" };
    multi method notes( Str $note ) { $!notes ~= "$note\n" ~ ⲧ };

}

class Journey does Notable {
    has $.origin;
    has $.destination;
    has @.travelers;

    method Str { "⤷ $!origin\n" ~ ⲧ ~ self.notes() ~ "$!destination ⤶\n" };
}

my $trip = Journey.new( :origin<Here>, :destination<There>,
                        travelers => <þor Freya> );

$trip.notes("First steps");
notes $trip: "Almost there";
print $trip;
# OUTPUT:
#⤷ Here
#       First steps
#       Almost there
#
#There ⤶

一旦编译器解析了角色声明的闭合花括号,角色就不可更改了。

57.3.1. 应用角色

角色应用与类的继承有很大的不同。当一个角色被应用到一个类中时,该角色的方法会被复制到该类中。如果将多个角色应用到同一个类中,冲突(如属性或同名的非 multi 方法)会导致编译时错误,可以通过在类中提供一个同名的方法来解决。

这比多重继承要安全得多,因为在多重继承中,冲突永远不会被编译器检测到,而是被解析到方法解析顺序中出现较早的超类,这可能不是程序员想要的。

例如,如果你发现了一种高效的骑牛方法,并试图将其作为一种新的流行交通方式进行推广,你可能会有一个类 Bull,用于你在家里饲养的所有公牛,而一个类 Automobile,用于你可以驾驶的东西。

class Bull {
    has Bool $.castrated = False;
    method steer {
        # Turn your bull into a steer
        $!castrated = True;
        return self;
    }
}
class Automobile {
    has $.direction;
    method steer($!direction) { }
}
class Taurus is Bull is Automobile { }

my $t = Taurus.new;
say $t.steer;
# OUTPUT: «Taurus.new(castrated => Bool::True, direction => Any)␤»

这样的设置,你可怜的客户会发现自己的金牛座(Taurus)无法转动,你也无法做出更多的产品! 在这种情况下,使用角色可能会更好:

role Bull-Like {
    has Bool $.castrated = False;
    method steer {
        # Turn your bull into a steer
        $!castrated = True;
        return self;
    }
}
role Steerable {
    has Real $.direction;
    method steer(Real $d = 0) {
        $!direction += $d;
    }
}
class Taurus does Bull-Like does Steerable { }

这段代码会死于这样的情况:

===SORRY!===
Method 'steer' must be resolved by class Taurus because it exists in
multiple roles (Steerable, Bull-Like)

这个检查会让你省去很多头疼的事情:

class Taurus does Bull-Like does Steerable {
    method steer($direction?) {
        self.Steerable::steer($direction)
    }
}

当一个角色应用于第二个角色时,实际应用会被延迟,直到第二个角色应用于一个类,这时两个角色都会应用于该类。因此:

role R1 {
    # methods here
}
role R2 does R1 {
    # methods here
}
class C does R2 { }

和类 C 这样做产生的结果一样:

role R1 {
    # methods here
}
role R2 {
    # methods here
}
class C does R1 does R2 { }

57.3.2. 存根

当角色中包含一个存根方法,也就是其代码仅限于 …​ 的方法时,在角色被应用到一个类时,必须提供一个同名方法的非存根版本。这允许你创建作为抽象接口的角色。

role AbstractSerializable {
    method serialize() { ... }  # 字面的三个点 ... 把方法标记为 stub
}

# 下面是一个编译时错误, 例如
# Method 'serialize' must be implemented by APoint because
# it is required by a role

class APoint does AbstractSerializable {
    has $.x;
    has $.y;
}

# 这个有效:
class SPoint does AbstractSerializable {
    has $.x;
    has $.y;
    method serialize() { "p($.x, $.y)" }
}

存根方法的实现也可以由其他角色提供。

57.3.3. 继承

角色不能从类中继承,但它们可以携带类,导致任何 does 该角色的类都能从携带的类中继承。所以如果你这样写:

role A is Exception { }
class X::Ouch does A { }
X::Ouch.^parents.say # OUTPUT: «((Exception))␤»

那么 X::Ouch 将直接从 Exception 继承,正如我们在上面列出它的父类所看到的那样。

由于它们没有使用所谓的继承,所以角色不是类层次结构的一部分。角色是用 .^roles 元方法来代替列出的,它使用 transitive 作为包括所有层次或只包括第一个层次的标志。尽管如此,一个类或实例仍然可以用智能匹配或类型约束来测试它是否做了(does)一个角色。

role F { }
class G does F { }
G.^roles.say;                    # OUTPUT: «((F))␤»
role Ur {}
role Ar does Ur {}
class Whim does Ar {}; Whim.^roles(:!transitive).say;   # OUTPUT: «((Ar))␤»
say G ~~ F;                      # OUTPUT: «True␤»
multi a (F $a) { "F".say }
multi a ($a)   { "not F".say }
a(G);                            # OUTPUT: «F␤»

57.3.4. 主从秩序

在类中直接定义的方法总是覆盖来自应用角色或继承类的定义。如果不存在这样的定义,来自角色的方法会覆盖从类中继承的方法。这种情况既发生在所述类被角色带入时,也发生在所述类被直接继承时。

role M {
  method f { say "I am in role M" }
}

class A {
  method f { say "I am in class A" }
}

class B is A does M {
  method f { say "I am in class B" }
}

class C is A does M { }

B.new.f; # OUTPUT «I am in class B␤»
C.new.f; # OUTPUT «I am in role M␤»

请注意,multi 方法的每个候选者都是自己的方法。在这种情况下,只有当两个这样的候选方法具有相同的签名时,上述规定才适用。否则,就不存在冲突,候选者只是被添加到 multi 方法中。

57.3.5. 自动角色双关

任何试图直接实例化一个角色或将其作为类型对象的尝试,都会自动创建一个与角色同名的类,使得可以透明地将角色作为一个类来使用。

role Point {
    has $.x;
    has $.y;
    method abs { sqrt($.x * $.x + $.y * $.y) }
    method dimensions { 2 }
}
say Point.new(x => 6, y => 8).abs; # OUTPUT «10␤»
say Point.dimensions;              # OUTPUT «2␤»

我们把这种自动创建类的行为称为 punning,生成的类称为 pun。

不过,双关(punning)并不是由大多数元编程构造引起的,因为那些构造有时会被用来直接与角色一起工作。

57.3.6. 参数化角色

角色可以被参数化,通过在方括号中给它们一个签名:

role BinaryTree[::Type] {
    has BinaryTree[Type] $.left;
    has BinaryTree[Type] $.right;
    has Type $.node;

    method visit-preorder(&cb) {
        cb $.node;
        for $.left, $.right -> $branch {
            $branch.visit-preorder(&cb) if defined $branch;
        }
    }
    method visit-postorder(&cb) {
        for $.left, $.right -> $branch {
            $branch.visit-postorder(&cb) if defined $branch;
        }
        cb $.node;
    }
    method new-from-list(::?CLASS:U: *@el) {
        my $middle-index = @el.elems div 2;
        my @left         = @el[0 .. $middle-index - 1];
        my $middle       = @el[$middle-index];
        my @right        = @el[$middle-index + 1 .. *];
        self.new(
            node    => $middle,
            left    => @left  ?? self.new-from-list(@left)  !! self,
            right   => @right ?? self.new-from-list(@right) !! self,
        );
    }
}

my $t = BinaryTree[Int].new-from-list(4, 5, 6);
$t.visit-preorder(&say);    # OUTPUT: «5␤4␤6␤»
$t.visit-postorder(&say);   # OUTPUT: «4␤6␤5␤»

这里的签名只包括类型捕获,但任何签名都可以:

enum Severity <debug info warn error critical>;

role Logging[$filehandle = $*ERR] {
    method log(Severity $sev, $message) {
        $filehandle.print("[{uc $sev}] $message\n");
    }
}

Logging[$*OUT].log(debug, 'here we go'); # OUTPUT: «[DEBUG] here we go␤»

你可以拥有多个同名的角色,但签名不同;选择多个候选者时适用正常的多重分派规则。

57.3.7. 角色混合

角色可以混合到对象中。角色的属性和方法将被添加到对象已有的方法和属性中。支持多重混合和匿名角色。

role R { method Str() {'hidden!'} };
my $i = 2 but R;
sub f(\bound){ put bound };
f($i); # OUTPUT: «hidden!␤»
my @positional := <a b> but R;
say @positional.^name; # OUTPUT: «List+{R}␤»

请注意,对象得到了角色混入,而不是对象的类或容器。因此,带 @ 魔符的容器将需要绑定来使角色粘住,如 @positional 的例子所示。有些操作符会返回一个新的值,这实际上是将混入的角色从结果中剥离出来。这就是为什么在使用 does 的变量声明中混入角色可能会更清楚:

role R {};
my @positional does R = <a b>;
say @positional.^name; # OUTPUT: «Array+{R}␤»

操作符 infix:<but> 比列表构造函数窄。当提供一个角色列表来混入时,总是使用括号。

role R1 { method m {} }
role R2 { method n {} }
my $a = 1 but R1,R2; # R2 is in sink context, issues a WARNING
say $a.^name;
# OUTPUT: «Int+{R1}␤»
my $all-roles = 1 but (R1,R2);
say $all-roles.^name; # OUTPUT: «Int+{R1,R2}␤»

Mixins 可以在你的对象生命中的任何时刻使用。

# A counter for Table of Contents
role TOC-Counter {
    has Int @!counters is default(0);
    method Str() { @!counters.join: '.' }
    method inc($level) {
        @!counters[$level - 1]++;
        @!counters.splice($level);
        self
    }
}

my Num $toc-counter = NaN;     # don't do math with Not A Number
say $toc-counter;              # OUTPUT: «NaN␤»
$toc-counter does TOC-Counter; # now we mix the role in
$toc-counter.inc(1).inc(2).inc(2).inc(1).inc(2).inc(2).inc(3).inc(3);
put $toc-counter / 1;          # OUTPUT: «NaN␤» (because that's numerical context)
put $toc-counter;              # OUTPUT: «2.2.2␤» (put will call TOC-Counter::Str)

角色可以是匿名的。

my %seen of Int is default(0 but role :: { method Str() {'NULL'} });
say %seen<not-there>;          # OUTPUT: «NULL␤»
say %seen<not-there>.defined;  # OUTPUT: «True␤» (0 may be False but is well defined)
say Int.new(%seen<not-there>); # OUTPUT: «0␤»

57.4. 元对象编程和自省

Raku 有一个元对象系统,这意味着对象、类、角色、grammar、枚举等的行为本身是由其他对象控制的,这些对象被称为元对象。元对象和普通对象一样,都是类的实例,在这种情况下,我们把它们称为元类。

对于每个对象或类,你可以通过对它调用 .HOW 来获得元对象。请注意,虽然这看起来像一个方法调用,但它的工作方式更像一个宏。

那么,你可以用元对象做什么呢?首先你可以通过比较两个对象是否具有相同的元类来检查它们是否相等。

say 1.HOW ===   2.HOW;      # OUTPUT: «True␤»
say 1.HOW === Int.HOW;      # OUTPUT: «True␤»
say 1.HOW === Num.HOW;      # OUTPUT: «False␤»

Raku 用 HOW(Higher Order Workings) 这个词来指代元对象系统。因此,在 Raku 中,控制类行为的元类的类名叫做 Perl6::Metamodel::ClassHOW 也就不奇怪了。每个类都有一个 Perl6::Metamodel::ClassHOW 的实例。

但当然,元模型还能为你做更多的事情。例如,它允许你对对象和类进行内省。元对象上的方法的调用惯例是调用元对象上的方法,并将感兴趣的对象作为对象的第一个参数传入。因此,要想获得一个对象的类的名称,你可以这样写:

my $object = 1;
my $metaobject = 1.HOW;
say $metaobject.name($object);      # OUTPUT: «Int␤»

# or shorter:
say 1.HOW.name(1);                  # OUTPUT: «Int␤»

(动机是 Raku 也希望允许一个更基于原型的对象系统,在这个系统中,不需要为每个类型创建一个新的元对象)。

有一个捷径可以避免两次使用同一个对象:

say 1.^name;                        # OUTPUT: «Int␤»
# same as
say 1.HOW.name(1);                  # OUTPUT: «Int␤»

参见 Metamodel::ClassHOW 中关于 class 的元类的文档,也可参见元对象协议的通用文档

58. 下标

58.1. Subscripts

通过索引或键访问数据结构中的元素。

通常,人们需要引用集合或数据结构中的一个特定的元素(或特定的元素切片)。从数学标记法中偷学到的,向量 v 的组成部分用 v₁, v₂, v₃ 来引用,在 Raku 中这个概念叫做 “下标” (或“索引”)。

58.2. Basics

Raku 提供了两个通用的下标接口:

	   elements are identified by     interface name    supported by
[ ]	 zero-based indices	              Positional        Array, List, Buf, Match, ...
{ }	 string or object keys            Associative       Hash, Bag, Mix, Match, ...
  • Positional 下标 (通过 _[postcircumfix [ ]] 通过元素在有序集合中的位置来寻址元素。)索引 0 引用第一个元素, 索引 1 引用第二个元素, 以此类推:

  my @chores = "buy groceries", "feed dog", "wash car";
  say @chores[0];  #-> buy groceries
  say @chores[1];  #-> feed dog
  say @chores[2];  #-> wash car
  • Associative 下标 (通过 postcircumfix { }), 不要求集合以任何特定的顺序保存元素 - 相反,它使用一个唯一的键来寻址每个值。键的种类取决于使用的集合: 举个例子, 一个标准的散列 使用字符串作为键, 而一个 Mix 能使用任意的对象作为键, 等等:

  my %grade = Zoe => "C", Ben => "B+";
  say %grade{"Zoe"};  #-> C
  say %grade{"Ben"};  #-> B+

  my $stats = ( Date.today => 4.18, Date.new(2015,  4,  5) => 17.253 ).Mix;
  say $stats{ Date.new(2015, 4, 5) };  #-> 17.253
相对于传递单个-单词字符串键给 `{ }` , 你也可以使用link:http://doc.raku.org/language/quoting#Word_quoting:_qw[以尖括号引起单词的结构] ,就像它们是后缀操作符一样:
  say %grade<Zoe>;    #-> C
  say %grade<Ben>;    #-> B+
这实际上仅仅是在编译时被转换为对应 `{ }` 形式的语法糖:
  %hash<foo bar>;     # same as %hash{ <foo bar> }
  %hash«foo $var»;    # same as %hash{ «foo $var» }
  %hash<<foo $var>>;  # same as %hash{ <<foo $var>> }

下标能应用到能返回可下标化对象的任何表达式上, 而不仅仅应用到变量上:

say "__Hello__".match(/__(.*)__/)[0];   #-> 「Hello」
say "__Hello__".match(/__(.*)__/).[0];  # same, in method notation

Positional 和 associative 下标并不互相排斥 - 举个例子, Match 对象两个都支持(每个访问不同的数据集)。还有, 为了让列表处理更方便, 类 Any 为`位置下标`提供了备用的实现,这会把调用者看作含有`一个`元素的列表。(但是对于关系下标,没有这样的备用实现, 所以会抛出一个异常,当下标被应用到没有实现支持的对象上时。)

say 42[0];    #-> 42
say 42<foo>;  # ERROR: postcircumfix { } not defined for type Int

58.3. Nonexistent elements

当通过下标寻址一个不存在的元素所发生的事情取决于正在使用的集合类型。标准的 Array 和 Hash 集合返回它们的value type constraint 的类型对象(这默认是 Any)。

my @array1;     say @array1[10];  #-> (Any)
my Int @array2; say @array2[10];  #-> (Int)

my %hash1;      say %hash1<foo>;  #-> (Any)
my Int %hash2;  say %hash2<foo>;  #-> (Int)

然而, 其它类型的集合可能在寻址不存在的元素的下标时反应也不用:

say (0, 10, 20)[3];       #-> Nil
say bag(<a a b b b>)<c>;  #-> 0

为了在下标操作中默默地跳过不存在的元素, 查看 Truncating slices#:v 副词。

58.4. From the end

Positional 索引是从集合的开头计数的, 但是也有一种标记法用于,通过相对于末尾的位置来寻址元素:-1 引用最后一个元素, -2 引用倒数第二个元素, 以此类推。

my @alphabet = 'A' .. 'Z';
say @alphabet[*-1];  #-> Z
say @alphabet[*-2];  #-> Y
say @alphabet[*-3];  #-> X

注意:星号很重要。在 Raku中,如果像在很多其它编程语言中那样传递一个裸的负整数(例如 @alphabet[-1]), 会抛出错误。

这里实际发生的是, -1 那样的表达式通过 Whatever 柯里化声明了一个代码对象 - [ ]`会把代码对象作为索引, 通过集合的长度作为参数来调用它并使用结果值作为实际的索引。 换句话说,@alphabet[-1]` 变成了 @alphabet[@alphabet.elems - 1]

这意味着你可以使用任何依赖于集合尺寸的表达式:

say @array[* div 2];  # 选择最中间的那个元素
say @array[$i % *];   # wrap around a given index ("模运算")
say @array[ -> $size { $i % $size } ];  # same as previous

58.5. Slices

当需要访问集合中的多个元素时,有一个快捷方式用于处理多个单独的下标操作:仅仅在下标中指定一个`索引/键`的列表,来取回一个元素的列表 - 也被叫做”切片” - 以相同的顺序。

对于 positional 切片, 你可以混合普通切片和 from-the-end 切片:

my @alphabet = 'a' .. 'z';
dd @alphabet[15, 4, *-9, 11];  #-> ("p", "e", "r", "l")

对于 associative 切片,尖括号形式的切片通常会很方便:

my %color = kiwi => "green", banana => "yellow", cherry => "red";
dd %color{"cherry", "kiwi"};  #-> ("red", "green")
dd %color<cherry kiwi>;       #-> ("red", "green")
dd %color{*};                 #-> ("green", "red", "yellow")

要知道切片是由传入 (one dimension of)下标的类型控制的,而非它的长度:

subscript	                               result
any Positional object not covered below	   normal slice
a Range or infinite sequence	           truncating slice (only for positional subscripts)
* (Whatever-star)	                       full slice (as if all keys/indices were specified)
any other object	                       single-element access rather than a slice
empty	                                   Zen slice

所以,即使一个单个元素的列表也会返回一个切片, 而一个裸的标量值不会:

dd @alphabet[2,];  #-> ("c",)
dd @alphabet[2];   #-> "c"

(尖括号形式的 associative 下标也没有问题,因为 word quoting 在单个单词的情况下很方便的返回一个 Str, 但是在多个单词的情况下返回一个 Parcel)。

对于普通的切片,下标的内容 (the current dimension of) 在它的元素被解释为 索引/键 之前会被展平(flattened):

dd @alphabet[0, (1..2, (3,)))];  #-> ("a", "b", "c", "d")

58.5.1. Truncating slices

通常, 在切片下标中引用不存在的元素会让输出列表包含未定义的值。然而, 如果传递给位置下标的对象是一个 Range 或使用序列操作符构建的无限序列, 它会被自动截断到集合的实际尺寸:

my @letters = <a b c d e f>;
dd @letters[3, 4, 5, 6, 7];  #-> ("d", "e", "f", Any, Any)
dd @letters[3 .. 7];         #-> ("d", "e", "f")

From-the-end 索引被允许作为范围的端点,代表无限的范围和序列:

say @array[*-3 .. *];       # select the last three elements
say @array[0, 2, 4 ... *];  # select all elements with even indices

如果你不想把你的切片指定为 range/sequence 但仍旧想默默地跳过不存在的元素, 你可以使用 #:v 副词。

58.5.2. Zen slices

如果你写的下标没有指定任何 索引/键 ,那它就会返回被脚注的对象自身。因为它是空的但是返回了全部东西, 这就是所谓的 "Zen slice"。

这和传递一个 Whatever-star (这,像普通的切片, 总是返回一个元素的 Parcel,不管原对象的类型)还有传递一个空的列表都不同(它返回一个空的切片):

my %bag := ("orange" => 1, "apple" => 3).Bag;
dd %bag<>;    #-> ("orange"=>1,"apple"=>3).Bag
dd %bag{};    #-> ("orange"=>1,"apple"=>3).Bag
dd %bag{*};   #-> (1, 3)
dd %bag{()};  #-> ()

这通常被用于把整个 数组/散列 插值到字符串中:

my @words = "cruel", "world";
say "Hello, @words[]!"  #-> Hello, cruel world!

58.6. Multiple dimensions

尚未实现!等到 9 月份?

58.7. Modifying elements

58.8. Autovivification

下标参与 "autovivification”(自动复活),i.e. 这是一种数组和散列在需要时会自动存在的处理, 以至于你没有必要在每一层级预声明集合的类型来构建嵌套的数据结构:

my $beatles;
$beatles{"White Album"}[0] = "Back in the U.S.S.R.";  # autovivification!
say $beatles.perl;  #-> {"White Album" => ["Back in the U.S.S.R."]}

$beatles 从未定义开始, 但是它变成了一个 Hash 对象, 因为它在赋值时用 { } 标注了。 类似地, $beatles{"White Album”} 变成一个 Array 对象, 因为它在赋值时用 [ ] 标注了。

注意下标本身不会引起 autovivification(自动复活):它只发生在下标链的结果被赋值时(或变化时)。

58.9. Binding

下标表达式也可以用在绑定语句的左侧。如果被标注的集合的类型支持, 这会使用指定的容器替换集合里的插槽的值:(给跪了!)

内置的 Array 和 Hash 类型支持这种绑定, 为了允许构建复杂的联动的数据结构:

my @a = 10, 11, 12, 13;
my $x = 1;

@a[2] := $x;  # binding! (@a[2] and $x refer to the same container now.)

$x++; @a[2]++;

dd @a;  #-> [10, 11, 3, 13]<>
dd $x;  #-> 3

查看 #method BIND-POS#method BIND-KEY 了解底层机制.

58.10. Adverbs

下标操作的返回值和可能存在的副作用能够使用副词来控制。

要知道副词操作符的优先级相对宽松,这可能需要你在合成表达式中添加括号:

if $foo || %hash<key>:exists { ... }    # WRONG, tries to adverb the || op
if $foo || (%hash<key>:exists) { ... }  # correct

支持的副词有:

58.10.1. :exists

返回请求的元素是否存在,而不是返回元素实际的值。这能够用于区别未定义值的元素和一点儿也不属于集合部分的元素:

my @foo = Any, 10;
dd @foo[0].defined;    #-> False
dd @foo[0]:exists;     #-> True
dd @foo[2]:exists;     #-> False
dd @foo[0, 2]:exists;  #-> (True, False)

my %fruit = apple => Any, orange => 10;
dd %fruit<apple>.defined;       #-> False
dd %fruit<apple>:exists;        #-> True
dd %fruit<banana>:exists;       #-> False
dd %fruit<apple banana>:exists; #-> (True, False)

也可以对副词取反来测试不存在:

dd %fruit<apple banana>:!exists; #-> (False, True)

要检查切片的所有元素是否存在, 使用 all junction:

if all %fruit<apple orange banana>:exists { ... }

:exists 可以和 :delete 还有 :p/:kv 副词组合 - 这时表达式的行为就由那些副词决定,除了使用表明元素存在的对应 Bool 值替换返回的元素值之外。

查看 method EXISTS-POSmethod EXISTS-KEY 了解底层机制.

58.10.2. :delete

从集合中删除元素, 除了返回它们的值以外。

my @tens = 0, 10, 20, 30;
dd @tens[3]:delete;     #-> 30
dd @tens;               #-> [0, 10, 20]<>

my %fruit = apple => 5, orange => 10, banana => 4, peach => 17;
dd %fruit<apple>:delete;         #-> 5
dd %fruit<peach orange>:delete;  #-> (17, 10)
dd %fruit;                       #-> {banana => 4}<>

使用否定形式的副词,元素实际上不会被删除。这意味着你可以传递一个标记,让它变成有条件的删除:

dd %fruit<apple> :delete($flag);  # deletes the element only if $flag is
                                  # true, but always returns the value.

能和 :exists 还有 :p/:kv/:k/:v 副词组合 - 这时返回值由那些副词决定, 但是同时元素也会被删除。

查看 method DELETE-POS and method DELETE-KEY 了解底层机制.

58.10.3. :p

以 Pair 的形式,返回元素的`索引/键` 和元素值, 并默默跳过不存在的元素:

my @tens = 0, 10, 20, 30;
dd @tens[1]:p;        #-> 1 => 10
dd @tens[0, 4, 2]:p;  #-> (0 => 0, 2 => 20)

my %month = Jan => 1, Feb => 2, Mar => 3;
dd %month<Feb>:p;          #-> "Feb" => 2
dd %month<Jan Foo Mar>:p;  #-> ("Jan" => 1, "Mar" => 3)

如果你不想跳过不存在的元素, 使用否定形式:

dd %month<Jan Foo Mar>:!p;  #-> ("Jan" => 1, "Foo" => Any, "Mar" => 3)

能和 :exists 还有 :delete 组合。

也可以查看 pairs 子例程.

58.10.4. :kv

以列表的形式返回元素的`索引/键`和`值` , 并默默地跳过不存在的元素。 当作用在切片上时,返回值是一个展平的键和值交叉着的单个列表:

my @tens = 0, 10, 20, 30;
dd @tens[1]:kv;        #-> (1, 10)
dd @tens[0, 4, 2]:kv;  #-> (0, 0, 2, 20)

my %month = Jan => 1, Feb => 2, Mar => 3;
dd %month<Feb>:kv;          #-> ("Feb", 2)
dd %month<Jan Foo Mar>:kv;  #-> ("Jan", 1, "Mar", 3)

如果你不想跳过不存在的元素, 使用否定形式:

dd %month<Jan Foo Mar>:!kv;  #-> ("Jan", 1, "Foo", Any, "Mar", 3)

这个副词一般用于遍历切片:

for %month<Feb Mar>:kv -> $month, $i {
    say "$month had {Date.new(2015, $i, 1).days-in-month} days in 2015"
}

能和 :exists 还有 :delete 组合。

也可以查看 kv 子例程.

58.10.5. :k

只返回元素的`索引/键` , 而不是它们的值, 并默默地跳过不存在的元素:

my @tens = 0, 10, 20, 30;
dd @tens[1]:k;        #-> 1
dd @tens[0, 4, 2]:k;  #-> (0, 2)

my %month = Jan => 1, Feb => 2, Mar => 3;
dd %month<Feb>:k;          #-> "Feb"
dd %month<Jan Foo Mar>:k;  #-> ("Jan", "Mar")

如果你不想跳过不存在的元素, 使用否定形式:

dd %month<Jan Foo Mar>:!k;  #-> ("Jan", "Foo", "Mar")

还可以查看 keys 子例程.

58.10.6. :v

返回元素的裸值(不是有可能返回一个可变值容器),并默默跳过不存在的元素:

my @tens = 0, 10, 20, 30;
dd @tens[1]:v;        #-> 10
dd @tens[0, 4, 2]:v;  #-> (0, 20)
@tens[3] = 31;        # OK
@tens[3]:v = 31;      # ERROR, cannot assign to immutable integer value

my %month = Jan => 1, Feb => 2, Mar => 3;
dd %month<Feb>:v;          #-> 2
dd %month<Jan Foo Mar>:v;  #-> (1, 3)

如果你不想跳过不存在的元素, 使用否定形式:

dd %month<Jan Foo Mar>:!v;  #-> (1, Any, 3)

还可以查看 values 子例程.

58.11. Custom types

这页描述的下标接口并不意味着和 Raku 的内置集合类型相排斥 - 你可以(并且应该)为任何想通过索引或键提供数据访问的自定义类型重用它们。

你不必手动重载 %20#postcircumfix_[_][postcircumfix [ ]] 和 postcircumfix { } 操作符并重新实现它们所有的戏法, 为了实现它, 相反,你可以依赖这个事实, 在幕后,它们的标准实现分派给了一个定义良好的底层方法集。例如:

当你这样写:	         这会在幕后调用如下底层方法:
%foo<aa>	        %foo.AT-KEY("aa")
%foo<aa>:delete	    %foo.DELETE-KEY("aa")
@foo[3,4,5]	        @foo.AT-POS(3), @foo.AT-POS(4), @foo.AT-POS(5)
@foo[*-1]	        @foo.AT-POS(@foo.elems - 1)

所以, 为了让你的下标工作, 你只需要为你的自定义类型实现或委托那些底层方法(下面描述详情)。

如果你这样做了, 你还应该让你的类型各自遵守 PositionalAssociative role

58.11.1. Custom type example

设想一下 HTTP::Header 类型,尽管它作为一个有特定行为的自定义类,却能像散列那样索引:

my $request = HTTP::Request.new(GET => "raku.org");
say $request.header.WHAT;  #-> (HTTP::Header)

$request.header<Accept> = "text/plain";
$request.header{'Accept-' X~ <Charset Encoding Language>} = <utf-8 gzip en>;
$request.header.push('Accept-Language' => "fr");  # like .push on a Hash

say $request.header<Accept-Language>.perl;  #-> ["en", "fr"]

my $rawheader = $request.header.Str;  # stringify according to HTTP spec

实现这个类的最简单的方法是,给它一个 Hash 类型的属性,并把所有的下标和迭代相关功能性委托给那个属性。(使用一个自定义类型约束来确保使用者不会在里面插入任何不合法的值):

class HTTP::Header does Associative is Iterable {
    subset StrOrArrayOfStr where Str | ( Array & {.all ~~ Str} );

    has %!fields of StrOrArrayOfStr
                 handles <AT-KEY EXISTS-KEY DELETE-KEY push
                          iterator list kv keys values>;

    method Str { #`[not shown, for brevity] }
}

然而, HTTP header 字段名被认为是大小写无关的(更偏好驼峰法)。我们可以通过把 *-keypush 方法拿到 handles 列表的外面来容纳它, 并像这样各自实现它们:

method AT-KEY     ($key) is rw { %!fields{normalize-key $key}        }
method EXISTS-KEY ($key)       { %!fields{normalize-key $key}:exists }
method DELETE-KEY ($key)       { %!fields{normalize-key $key}:delete }
method push (*@_) { #`[not shown, for brevity] }

sub normalize-key ($key) { $key.subst(/\w+/, *.tc, :g) }

注意下标 %!fields 返回一个适当的 rw 容器, 而我们的 AT-KEY 能够简单地传递。

然而, 我们可能倾向于少一点对用户输入的限制, 相反我们自己关心字段值的消毒。那种情况下,我们可以移除 %!fields 上的 StrOrArrayOfStr 类型约束, 并在赋值时使用返回自定义的关心消毒值的 Proxy 容器来替换我们的 AT-KEY 实现:

multi method AT-KEY (::?CLASS:D: $key) is rw {
    my $element := %!fields{normalize-key $key};

    Proxy.new(
        FETCH => method () { $element },

        STORE => method ($value) {
            $element = do given $value».split(/',' \s+/).flat {
                when 1  { .[0] }    # a single value is stored as a string
                default { .Array }  # multiple values are stored as an array
            }
        }
    );
}

注意把方法声明为 multi 并把它限制为 :D (defined invocants) 确保未定义情况被传递给由 Any(这在自动复活中被调用) 提供的默认实现。(我去,翻译不来哦!)

58.11.2. Methods to implement for positional subscripting

为了通过 %20#postcircumfix_[_][postcircumfix [ ]] 让基于索引的下标在你的自定义类型中工作,你应该至少实现下面的 elems, AT-POSEXISTS-POS- 还有其它可选项。

elems 方法
multi method elems (::?CLASS:D:)

预期返回一个数字,用于表明对象中有多少个可标注的元素。 可能被用户直接调用, 并且当从末尾索引元素的时候, 还会被 postcircumfix [ ] 调用, 就像 @foo[*-1] 中那样。

如果没有实现这个方法, 你的类型会从 Any 继承默认的实现, 对定义过的调用者这总是返回 1 - 这最不可能是你想要的。 所以, 如果不能从你的位置类型知晓元素的个数, 那就添加一个 fails 或 dies 实现, 以避免沉默地做了错事。

AT-POS 方法
multi method AT-POS (::?CLASS:D: $index)

期望返回 $index 位置处的元素。这就是 postcircumfix [ ] 通常调用的方法。 如果你想让元素可变(像它们用于 Array 类型那样), 你就必须确保以 item 容器的形式返回它, 并在被赋值时更新它。(记得使用 return-rwrw 子例程 trait 以使它工作; 查看例子。)

EXISTS-POS 方法
multi method EXISTS-POS (::?CLASS:D: $index)

返回一个布尔值以表明在 $index 位置处是否有元素。这就是引用 @foo[42]:exists 时, postcircumfix [ ] 所调用的方法。

元素"存在"意味着什么, 取决于你的类型。

如果你没有实现它, 你的类型会从 Any 那儿继承默认的实现, 对于索引 0 它会返回 True, 对于 其它索引它会返回 false — 这可能不是你想要的。所以如果你的类型不能做元素存在检测, 那就添加一个 fails 或 die 实现, 以避免静默地做错事情。

DELETE-POS 方法
multi method DELETE-POS (::?CLASS:D: $index)

删除 $index 处的元素, 并返回它所删除的这个元素。这就是引用 @foo[42]:delete 时, postcircumfix [ ] 所调用的方法。

"删除"元素的意思是什么, 取决于你的类型。

实现这个方法是可选的; 如果你没有实现它, 那么用户尝试从这种类型的对象中删除元素会得到一个合适的错误信息。

ASSIGN-POS 方法
multi method ASSIGN-POS (::?CLASS:D: $index, $new)

$index 位置处的元素设置为 $new 值。实现这个方法完全是可选的; 如果你没有实现这个方法, 那么会使用 self.AT-POS($index) = $new 代替, 如果你确实实现了该方法, 那么确保它拥有相同的效果。

这意味着 opt-in 性能优化, 以至于简单的诸如 @numbers[5] = "five" 的赋值能在不调用 AT-POS(这必须创建并返回一个潜在的昂贵的容器对象) 方法时操作。

注意, 实现 ASSIGN-POS 不能解除让 AT-POS 变成一个 rw 方法, 因为诸如 @numbers[5]++ 的不太重要的赋值/修改 仍旧会使用 AT-POS

BIND-POS 方法
multi method BIND-POS (::?CLASS:D: $index, \new)

把值或容器 new 绑定给位置 $index 处的插槽上, 替换那儿能找到的任何容器。这是当你这样写的时候所调用的东西:

my $x = 10;
@numbers[5] := $x;

一般的数组类支持这以允许创建复杂的链接数据结构, 但是对于更特定领域类型它可能没有意义, 所以不强求去实现它。如果你没有实现该方法, 用户会获得一个合适的错误信息, 当它们尝试绑定到这种类型的对象的一个位置插槽上时。

58.11.3. Methods to implement for associative subscripting

为了通过 postcircumfix { } 让基于键的下标能够工作于你的自定义类型中, 你应该至少实现 AT-KEYEXISTS-KEY — 还有可选地实现下面的方法。

AT-KEY 方法
multi method AT-KEY (::?CLASS:D: $key)

返回和 $key 相关联的元素。这正是 postcircumfix { } 通常所调用的方法。

如果你想让元素可变(就像它们是为了内置的 Hash 类型), 你必须确保以 item 容器的形式返回它, 并在被赋值时更新它。(记得使用 return-rwis rw 子例程 trait 以使其有效; 查看例子。)

另一方面, 如果你想让你的集合只读, 请直接返回非容器值。

EXISTS-KEY 方法
multi method EXISTS-KEY (::?CLASS:D: $key)

返回一个布尔值以表明和 $key 相关联的元素是否存在。这就是引用 %foo<aa>:exists 时, postcircumfix { } 所调用的方法。

元素"存在"意味着什么, 取决于你的类型。

如果你没有实现它, 你的类型会从 Any 那儿继承默认的实现, 这通常返回 False — 这可能不是你想要的。所以如果你的类型不能做元素存在检测, 那就添加一个 fails 或 die 实现, 以避免静默地做错事情

DELETE-KEY 方法
multi method DELETE-KEY (::?CLASS:D: $key)

删除和 $key 相关联的元素, 并返回它所删除的这个元素。这就是引用 %foo<aa>:delete 时, postcircumfix { } 所调用的方法。

"删除"元素的意思是什么, 取决于你的类型 — 尽管它通常让 EXISTS-KEY 因为那个键变为 False

实现这个方法是可选的; 如果你没有实现它, 那么用户尝试从这种类型的对象中删除元素会得到一个合适的错误信息。

ASSIGN-KEY 方法
multi method ASSIGN-KEY (::?CLASS:D: $key, $new)

把和 $key 相关联的元素设置为 $new 值。实现这个方法完全是可选的; 如果你没有实现这个方法, 那么会使用 self.AT-KEY($key) = $new 代替, 如果你确实实现了该方法, 那么确保它拥有相同的效果。

这意味着 opt-in 性能优化, 以至于简单的诸如 %age<Claire> = 29 的赋值能在不调用 AT-KEY(这必须创建并返回一个潜在的昂贵的容器对象) 方法时操作。

注意, 实现 ASSIGN-KEY 不能解除让 AT-KEY 变成一个 rw 方法, 因为诸如 %age<Claire>++ 的不太重要的赋值/修改 仍旧会使用 AT-KEY

BIND-KEY 方法
multi method BIND-KEY (::?CLASS:D: $key, \new)

把值或容器 new 绑定给跟 $key 相关联的插槽上, 替换那儿能找到的任何容器。这是当你这样写的时候所调用的东西:

my $x = 10;
%age<Claire> := $x;

一般的散列类支持这以允许创建复杂的链接数据结构, 但是对于更特定领域类型它可能没有意义, 所以不强求去实现它。如果你没有实现该方法, 用户会获得一个合适的错误信息, 当它们尝试绑定到这种类型的对象的一个位置插槽上时。

59. Unicode 和 ASCII 符号

可以在 Raku 中使用以下 Unicode 符号,而无需加载任何其他模块。其中一些具有可以使用 ASCII 独有字符键入的等效物。这些变体通常由比 Unicode 版本更多的字符组成,因此它们看起来更大。

下面参考 unicode 码点的各种属性。最终列表可以在这里找到:https://www.unicode.org/Public/UCD/latest/ucd/PropList.txt.

59.1. 字母字符

任何具有 Ll(字母,小写),Lu(字母,大写),Lt(字母,标题),Lm(字母,修饰符)或 Lo(字母,其他)属性的代码点都可以像任何其他字母一样使用 ASCII 范围内的字符。

59.2. 数字字符

任何具有 Nd(数字,十进制数字)属性的代码点都可以用作任何数字的数字。例如:

my $var = 19; # U+FF11 U+FF19
say $var + 2;  # OUTPUT: «21␤»

59.3. 数字值

任何具有 No(Number,other)或 Nl(Number,letter)属性的代码点都可以单独用作数值,例如 ½ 和 ⅓。 (这些不是十进制数字,因此不能组合。)例如:

my $var = ⅒ + 2 + Ⅻ; # here ⅒ is No and Rat and Ⅻ is Nl and Int
say $var;            # OUTPUT: «14.1␤»

59.4. 空白字符

除了空格和制表符,您还可以使用具有 Zs(分隔符,空格),Zl(分隔符,行)或 Zp(分隔符,段落)属性的任何其他 unicode 空白字符。

59.5. 其它可接受的单代码点

此列表包含 Raku 中具有特殊含义的单个代码点及其 ASCII 等价物。

Symbol

Codepoint

ASCII

Remarks

«

U+00AB

<<

作为 «» 或 .« 的一部分, 或正则表达式左单词边界

¯

U+00AF

-

(必须使用显式的数字) 作为幂的一部分 (长音符号是减号的另一种写法)

²

U+00B2

**2

可以与 ⁰..⁹ 结合

³

U+00B3

**3

可以与 ⁰..⁹ 结合

¹

U+00B9

**1

可以与 ⁰..⁹ 结合

»

U+00BB

>>

作为 «» 或 .« 的一部分, 或正则表达式右单词边界

×

U+00D7

*

÷

U+00F7

/

π

U+03C0

pi

3.14159_26535_89793_238e0

τ

U+03C4

tau

6.28318_53071_79586_476e0

U+2018

'

作为 ‘’ 或 ’‘ 的一部分

U+2019

'

作为 ‘’ 或 ‚’ 或 ’‘ 的一部分

U+201A

'

作为 ‚‘ 或 ‚’ 的一部分

U+201C

"

作为 “” 或 ”“ 的一部分

U+201D

"

作为 “” 或 ”“ 或 ”” 的一部分

U+201E

"

作为 „“ 或 „” 的一部分

U+2026

…​

U+2070

**0

可以与 ⁰..⁹ 结合

U+2074

**4

可以与 ⁰..⁹ 结合

U+2075

**5

可以与 ⁰..⁹ 结合

U+2076

**6

可以与 ⁰..⁹ 结合

U+2077

**7

可以与 ⁰..⁹ 结合

U+2078

**8

可以与 ⁰..⁹ 结合

U+2079

**9

可以与 ⁰..⁹ 结合

U+207A

|

(必须使用显式的数字) 作为幂的一部分

U+207B

-

(必须使用显式的数字) 作为幂的一部分

U+2205

set()

(empty set)

U+2208

(elem)

U+2209

!(elem)

U+220B

(cont)

U+220C

!(cont)

U+2212

-

U+2216

(-)

U+2218

o

U+221E

Inf

U+2229

(&)

U+222A

(|)

U+2245

=~=

U+2260

!=

U+2264

U+2265

>=

U+2282

(<)

U+2283

(>)

U+2284

!(<)

U+2285

!(>)

U+2286

(⇐)

U+2287

(>=)

U+2288

!(⇐)

U+2289

!(>=)

U+228D

(.)

U+228E

(+)

U+2296

(^)

𝑒

U+1D452

e

2.71828_18284_59045_235e0

U+FF62

Q//

作为 「」 的一部分 (注意: Q// 变体不能裸用在正则表达式中)

U+FF63

Q//

作为 「」 的一部分 (注意: Q// 变体不能裸用在正则表达式中)

59.5.1. 原子运算符

原子运算符将 U+269B ⚛ ATOM SYMBOL 合并到其中。它们的 ASCII 等价物是普通的子程序,而不是运算符:

my atomicint $x = 42;
$x⚛++;                # Unicode version
atomic-fetch-inc($x); # ASCII version

ASCII 替代方案如下:

Symbol

ASCII

Remarks

⚛=

atomic-assign

atomic-fetch

this is the prefix:<⚛> operator

⚛+=

atomic-add-fetch

⚛-=

atomic-sub-fetch

⚛−=

atomic-sub-fetch

this operator uses U+2212 minus sign

++⚛

atomic-inc-fetch

⚛++

atomic-fetch-inc

--⚛

atomic-dec-fetch

⚛--

atomic-fetch-dec

59.6. 多代码点

此列表包含多个代码点运算符,这些运算符需要对其 ASCII 等价物项进行特殊组合。请注意,代码点以空格分隔显示,但在使用时应作为相邻代码点输入。

Symbol

Codepoints

ASCII

Since

Remarks

»=»

U+00BB = U+00BB

>>[=]>>

v6.c

uses ASCII '='

«=«

U+00AB = U+00AB

<<[=]<<

v6.c

uses ASCII '='

«=»

U+00AB = U+00BB

<<[=]>>

v6.c

uses ASCII '='

»=«

U+00BB = U+00AB

>>[=]<<

v6.c

uses ASCII '='

60. 异常

Raku 中的异常是保存有关错误信息的对象。例如,错误可能是意外接收数据或网络连接不再可用,或者丢失文件。异常对象存储的信息是关于错误条件的人类可读消息,错误引发的回溯等等。

所有内置异常都继承自 Exception,它提供了一些基本行为,包括回溯的存储和回溯打印机的接口。

60.1. 热异常

通过调用带有描述错误的 die 函数来使用热异常:

die "oops, something went wrong";
# RESULT: «oops, something went wrong in block <unit> at my-script.p6:1␤»

值得注意的是,die 会将错误消息打印到标准错误 $*ERR

60.2. 类型化的异常

类型化异常提供有关异常对象中存储的错误的更多信息。

例如,如果在对象上执行 .zombie copy 时,所需的路径 foo/bar 变得不可用,则可以引发 X::IO::DoesNotExist异常:

die X::IO::DoesNotExist.new(:path("foo/bar"), :trying("zombie copy"))

# RESULT: «Failed to find 'foo/bar' while trying to do '.zombie copy'
#          in block <unit> at my-script.p6:1»

请注意对象如何为回溯提供有关出错的信息。代码的用户现在可以更轻松地找到并纠正问题。

60.3. 捕获异常

通过提供 CATCH 块可以处理异常情况:

die X::IO::DoesNotExist.new(:path("foo/bar"), :trying("zombie copy"));

CATCH {
    when X::IO { $*ERR.say: "some kind of IO exception was caught!" }
}

# OUTPUT: «some kind of IO exception was caught!»

在这里,我们说如果发生 X::IO 类型的任何异常,那么消息 some kind of IO exception was caught! 会被发送到 stderr,这是 $*ERR.say 所做的事情,在那一刻构成标准错误设备的任何内容上显示,默认情况下可能是控制台。

CATCH 块使用类似于 given/when 对选项进行智能匹配的智能匹配,因此可以捕获和处理 when 块内的各种类别的异常。

要处理所有异常,请使用 default 语句。此示例打印出与普通回溯打印机几乎相同的信息。

CATCH {
     default {
         $*ERR.say: .message;
         for .backtrace.reverse {
             next if .file.starts-with('SETTING::');
             next unless .subname;
             $*ERR.say: "  in block {.subname} at {.file} line {.line}";
         }
     }
}

请注意,匹配目标是一个角色。要允许用户定义的异常以相同的方式匹配,它们必须实现给定的角色。仅存在于同一名称空间中看起来相似但在 CATCH 块中不匹配。

60.4. 异常处理程序和闭合块

CATCH 处理异常之后,退出包围 CATCH 块的块。

换句话说,即使成功处理异常,封闭块中的其余代码也永远不会被执行。

die "something went wrong ...";

CATCH {
    # will definitely catch all the exception
    default { .Str.say; }
}

say "This won't be said.";   # but this line will be never reached since
                             # the enclosing block will be exited immediately
# OUTPUT: «something went wrong ...␤»

和这个作对比:

CATCH {

  CATCH {
      default { .Str.say; }
  }

  die "something went wrong ...";

}

say "Hi! I am at the outer block!"; # OUTPUT: «Hi! I am at the outer block!␤»

有关如何将控制权返回到发生异常的位置,请参阅恢复异常

60.5. try 块

try 块是一个普通块,它隐式打开 use fatal pragma 编译指示,并包含一个隐式 CATCH 块,它会删除异常,这意味着您可以使用它来包含它们。 捕获的异常存储在$中! 变量,它包含 Exception 类型的值。

像这样的普通块将会失败:

{
    my $x = +"a";
    say $x.^name;
} # OUTPUT: «Failure␤»

但是,try 块将包含异常并将其放入 $! 变量:

try {
    my $x = +"a";
    say $x.^name;
}

if $! { say "Something failed!" } # OUTPUT: «Something failed!␤»
say $!.^name;                     # OUTPUT: «X::Str::Numeric␤»

在这样的块中抛出的任何异常都将被 CATCH 块捕获,无论是隐式的还是由用户提供的。在后一种情况下,任何未处理的异常都将被重新抛出。如果您选择不处理异常,则它们将被块包含。

try {
    die "Tough luck";
    say "Not gonna happen";
}

try {
    fail "FUBAR";
}

在上面的两个 try 块中,异常将包含在块中,但不会运行 say 语句。但我们可以处理它们:

class E is Exception { method message() { "Just stop already!" } }

try {
    E.new.throw; # this will be local

    say "This won't be said.";
}

say "I'm alive!";

try {
    CATCH {
        when X::AdHoc { .Str.say; .resume }
    }

    die "No, I expect you to DIE Mr. Bond!";

    say "I'm immortal.";

    E.new.throw;

    say "No, you don't!";
}

这会输出:

I'm alive!
No, I expect you to DIE Mr. Bond!
I'm immortal.
Just stop already!
  in block <unit> at exception.p6 line 21

由于 CATCH 块只处理 die 语句抛出的 X::AdHoc 异常,而不处理 E 异常。 如果没有 CATCH 块,所有异常都将被包含和删除,如上所示。 恢复将在异常抛出后立即恢复执行; 在这种情况下,在 die 语句中。 有关详细信息,请参阅有关恢复异常的部分。

try-block 是一个普通的块,因此将其最后一个语句视为自身的返回值。 因此,我们可以将其用作右手边。

say try { +"99999" } // "oh no"; # OUTPUT: «99999␤»
say try { +"hello" } // "oh no"; # OUTPUT: «oh no␤»

通过返回表达式的返回值来间接尝试块支持 else 块,如果抛出异常,则返回 Nil

with try +"♥" {
    say "this is my number: $_"
} else {
    say "not my number!"
}
# OUTPUT: «not my number!␤»

try 也可以和语句一块用而非块:

say try "some-filename.txt".IO.slurp // "sane default";
# OUTPUT: «sane default␤»

try 实际导致的是,通过 use fatal pragma,立即抛出在其范围内发生的异常,但通过这样做,从抛出异常的点调用 CATCH 块,定义其范围。

my $error-code = "333";
sub bad-sub {
    die "Something bad happened";
}
try {
    my $error-code = "111";
    bad-sub;

    CATCH {
        default {
            say "Error $error-code ", .^name, ': ',.Str
        }
    }
}
# OUTPUT: «Error 111 X::AdHoc: Something bad happened␤»

60.6. 抛出异常

可以使用Exception对象的 .throw 方法显式抛出异常。

此示例抛出 AdHoc 异常,捕获它并允许代码通过调用 .resume 方法从异常点继续。

{
    X::AdHoc.new(:payload<foo>).throw;
    "OHAI".say;
    CATCH {
        when X::AdHoc { .resume }
    }
}

"OBAI".say;

# OUTPUT: «OHAI␤OBAI␤»

如果 CATCH 块与抛出的异常不匹配,则将异常的有效负载传递给回溯打印机制。

{
    X::AdHoc.new(:payload<foo>).throw;
    "OHAI".say;
    CATCH {  }
}

"OBAI".say;

# RESULT: «foo
#          in block <unit> at my-script.p6:1»

下一个示例不会从异常点恢复。相反,它会在封闭块之后继续,因为捕获了异常,然后在 CATC H块之后控制继续。

{
    X::AdHoc.new(:payload<foo>).throw;
    "OHAI".say;
    CATCH {
        when X::AdHoc { }
    }
}

"OBAI".say;

# OUTPUT: «OBAI␤»

throw 可以被视为 die 的方法形式,只是在这种特殊情况下,例程的 sub 和 method 形式有不同的名称。

60.7. 异常恢复

异常会中断控制流并将其从抛出语句后的语句中转移出去。可以恢复用户处理的任何异常,并且控制流将继续使用抛出异常的语句之后的语句。为此,请在异常对象上调用方法 .resume

CATCH { when X::AdHoc { .resume } }         # this is step 2

die "We leave control after this.";         # this is step 1

say "We have continued with control flow."; # this is step 3

恢复将在导致异常的语句之后和最里面的调用帧中发生

sub bad-sub {
    die "Something bad happened";
    return "not returning";
}

{
    my $return = bad-sub;
    say "Returned $return";
    CATCH {
        default {
            say "Error ", .^name, ': ',.Str;
            $return = '0';
            .resume;

        }
    }
}
# OUTPUT:
# Error X::AdHoc: Something bad happened
# Returned not returning

在这种情况下,.resume 将转到在 die 语句之后发生的 return 语句。请注意,$return 的赋值不起作用,因为 CATCH 语句发生在对 bad-sub 的调用中,bad-sub 通过 return 语句为其分配不返回的值。

60.8. 未捕获的异常

如果抛出异常但未捕获异常,则会导致程序以非零状态代码退出,并且通常会将消息输出到程序的标准错误流。通过在异常对象上调用 gist 方法获得此消息。您可以使用它来抑制打印回溯的默认行为以及消息:

class X::WithoutLineNumber is X::AdHoc {
    multi method gist(X::WithoutLineNumber:D:) {
            $.payload
    }
}
die X::WithoutLineNumber.new(payload => "message")

# prints "message\n" to $*ERR and exits, no backtrace

60.9. 控制异常

某些关键字会引发控制异常,并自动或由相应的 phaser 处理。任何未处理的控制异常都将转换为正常异常。

{ return; CATCH { default { $*ERR.say: .^name, ': ',.Str } } }

# OUTPUT: «X::ControlFlow::Return: Attempt to return outside of any Routine␤»
# was CX::Return

61. Traits

在 Raku 中,*traits*是附加到对象和类的编译器钩子,它们修改了类和对象的默认行为,功能或表示。作为这样的编译器钩子,它们是在编译时定义的,尽管它们可以用于运行时。

通过使用 trait_mod 关键字,已经将几个 traits 定义为语言或 Rakudo 编译器的一部分。接下来列出并解释它们。

61.1. is trait

定义为

proto sub trait_mod:<is>(Mu $, |) {*}

is 适用于任何类型的标量对象,并且可以接收任意数量的命名参数或位置参数。它是最常用的 trait,取决于第一个参数的类型,采用以下形式。

61.1.1. is 应用于类

最常见的形式涉及两个类,一个正在定义,另一个现有,定义为 defines parenthoodA is B, 如果两个都是类,则将 A 定义为 B 的子类。

is DEPRECATED 可以应用于类,属性或例程,将它们标记为已弃用并发出警告消息(如果提供了的话)。

is 的几个实例被直接转换为它们引用的类的属性:rwnativesizectypeunsignedhiddenarray_type

不可实例化的表示 trait 与表示没有多大关系,与特定类可以做什么有关; 它有效地防止以任何可能的方式创建类的实例。

constant @IMM = <Innie Minnie Moe>;

class don't-instantiate is repr('Uninstantiable') {
    my $.counter;

    method imm () {
        return @IMM[ $.counter++ mod @IMM.elems ];
    }
}
say don't-instantiate.imm for ^10;

不能实例化的类仍然可以通过它们的类变量和方法使用, 如上所示。尝试这样: my $do-instantiate = don’t-instantiate.new; 来实例化它们会产生错误。

61.1.2. is repr 和原生表示

由于 is trait 通常指的是它们所应用的类或对象的性质,因此它们在link:(原生调用)中被广泛使用,以指定将由原生函数通过 is repr 后缀处理的数据结构的表示。同时,is native 用于通过原生函数实际实现的例程。这些是可以使用的表示:

  • CStruct 对应于 C 语言中的 struct。它是一种复合数据结构,包括不同的异构和低级数据结构;请参阅此示例和进一步说明。

  • 类似地,CPPStruct 对应于 C++ 中的 struct。但是,这是暂时是 Rakudo 特定的。

  • CPointer 是任何这些语言的指针。它是一个动态数据结构,必须在使用之前进行实例化,可用于其方法也是原生的类。

  • CUnion 将使用与 C 中的 union 相同的表示形式; 看一下这个例子

另一方面,P6opaque 是用于 Raku 中所有对象的默认表示。

class Thar {};
say Thar.REPR;    #OUTPUT: «P6opaque␤»

除非另有说明,否则元对象协议默认对每个对象和类使用它;因此,除非您有效地使用该接口,否则通常没有必要。

61.1.3. is 作用于例程

is trait 可用于定义方法和例程以建立优先级关联性。它们充当使用 trait_mod 定义的子元素,该元素将要添加的 trait 的类型和名称作为参数。在子例程的情况下,trait 将是添加跨越类和角色层次结构的功能的一种方式,或者甚至可以用于向独立定义的例程添加行为。

62. 元对象协议

62.1. 自省和 Raku 的对象系统

Raku 是构建在元对象层上面的。那意味着有些对象(元对象)控制着各种面向对象结构(例如类、roles、方法、属性、枚举,…)怎样去表现。

要感受类的元对象, 这儿有一个同样的例子出现2次: 一次一种 Raku中的普通声明, 一次通过元模型来表达:

class A {
    method x() { say 42 }
}
A.x(); # 42

对应于:

constant A := Metamodel::ClassHOW.new_type( name => 'A' );  # class A {
A.^add_method('x', my method x(A:) { say 42 });             # method x() .. .
A.^compose;                                                 # }

A.x(); # 42

(除了声明形式的运行在编译时, 后面这种形式不是)

对象后面的元对象能使用 `$obj.HOW`获取, 这儿的 HOW 代表着 Higher Order Workings(或者 HOW the *%@$ does this work?)。

这儿, 带有 `.^`的调用是元对象的调用, 所以 `A.^compose`是 `A.HOW.compose(A)`的简写。调用者也被传递到参数列表中, 以使它能够支持原型类型风格的类型系统, 那儿只有一个元对象。

就像上面的例子展示的那样, 所有的面向对象特性对使用者都是可获得的, 而不仅仅是编译器。实际上编译器就是使用元对象的这样的调用的。

62.2. 元对象(MetaObjects)

这些是内省的宏, 类似于方法调用。

元对象通常以 ALLCAPS(全大写)命名, 并且避免使用你自己的带有全大写名字的方法被认为是一个好的风格。这会避免和可能出现在未来版本中的任何元对象发生冲突。注意, 如果你必须使用带有全大写名字的方法的话, 把你的这个方法名字用引号引起来来间接安全地调用:

#| THIS IS A CLASS FOR SHOUTING THINGS
class MY-CLASSES-ARE-ALL-CAPS {
    method WHY { "I DON'T KNOW" }
}
my $c = MY-CLASSES-ARE-ALL-CAPS.new;
say $c.WHY      # "THIS IS A CLASS FOR SHOUTING THINGS"? 显示这?你在逗我!
say $c."WHY"()  # "I DON'T KNOW"

62.2.1. WHAT

类型的类型对象。例如 42.WHAT 返回 `Int`类型对象。

62.2.2. WHICH

对象的同一值。这能用于哈希和同一比较, 并且这是 `===`中缀操作符的实现方式。

> "a".WHICH
Str|a

62.2.3. WHO

支持对象的包

> "a".WHO
Str

62.2.4. WHERE

对象的内存地址。注意这在移动的/紧凑的垃圾回收实现中是不稳定的。 在稳定的同一指示器中使用 WHERE

62.2.5. HOW

元类对象(the metaclass object):“Higher Order Workings”。

62.2.6. WHY

附加的 Pod 值。

62.2.7. DEFINITE

对象有一个有效的强制表现。

对于实例返回 True, 对于类型对象返回 False

> 42.DEFINITE
True
> Int.DEFINITE
False

62.2.8. VAR

返回底层的 Scalar 对象, 如果有的话。

62.3. 元对象系统的结构

对于每个类型声明符关键字, 例如 classroleenummodulepackagegrammar 或`subset`, 就有一个独立的元类在 Matamodel::`命名空间中。(Rakudo 在 `Raku::Metamodel::`命名空间中实现了它们, 然后把 `Raku::Metamodel`映射到 `Metamodel)。

这些元类(meta classes)中的很多都共享公共的功能。例如 roles、grammars和 classes(类)都能包括方法和属性, 还能遵守 roles。这个共享的功能是在 roles 中实现的, 它被组合进合适的元类中。例如 role Metamodel::RoleContainer实现了类型能处理 roles 和 `Metamodel::ClassHOW`的功能, 它是在 `class`关键字后面的元类, 遵守了这个 role。

62.4. Bootstrapping concerns

你可能想知道为什么 `Metamodel::ClassHOW`可以是一个类, 当按照`Metamodel::ClassHOW`作为一个类被定义时, 或者 roles 负责 role 处理的怎么能是 roles。答案是通过魔法。

开玩笑啦。自举是特别实现的。Rakudo 使用语言的对象系统来实现自举, 它恰好(几乎)就是 Raku 的一个子集: NQP, Not Quite Perl。 NQP 有原始的, class-like 叫做 konwhow 的性质, 它用于自举它自己的类和 roles 实现。`konwhow`建立在NQP 提供的虚拟机的原始基础上。

因为元对象是根据低级(low-level)类型引导的, 自省有时能返回低级(low-level)类型而非你期望的那个类型, 例如返回一个 NQP-level 的子例程而非普通的 `Routine`对象, 或返回一个引导的属性而非Attribute

62.5. 组合和静态推理

在 Raku中, 类型是在解析时被构造的, 所以在开始, 它必须是可变的。然而, 如果所有类型一直是可变的, 那么关于类型的所有推断会在任何类型的修改时变得无效。例如父类的列表因此类型检测的结果能在那个时候改变。

所以为了获得这两个世界中最好的东西, 当类型从可变转为不可变时是好时机。这就叫做组合, 并且对于从句法构成上声明的类型, 它发生在类型声明被完全解析时(所以总是在闭合花括号被解析时)。

如果你通过元对象系统直接创建类型, 你必须要在它们身上调用 .^compose, 在它们变得完全起作用之前。

很多元类也使用组合时来计算一些诸如方法解析顺序这样的属性, 发布一个方法缓存, 和其它清扫任务。在它们被组合之后干预类型有时是可能的, 但通常是造成灾难的因素。 不要那样做。

62.6. 能力和责任

元对象协议提供了很多常规 Raku 代码故意限制了的能力, 例如调用类中不信任你的私有方法, 窥探私有属性, 和其它通常不能完成的东西。

常规的 Raku 代码有很多就地的安全检测; 元模型中不是这样,它靠近底层的虚拟机, 违反和虚拟机的约定可以导致所有奇怪的行为, 而在正常代码中, 显而易见的会是 bugs。

所以, 在写元类型的时候要格外小心和思考。

62.7. 能力、便利和陷阱

元对象协议被设计的强大到实现 Raku 的对象系统。这种能力间或花费了便利的代价。

例如, 当你写了 my $x = 42`并在 `$x 上调用方法时, 大部分方法会在整数 42 上起作用, 而不是在存储 42 的标量容器上。这是 Raku中设立的一块便利。元对象协议中的大部分不能提供自动忽略标量容器的便利性, 因为它们也用于实现那些标量容器。 所以, 如果你写了 my $t = MyType; …​ $t.^compose, 那么你正组合那个`$` 变量表明的标量, 而不是 MyType

结果就是你需要很详尽的理解 Raku 的底层以避免陷阱, 当使用 MOP 时, 并且不能期望得到和普通 Raku 代码提供的 "do what I mean" 的便利。

63. Raku 中的换行处理

不同的操作系统使用不同的字符或字符的组合来表示换行符。每种语言都有自己的一套规则来处理这个问题。 Raku 有以下几个规则:

  • 字符串字面量中的 \n 表示 Unicode 代码点 10。

  • say 附加到字符串的默认 nl-out 也是 \n

  • 在输出时,当在 Windows 上时,编码器默认将 \n 转换为 \r\n,当它转到文件,进程或终端时(但它不会在套接字上执行此操作)。

  • 在输入时,在任何平台上,解码器默认将 \r\n 标准化为 \n,以便从文件,进程或终端(同样不是套接字)输入。

  • 以上两点一起意味着你可以 - 把套接字编程放在一边 - 期望永远不会在你的程序中看到 \r\n(这也是许多其他语言的工作原理)。

  • :$translate-nl 命名参数存在于控制此转换的各个位置,例如,在 Proc::Async.newProc::Async.Supply 中。

  • 正则表达式语言中的 \n 是合乎逻辑的,并且匹配 \r\n

您可以通过在创建该句柄时设置 :nl-out 属性来更改特定句柄的默认行为。

my $crlf-out = open(IO::Special.new('<STDOUT>'), :nl-out("\\\n\r"));
$*OUT.say: 1;     #OUTPUT: «1␤»
$crlf-out.say: 1; #OUTPUT: «1\␤␍»

在这个例子中,我们通过使用 IO::Special 将标准输出复制到新句柄,我们在字符串的末尾附加一个 \,然后是换行符 和回车符 ; 我们打印到该句柄的所有内容都会在行尾添加这些字符,如图所示。

在正则表达式中,\n 是根据逻辑换行符的Unicode定义定义的。它会匹配 ., 还有 \v,以及包含空格的任何类。

64. 术语

64.1. 匿名

子例程、方法或子方法,当它们不能通过名字调用时,就被称为匿名的

# named subroutine
sub double($x) { 2 * $x };
# 匿名子例程,存储在一个具名的标量里
my $double = sub ($x) { 2 * $x };

注意,匿名子例程仍然可以有名字

# 使用 anon 关键字使子例程匿名
my $s = anon sub triple($x) { 3 * $x }
say $s.name;        # triple

64.2. 副词

通常, 副词是函数的命名参数. 也有一些其它特殊语法形式允许副词出现在某些合适的地方:

q:w"foo bar"   # ":w" is a Quotelike form modifier adverb
m:g/a|b|c/     # ":g" is also
4 +> 5 :rotate # ":rotate" is an operator adverb
@h{3}:exists   # ":exists" is also, but is known as a subscript adverb

副词通常使用冒号对儿标记来表示, 因为这个原因, 冒号对儿标记法也以副词对儿形式著称:

:a(4)          # Same as "a" => 4

64.3. Autothreading

Autothreading 是这样的: 如果你传递一个 junction 给子例程, 该子例程期望的参数类型为`Any` 或它的子类型. 那么这个子例程调用会被执行多次, 每次使用一个不同的 junction 状态. 这些调用的结果被组合成一个跟原 junction 同类型的 junction.

sub f($x) { 2 * $x };
if f(1|2|3) == 4 {
    say 'success';
}

这里 f() 是含有一个参数的子例程,然而因为它没有显式的类型声明,它就被隐式的声明为 Any 型。 Junction 参数使 f(1|2|3) 调用在内部作为 f(1)|f(2)|f(3) 执行,而结果是跟原 junction 同类型的 junction , 即 2|4|6. 这种把一个 Junction 分成对多次函数调用的处理就叫做 autothreading.

64.4. Colon Pair 和 Colon List

冒号对儿是用于创建或 Pair 对象的便捷语法. 两种最常见的形式是:

:a(4)          # Same as "a" => 4,   same as Pair.new(:key<a>,:value(5))
:a<4>          # Same as "a" => "4", same as Pair.new(:key<a>,:value<5>)

这也是人们熟知的副词对儿形式. 注意, 当冒号后面括号前面的部分不是一个合法的标识符的时候, 会应用其它语义, 不是所有的副词对儿都创建 Pair 对象. 另外两个常见的形式是:

:a             # Same as :a(True)
:!a            # Same as :a(False)

一个 colon 列表是一个仅包含冒号对儿的列表, 不需要逗号, 甚至不需要空格:

:a(4):c:!d:c   # Same as a => 4, c => True, d => False, c => True

64.5. Constraint

约束是给参数或 subset 类型添加的限制. 通过单词 where 引入约束. 在下面的例子中, 约束用于确保 , 当调用一个名为 abbreviate 的子例程, 其参数为一个长度小于 10 个字符的字符串时,会抛出一个错误:

sub abbreviate (Str $thing where { .chars >= 10 }) { ... }

上例中的 Str 也是一个约束, 但是经常作为"类型约束".

64.6. Instance

类的实例在其它编程语言中也叫对象. 对象存储属性, 通常是 new 方法调用的返回值, 或者是对象字面量. 大部分类型的实例被定义为 True, 例如 defined($instance) 为 True.

my Str $str = "hello";  ## 这使用内建类型,例如 Str
if defined($str) {
    say "Oh, yeah. I'm defined.";
} else {
    say "No. Something off? ";
}
## if you wanted objects...
class A {
    # nothing here for now.
}
my $an_instance = A.new;
say $an_instance.defined.perl;# defined($an_instance) works too.

类拥有方法和属性的所有蓝图, 而类的实例把蓝图带到真实世界中.

64.7. Invocant

在 Raku 中调用方法的对象叫做调用者. 在方法中它就是 self 引用的东西.

say 'str'.uc;   # 'str' 是 方法 uc 的调用者

64.8. Literal

字面量是一块直接代表对象的代码, 通常指向对象自身.

my $x = 2;      # the 2 is a literal
say $x;         # $x is not a literal, but a variable

64.9. lvalue

lvalue 或者左值是能出现在赋值操作符左侧的任何东西; 典型的左值有变量,私有属性和 `is rw`属性, 变量列表和左值子例程。

左值的例子:

Declaration             lvalue          Comments
my $x;                  $x
my ($a, $b);            ($a, $b)
has $!attribute;        $!attribute     Only inside classes
has $.attrib is rw;     $.attrib
sub a is rw { $x };     a()

不是左值的例子:

3                        # literals
constant x = 3;          # constants
has $.attrib;            # attributes; you can only assign to $!attrib
sub f { }; f();          # "normal" subs are not writable
sub f($x) { $x = 3 };    # error - parameters are read-only by default

64.10. Mainline

mainline 是程序中不属于任何 block 的程序文本.

use v6;     # mainline
sub f {
            # not in mainline, in sub f
}
f();        # in mainline again

64.11. Slurpy

子例程或方法中的形参如果能接收任意数量的参数, 那这个形参就会被认为是 slurpy 的. 它由参数名字前面的星号标出.

sub sum (*@numbers) {
    return [+] @numbers;
}

64.12. Type Object

类型对象是一个代表类 /role/package/grammar/enum 的对象. 它通常和类型名相同.

class A { };
say A;              # A is the type object
my $x = A.new();    # same here
my $x = class {
    method greet() {
        say "hi";
    }
}

# $x now holds a type object returned from the
# anonymous class definition

65. FAQ

65.1. 通用

65.1.1. Rakudo 和 Raku 的区别是什么?

Rakudo 是 Raku 的一个实现。目前它是完成度最好的但是过去也有其它的实现, 将来也可能会有其它实现。Raku 是语言的定义。很多场合

这两个名字可以宽松地使用并互相替换。

65.1.2. 会有 Raku 版本 6.0.0 吗?

第一个稳定语言版本的版本称为 v6.c,而不是 6.0.0。 不同的命名方案使得不太可能发布具有精确版本 6.0.0 的语言。

您可以使用下面的代码检查您的 Rakudo 编译器是当前至少是什么版本(注意这可能不是真正的供应商二进制文件):

raku -e 'say q[too old] if $*PERL.version before Version.new(q[6.c])'

它首先由 Rakudo Raku 编译器版本的 2015.12 实现,并且可能通过使用 'use 6.c' 指令在可预见的未来支持后续版本。 下一个语言版本(无发布日期)为 v6.d.

65.1.3. 作为一个 Raku 初学者我应该安装什么?

如果你是一个 Linux 或 Mac 用户, 你可能需要下载 Rakudo Star 并通过编译 MoarVM 版本安装(一个简单的处理)

如果你是一个 Windows 32 或 64 位用户, 那么 Rakudo Star 二进制版本在 rakudo 网站也能获得。你需要 Windows Git 来使用 panda。

Linux 和 Mac 二进制版本稍后也可能从供应商和第三方那儿获取到。尽管供应商版本可能过时了。

或者有一个官方的 rakudo star Docker 镜像, 地址为 https://hub.docker.com//rakudo-star/

65.1.4. 作为一个中高级用户我想跟进 Rakudo 开发

安装类似于 Perl 5 的 perlbrew — rakudobrew , 同等的 Python 还有 Ruby 工具。

65.1.5. 从哪里能找到关于 Raku 的好文档?

最令人信赖的信息能在 raku.org 或那儿的直接链接。

你也可以使用 Google 搜索 Freenode #raku IRC 频道。

65.1.6. 什么是 Raku spec?

"spec" 指的是 Raku的官方测试套件。它被称作 roast 并被托管在 github 上.

它被用来测量一个 Raku 的实现有多彻底。

65.1.7. 有没有 Raku 的术语相关的项目?

查看 glossary

65.1.8. 我是一个 Perl 5 程序员. Perl 5 和 Raku 的区别在哪儿?

在 link: https://docs.raku.org/language/5to6-nutshell 下面查看 ‘5to6-nutshell’ pod 文档和相关页面。

65.2. 模块

65.2.1. Raku 有 CPAN 吗? 或者 Raku 会使用 Perl 5 的 CPAN 吗?

Raku 还没有像 CPAN 那样成熟的模块仓库. 但是 modules.raku.org 有很多已知的 Raku 模块, panda 能在 Rakudo 上安装这些模块.

65.2.2. 我能在 Raku 中使用 Perl 5的模块吗?

使用 Inline::Perl5 能让大部分 Perl 5 模块工作, 它甚至能很好地运行 Perl 5 的 Catalyst 和 DBI。

65.2.3. 我能在 Raku 中使用 C 和 C++ 吗?

Nativecall 让这个特别容易。

65.2.4. Nativecall 找不到 libfoo.so 并且我只有 libfoo.so.1.2!

这在 Debian 那样的系统中很常见。 你需要安装 "libfoo-dev" 来为丢失的文件设置符号链接。

65.2.5. 所有的传统 Unix 库函数去哪儿了?

使用 Nativecall 访问它们很容易。 POSIX 模块也可以。

65.2.6. Rakudo 有核心标准库吗?

Rakudo 是一个包含最小电量的编译器发布(Test 和 Nativecall等等),像 linux 内核一样。

Rakudo Star 是一个带有一些有用模块的 rakudo, 并且更多的模块可以从生态系统里安装。

65.2.7. 有像 B::Deparse 那样的东西吗?/我怎么抓住 AST?

使用 raku --target=ast -e 'very-short-example()' 来抓取编译单元的抽象语法树(AST)。

65.3. 语言特性

65.3.1. 我怎么 dump Raku 的数据结构(就像 Perl 5 的 Data::Dumper 和类似的)?

examples:

my $foo="bar"
dd $foo        # Str $foo = "bar"
say :$foo.perl # :foo("bar")
say :$foo.gist # foo => bar

生态系统中还有模块来做这个事情, 例如 Data::Dump 使用颜色来 Dump。

65.3.2. 我怎么在 Raku 提示符(REPL)中找到历史命令行?

从生态系统中安装 Linenoise.

作为一种选择, 在 UNIX 那样的系统中可以安装 rlwrap。这在类 Debian 系统中可以通过`apt-get install rlwrap` 安装。

65.3.3. 为什么 Rakudo 编译器有时候报错更友好?

如果在输出中出现 SORRY! , 则错误是编译时错误, 否则是运行时错误。

Examples:

say 1/0     # Attempt to divide 1 by zero using div

sub foo ( Int $a, Int $b ) {...}
foo(1)      # ===SORRY!=== Error while compiling ...

65.3.4. 什么是 (Any)?

Any 是一个用于新类的默认超类(superclass)的顶层类。 它经常在这样的上下文出现:变量被定义但没有被赋值, 这里它类似于其它语言中的 undef 或 null 值。

examples:

my $foo;
say $foo;       # (Any) 注意圆括号表明的类型对象
say $foo.^name  # Any

(Any) 不应该被用于检查 definedness。 在 Raku 中, definedness 可能是一个对象的属性。 通常实例是被定义的, 而类型对象是未定义的。

say 1.defined       # True
say (Any).defined   # False

65.3.5. so 是什么?

so 是一个松散优先级的操作符, 它强制上下文为 Bool.

so 拥有和 ? 前缀操作符同样的语义, 就像 and&& 的低优先级版本一样.

用法示例:

say so 1|2 == 2;    # Bool::True

在这个例子中, 比较的结果(结果是 Junction)在打印之前被转换为 Bool 值了.

65.3.6. 签名中的那些 :D 和 :U 是什么东东?

在 Raku 中, 类和其它类型是对象, 并且传递自身类型的类型检测。 例如如果你声明一个变量

my Int $x = 42;

那么, 你不仅可以给它赋值整数(即, Int 类的实例), 还能给它赋值 Int 类型对象自身:

$x = Int

如果你想排除类型对象, 你可以追加一个 :D 类型微笑符, 它代表"定义"(definite):

my Int:D $x = 42;
$x = Int;  # dies with:
           # Type check failed in assignment to $x;
           # expected Int:D but got Int

同样地, :U 约束为未定义的值, 即类型对象。 要显式地允许类型对象或实例, 你可以使用 :_

65.3.7. 签名中的 -→ 是什么东东?

-→ 是一个返回值约束, 要么是类型要么是有定义的值。

类型约束的例子:

sub divide-to-int( Int $a, Int $b --> Int ) {
        return ($a / $b).narrow;
}

divide-to-int(3, 2)
# Type check failed for return value; expected Int but got Rat

有明确返回值的例子:

sub discard-random-number( --> 42 ) { rand }
say discard-random-number
# 42

在这种情况下,最终值被抛弃,因为已经指定了返回值。

65.3.8. Any 和 Mu 的区别是什么?

Mu 是所派生出的所有其它类型的基类型. Any 是从 Mu`派生来的, 代表着任何类型的 Raku 值. 主要区别是, `Any 不包含 Junction.

子例程参数的默认类型是 Any, 以至于当你声明 sub foo ($a) 时, 你真正表达的是 sub foo (Any $a) . 类似地, 类的声明被假定继承自 Any, 除非使用了像 is Mu 这样的 trait 特征.

65.3.9. 怎么从 Junction 中提取值?

如果你想从 Junction 中提取值(特征态), 那你可能正误入歧途. 应该使用 Set 代替

Junctions 作为匹配器, 而不是使用它们做代数.

如果你还是想那样做, 你可以滥用自动线程(autothreading):

sub eigenstates(Mu $j) {
    my @states;
    -> Any $s { @states.push: $s }.($j);
    @states;
}

say eigenstates(1|2|3).join(', ');
# prints 1, 2, 3 or a permutation thereof

65.3.10. 如果 Str 是不可变的, 那么 s/// 是怎么工作的? 如果 Int 是不可变的, $i++ 是怎么工作的?

在 Raku 中, 很多基本类型是不可变的, 但是保存它们的变量不是. s/// 作用于变量上, 在这个变量中放入一个新创建的字符串对象. 同样地, $i++ 作用于 $i 变量上, 而不是作用在它里面的值身上.

更多详情请查看: containers 文档。

65.3.11. 什么是数组引用和自动解引用? 我仍然需要 @ 符号吗?

在 Raku 中, 几乎所有的东西都是引用. 所以谈论 taking references 没有多大意义. 不像 Perl 5 那样, Raku 的标量变量也能直接包含数组:

my @a = 1, 2, 3;
say @a;                 # "1 2 3\n"
say @a.WHAT;            # (Array)

my $scalar = @a;
say $scalar;            # "1 2 3\n"
say $scalar.WHAT;       # (Array)

最大的区别是, 标量中的数组在列表上下文中是一个值, 然而数组会被愉快地迭代:

my @a = 1, 2, 3;
my $s = @a;

for @a { ... }          # loop body executed 3 times
for $s { ... }          # loop body executed only once

my @flat = flat @a, @a;
say @flat.elems;        # 6

my @nested = flat $s, $s;
say @nested.elems;      # 2

你可以使用 @( …​ ) 或通过在表达式身上调用 .list 方法来强制展平, 使用 $( …​ ) 或通过在表达式身上调用 .item 方法强制为 item 上下文(不展平).

65.3.12. 为什么还要符号? 你不能没有它们吗?

有几个原因:

  • 它们使插值变量到字符串中变得更容易

  • 它们为不同的变量和 twigils 组成了微型命名空间, 因此避免了名字冲突

  • 它们允许简单的 单数/复数 区别

  • 它们像使用强制性名词标记的自然语言一样工作,所以我们的大脑为处理它而生

  • 它们不是强制性的,因为你可以声明无符号名字(如果你不介意含糊不清)

65.3.13. 类型 Str 不支持关联索引

你可能会把字符串插值和 HTML 搞混。

my $foo = "abc";
say "$foo<html-tag>";

Raku 认为 $foo 是一个散列而 <html-tag> 是一个字符串字面量的散列键。使用闭包来帮助你理解吧。

my $foo = "abc";
say "{$foo}<html-tag>";

65.3.14. Raku 有协程吗? 什么是 yield ?

Raku 没有 Python 那样的 yield 语句, 但是它通过惰性列表却能提供类似的功能. 有两种很潮的方式来写出能返回惰性列表的例程:

# first method, gather/take
my @values := gather while have_data() {
    # do some computations
    take some_data();
    # do more computations
}

# second method, use .map or similar method
# on a lazy list
my @squares := (1..*).map(-> $x { $x * $x });
# or
my @squares = (1..*).map(-> \x { x² });

65.3.15. 为什么我需要反斜线(unspace)在多行上分割方法调用?

(请在这儿添加答案)

65.3.16. 为什么我不能从 new 方法初始化私有属性, 我怎么修复它?

这样的代码:

class A {
    has $!x;
    method show-x {
        say $!x;
    }
}
A.new(x => 5).show-x;

不会打印出 5. Private 属性是私有的, 这意味着私有属性在外面是不可见的. 如果默认的构造器能够初始化私有属性, 那么这些私有属性就会泄露到公共 API 中.

如果你仍旧想让它工作, 你可以添加一个 submethod BUILD 来初始化它们:

class B {
    has $!x;
    submethod BUILD(:$!x) { }
    method show-x {
        say $!x;
    }
}
A.new(x => 5).show-x;

BUILD 由默认的构造器使用用户传递给构造器的所有具名参数调用(间接地, 更多细节查看Object Construction)。 :$!x 是名为 x 的具名参数, 当使用名为 x 的具名参数来调用时, 它的值被绑定到属性 $!x 上.

但不要这样做。如果名字是 public 的,使用 $.x 以那样的方式声明没有什么不好,因为默认情况下外部视图是只读的(readonly),你仍然可以使用 $!x 从内部访问它。

65.3.17. say, put 和 print 怎么不同, 为什么不同?

最明显的区别是, sayput 在输出后面添加了一个换行符, 而 print 没有.

但是还有另外一个区别: printput 通过对每一个传递来的 item 调用 Str 方法来把它的参数转换为字符串, 相反, say 使用 gist 方法. 前者是为计算机设计的, 后者是为人类.

或者它俩被解析的方式不同, $obj.Str 给出一个字符串表示, $obj.gist 是对象的一个简短总结, 适合编程人员的快速识别, $obj.perl 打印一个 Perlish 的表示.

例如, 类型对象, 也是熟知的 “未定义值”, 字符串化为一个空的字符串和警告, 而 gist 方法返回由一对圆括号包裹的类型的名字.(用于表明除了类型之外什么也没有).

my Date $x;     # $x now contains the Date type object
print $x;       # empty string plus warning
say $x;         # (Date)\n

所以, say 优化的用于调试和向人们展示, printput 更适合于产生用于其它程序的输出.

put 因此是 printsay 之间的一种混合; 像 print, 它的输出适合于其它程序, 也像 say, 它在输出的末尾添加了换行符。

65.3.18. token 和 rule 之间的区别是什么?

regex , tokenrule 这三个都引入了正则表达式, 但是语义略微有一点不同.

token 隐含了 :ratchet:r 修饰符, 这防止了 rule 的回溯.

rule 隐含了 :ratchet:sigspace (缩写为 :s)修饰符, 这意味着规则(rule)不会回溯, 并且它把 regex 的文本中的空白当作 <.ws> 调用(例如匹配空白, 除了在两个单词字符之间之外, 它是可选的). regex 开头的空白和备选分支中每个分支开头的空白会被忽略.

regex 声明一个简单的正则表达式,没有任何隐含的修饰符。

65.3.19. die 和 fail 之间的区别是什么?

die 抛出一个异常.

fail 返回一个 Failure 对象。 (如果调用者已经声明了 use fatal; 在调用作用域中, fail 会抛出一个异常而不返回)

Failure 是一个 “未知的” 或 “懒惰的” 异常.它是一个含有异常的对象, 当这个 Failure 被用作普通的对象或者在 sink 上下文中忽略它时, 则会抛出一个异常.

Failure 从 defined 检查中返回 False, 并且你可以使用 exception 方法提取出异常.

65.3.20. 为什么 wantarray 或 want 不见了? 我能在不同的上下文中返回不同的东西吗?

Perl 拥有 wantarray 函数来告诉你这是在空上下文, 标量上下文,还是在列表上下文中调用的. Raku 没有与之等价的结构, 因为上下文不是向内流动的, 例如, 子例程不知道调用所在的上下文.

一个愿意是因为 Raku 有多重分派, 在这样一个例子中:

multi w(Int $x) { say 'Int' }
multi w(Str $x) { say 'Str' }
w(f());

没办法决定子例程 f 的调用者想要一个字符串还是想要一个整数, 因为它还不知道调用者是什么. 通常这要求解决 halting 问题, 在这个问题上, 即使写 Raku编译器的人也会遇到麻烦.

在 Raku 中达到上下文敏感的方式是返回一个知道怎样响应方法调用的对象.

例如, regex 匹配返回 Match 对象, 该对象知道怎样响应列表索引, 散列索引, 并能变成匹配的字符串.

65.3.21. Pointer 和 OpaquePointer 的区别是声明?

OpaquePointer 被废弃了并且已经用 Pointer 代替了。

65.4. Raku 实现

65.4.1. 哪个 Raku 的实现是可用的?

当前开发最好的是 Rakudo(使用多个虚拟机后端)。历史上的实现还包括 Niecza (.NET) 和 Pugs (Haskell). 其它的列出在 Raku Compilers 下面。

65.4.2. Rakudo 是用什么语言写的?

NQP 是(1)NQP 代码,(2)底层虚拟机使用的任何语言,(3)一些第三方 C 和 Java 库,以及(4)早期运行构建过程创建的一些引导文件的混合 。

65.4.3. 为什么我不能把所有的数值都赋值给 Num 类型的变量?

my Num $x = 42;
# dies with
# Type check failed in assignment to '$x'; expected 'Num' but got 'Int'

Num 是浮点类型, 与 integers 不兼容. 如果你想要一个允许任何数字值的类型约束, 使用 Numeric (它也允许复数), 或 Real如果你想排除复数.

65.5. 元问题和宣传

65.5.1. Raku 什么时间会准备好? 就是现在吗?

编程语言和它们的编译器的准备就绪不是一个二元决策. 因为它们(语言和实现)能进化, 它们平稳地发展变得更可用. 根据你对编程语言的要求, 它可能适合也可能不适合你.

请查看 功能对比矩阵 了解更详尽的实现了的功能.

请注意, Larry Wall 已经在 FOSDEM 2015 会议上宣布, 一个产品级的 Rakudo Raku 将会在 2015 圣诞节发布.

65.5.2. 为什么我要学习 Raku? 它有什么了不起的吗?

Raku 统一了很多其它编程语言中不经常有的伟大想法. 虽然其中的几种语言提供了其中的某些功能, 但是没有提供全部.

不像大部分语言那样, 它提供了:

  • Raku 提供了过程式的, 面向对象的和函数式编程方法。

  • 易于使用的一致性语法, 数据结构中的符号不变性。

  • 完全基于字素的 Unicode 支持, 包括附件 #29

  • 足够清晰的正则表达式, 更易读, 更多功能。

  • Junctions 允许多个可能性的简单检测, 例如 $a == 1|3|42(意思是 $a 等于 1 或 3 或 42)

  • 相对于全局变量, 动态作用域变量提供了词法作用域备选

  • 强调可组合性和本地作用域以阻止「超距作用」。例如, imports 总是本地作用域的。

  • 易于理解的一致性作用域规则和闭包

  • 强大的面向对象, 含有类和 roles(所有的东西都可以当做对象)。继承、子类型、代码复用。

  • 内省到对象和元对象中(叠罗汉)

  • 元对象协议允许元编程而不需要生成/解析代码。

  • 子例程和方法签名,便于解包位置参数和命名参数。

  • 根据元数,类型和可选的额外代码使用不同的签名对同一具名子例程/方法进行多重分派。

  • 未知子例程/不可能的分派在编译时给出错误报告。

  • 可选的渐进类型检查,无需额外的运行时成本。 还有可选类型注解。

  • 基于对编译器/运行时状态的内省的高级错误报告。这意味着更有用,更精确的错误信息。

  • Phasers(如 BEGIN/END) 允许代码作用域 进入/退出, 首次循环/last/next 和其它更多上下文中执行。

  • 高级并发模型,用于隐式以及显式多进程处理,这超越了原始线程和锁。 Raku 的并发提供了一组丰富的(可组合的)工具。

  • 多核计算机越来越多地被使用,由于并行性使得 Raku 可以使用多核,包括隐式(例如使用>>.方法)和显式 (start {code}) 。这很重要,因为摩尔定律正在结束。

  • 提供结构化语言支持以实现异步执行代码的编程。

  • Supplies 允许在发生某些事情时执行代码(如定时器,信号或文件系统事件)。

  • react/whenever/supply 关键字允许容易地构建交互式,事件驱动的应用程序。

  • 懒惰求值,如果可能的话,急切求值当需要或必要时。这意味着,例如,惰性列表,甚至无限延迟列表,如斐波纳契序列或所有素数。

  • 原生数据类型用于更快的处理

  • 使用 NativeCall 连接到 C/C++ 中的外部库非常简单。

  • 使用 Inline::Perl5 和 Inline::Python 连接 Perl 5(CPAN)/Python 非常简单。

  • 可以同时安装和加载模块的多个版本。

  • 由于更简单的更新/升级策略,简化了系统管理。

  • 简单的数值计算没有损失精度,因为 Rats(有理数)。

  • 用于解析数据或代码的可扩展语法(Raku 用它解析自身)

  • Raku 是一种非常易变的语言(定义自己的函数,运算符,traits 和数据类型,为您修改解析器)。

  • 很多的数据类型选择,加上创建自己的类型的可能性。

  • 具有适当边界检查的多维成型的和/或原生数组

  • 在某个匹配出现时, 词法解析期间随时执行代码

  • 添加自定义运算符或添加 trait 特征和编写子例程一样简单。

  • 在任何运算符(系统或自定义添加的)上自动生成超运算符。

  • 运行在各种后端上。目前 MoarVM 和 JVM,JavaScript在开发中,可能会有更多。

  • 执行期间(JIT)热代码路径的运行时优化。

  • 运行在小型(例如 Raspberry Pi)和大型多处理器硬件上。

  • 基于垃圾收集:没有及时销毁,所以引用计数没有必要。使用 phasers 用以及时的动作。

  • 方法可以在运行时混合到任何实例化的对象中,例如。以允许添加带外数据。

  • 通过使用具有多重分派和自动生成使用信息的 MAIN 子例程,使命令行接口易于访问。

  • 更少的代码行创建更紧凑的程序。名字的霍夫曼编码允许更好的可读性。

  • 使用简单的迭代器接口定义的惰性列表,任何类可以通过最小化的提供单个方法来提供。

  • Perl 6 的座右铭与 Perl一直保持不变:Perl是不同的。简而言之,Perl旨在"使容易的工作变得容易,使困难的工作变得可能"。和"条条大路通罗马"。现在有更多 -Ofun 添加进来。

请查看 功能比较矩阵 获取更多信息.

65.5.3. 为什么不把它叫做除了 Perl 以外的其它东西?

很多人建议, Raku 跟之前的 Perl 版本的区别太大了, 我们应该考虑给它改名, 或者考虑到 Raku 伤害了 Perl 5, 仅仅拥有同样的名字却有更高的版本号.

Raku 仍然叫做 “Perl" 的主要原因是:

  • Raku 仍然是一个 perlish 风格的语言, 和之前的版本遵守相同的底层思想(用于微型命名空间的符号, 条条大路通罗马, 吸收了很多自然语言的思想..)

  • Raku 的代码很 perlish.

  • Perl 仍然是一个强健的品牌名, 我们不想马上抛弃它

  • 找到一个替代的名字很困难. 而且, “camelia” 和 “rakudo" 不是合适的编程语言名

  • 即使 Raku 更改了它的名字, Perl 5 也不大可能增加它的版本号为 6.因为 Raku 已经根植于人们的头脑中了

65.5.4. Raku 对我来说足够快了吗?

那取决于你正在做什么。Raku 一直奉行“做对的事情然后做的更快”的哲学进行开发。对于某些东西来说它够快了, 但是需要做的更多。 Raku 大部分是由志愿者开发的, 但是 Raku 的性能在不久的将来有待提高, 因为 MoarVM 后端包含一个现代的即时(JIT)编译器。 Perl 5 程序员应该意识到 Raku 在面向对象方面有很多内建函数并且还有更多其它的。 简单的基准测试会误导除非你在你的 Perl 5脚本中包含了诸如 Moose, 类型检测模块等。

下面这个粗超的基准测试, 使用了所有诸如此类的一般说明, 能展示 Raku 在某些类似任务上能和 Perl 5的速度接近。 在你的系统上尝试下, 你可能会感到很惊讶!

# Raku version
use v6;

class Foo { has $.i is rw };

for (1..1_000_000) -> $i {
    my $obj = Foo.new;
    $obj.i = $i;
}

# Perl 5 version
package Foo;
use Moose;

has i => (is => 'rw');

__PACKAGE__->meta->make_immutable;

for my $i (1..1_000_000) {
    my $obj = Foo->new;
    $obj->i($i);
}

1;

# Another Perl 5 version that offers bare-bones set of features
# compared to Moose/Raku's version but those are not needed in this
# specific, simple program anyway.
package Foo;
use Mojo::Base -base;

has 'i';

for my $i (1..1_000_000) {
    my $obj = Foo->new;
    $obj->i($i);
}

1;

# A perl program which works under both perl5 (with perl -Mbigint)
# and raku

my ($prev, $current) = (1, 0);

for (0..100_000) {
    ($prev, $current) = ($current, $prev + $current);
}
print $current;

66. 社区

“Perl 5是我对 Perl 的重写。我希望 Raku 能够成为社区重写的 Perl, 并且 Raku 是社区的 Perl。” - 拉里沃尔

66.1. Raku 社区

freenode.net 上的 #raku 频道有很多人,他们很乐意提供支持和回答问题。可以在 raku.org 社区页面 中找到更多资源。 Camelia 是她身上带有 P 6 的多色蝴蝶,她是这个多元化和热情的社区的象征。我们广泛使用 #raku IRC 频道进行沟通,提问和简单地闲逛。查看此IRC术语资源,了解那里经常使用的缩写。 StackOverflow 也是一个很好的资源,用于提出问题并帮助其他人解决他们的 Raku 问题和挑战。

66.2. Raku 周刊

Elizabeth Mattijsen 通常在 “Raku Weekly” 博客中发帖,这是有关 Raku 的帖子,推文,评论和其他有趣花絮的摘要。是知道 Perl 社区正在发生什么的最佳单个资源。

66.3. Raku 降临节日历

Raku 社区每年 12 月都会发布一个 Advent Calendar,每天都有 Raku 教程,直到圣诞节。通过不同的 Raku 频道和 Raku/mu 存储库完成组织和日期分配。如果您想参与,它将在10月底开始组织,因此请查看上面的频道。

67. Pod 文档

Raku Pod 是一种易于使用的标记语言。 Pod 可用于编写语言文档,用于文档化程序和模块,以及其他类型的文档组合。

每个 Pod 文档必须以 =begin pod 开头,以 =end pod 结束。这两个分隔符之间的所有内容都将被处理并用于生成文档。

=begin pod

A very simple Raku Pod document

=end pod

67.1. 块结构

Pod 文档可能包含多个 Pod 块。有四种方法可以定义块(分隔符,段落,缩写和声明符); 前三个产生相同的结果,但第四个不同。你可以使用最方便你的特定文档任务的任何形式。

67.1.1. 分割符块

分隔块由 =begin=end 标记限定,两者都后跟有效的 Raku 标识符,后者是块的 typename。完全小写的类型名称(例如 =begin head1)或完全大写(例如:=begin SYNOPSIS)保留。

=begin head1
Top Level Heading
=end head1

67.2. 配置信息

在 typename 之后, =begin 标记行的其余部分被视为块的配置信息。此信息由不同类型的块以不同方式使用,但始终使用 Raku-ish 选项对指定。也就是说,任何:

alue is…​

Specify with…​

Or with…​

Or with…​

List

:key[$e1, $e2, …​]

:key($e1, $e2, …​)

Hash

:key{$k1⇒$v1, $k2⇒$v2}

Boolean (true)

:key

:key(True)

:key[True]

Boolean (false)

:!key

:key(False)

:key[False]

String

:key<str>

:key('str')

:key("str")

Int

:key(42)

:key[42]

Number

:key(2.3)

:key[2.3]

其中'$e1,$e2,…​'是 String,Int,Number 或 Boolean 类型的列表元素。列表可能具有混合元素类型。请注意,单元素列表将转换为其元素的类型(String,Int,Number 或 Boolean)。另请注意,如果需要,可以使用“bigints”。

对于散列,'$k1,$k2,…​'是 Str 类型的键,'$v1,$2,…​'是 String,Int,Number 或 Boolean 类型的值。

字符串由单引号或双引号分隔。空格在字符串之外是微不足道的。散列键不需要引用分隔,除非它们包含重要的空格。

当然,所有选项键和值必须是常量,因为Pod是一种规范语言,而不是编程语言。具体来说,选项值不能是闭包。有关各种Raku对符号的详细信息,请参见概要2。

配置部分可以通过在第一(虚拟)列中带有=后跟空白字符的那些行开始在后续行上扩展。 (注意:此功能尚未实现。当前所有配置信息必须与= begin标记行在同一行提供,或=为段落块的名称提供。)

67.2.1. 段落块

段落块以 =for 标记开始,以下一个 Pod 指令或第一个空行结束。 =for 标记后面跟着块的类型名加上,可选地,跟上面描述的分隔块中的任何配置数据。

=for head1
Top Level Heading

67.3. 缩写块

缩写块以 = 符号开头,紧接着是块的类型名称。以下所有数据都是块内容的一部分,因此无法为缩写块指定配置数据。该块在下一个Pod指令或第一个空行结束。

=head1 Top Level Heading

67.4. 声明器块

声明器块与其他声明块不同,没有特定类型,而是附加到某些源代码。

声明器块由特殊注释引入:=|,必须紧跟空格或左括号。如果后跟一个空格,则该块在行尾终止;如果后跟一个或多个左括号,则该块由关闭括号的匹配序列终止。

以#开头的块附加到它们之后的代码,以 #= 开头的块附加到它们之前的代码。

由于声明器块附加到源代码,因此它们可用于记录类,角色,子例程等。

WHY方法可用于这些类,角色,子例程等,以返回附加的 Pod 值。

#| Base class for magicians
class Magician {
  has Int $.level;
  has Str @.spells;
}

#| Fight mechanics
sub duel(Magician $a, Magician $b) {
}
#= Magicians only, no mortals.

say Magician.WHY; # OUTPUT: «Base class for magicians␤»
say &duel.WHY.leading; # OUTPUT: «Fight mechanics␤»
say &duel.WHY.trailing; # OUTPUT: «Magicians only, no mortals.␤»

这些声明可以扩展多个块:

#|( This is an example of stringification:
    * Numbers turn into strings
    * Regexes operate on said strings
    * C<with> topicalizes and places result into $_
)
sub search-in-seq( Int $end, Int $number ) {
    with (^$end).grep( /^$number/ ) {
        .say for $_<>;
    }
}
#=« Uses
    * topic
    * decont operator
»

通过使用匹配的括号构造对,例如 ()«»,注释可以扩展多行。但是,这种格式不会转换为 raku -doc 的多行显示。

67.5. 块类型

Pod提供多种标准块类型。

67.5.1. 标题

可以使用= headN来定义标题,其中N大于零(例如,= head1,= head2,…​)。

=head1 A Top Level Heading

=head2 A Second Level Heading

=head3 A Third Level Heading

67.5.2. 普通段落

普通段落由在当前嵌套级别格式化为文档的文本组成,其中空格被挤压,线条填充,并且应用了任何特殊的内联标记。

普通段落由一个或多个连续的文本行组成,每行文本以非空白字符开头。段落由第一个空行或块指令终止。

例如:

=head1 This is a heading block

This is an ordinary paragraph.
Its text  will   be     squeezed     and
short lines filled. It is terminated by
the first blank line.

This is another ordinary paragraph.
Its     text    will  also be squeezed and
short lines filled. It is terminated by
the trailing directive on the next line.

=head2 This is another heading block

This is yet another ordinary paragraph,
at the first virtual column set by the
previous directive

普通段落不需要明确的标记或分隔符。

或者,还有一个显式的 =para 标记,可用于明确标记段落。

=para
This is an ordinary paragraph.
Its text  will   be     squeezed     and
short lines filled.

另外,可以使用较长 = begin para=end para 形式。

例如:

=begin para
This is an ordinary paragraph.
Its text  will   be     squeezed     and
short lines filled.

This is still part of the same paragraph,
which continues until an...
=end para

如前面的示例所示,在分隔 =begin para=end para 块中,保留任何空行。

67.6. 代码块

代码块用于指定源代码,应该在没有重新调整的情况下进行渲染,不需要空格压缩,也不需要识别任何内联格式代码。通常,这些块用于显示代码,标记或其他文本规范的示例,并使用固定宽度字体进行渲染。

代码块可以隐式地指定为一行或多行文本,每行文本以空白字符开头。然后通过空行终止隐式代码块。

例如:

This ordinary paragraph introduces a code block:

    my $name = 'John Doe';
    say $name;

代码块也可以通过将它们包含在= begin code和= end code中来显式定义

   =begin code
    my $name = 'John Doe';
    say $name;
   =end code

67.7. I/O 块

Pod 提供用于指定程序输入和输出的块。

=input 块用于指定预先格式化的键盘输入,应该在不重新对齐或挤压空格的情况下进行渲染。

=output 块用于指定预先格式化的终端或文件输出,也应该在没有重新调整或空白压缩的情况下进行渲染。

67.8. 列表

67.8.1. 无序列表

Pod 中的列表被指定为一系列 =item 块。

例如:

The three suspects are:

=item  Happy
=item  Sleepy
=item  Grumpy

三名嫌犯是:

  • Happy

  • Sleepy

  • Grumpy

67.8.2. 定义列表

定义术语或命令的列表使用 =defn,等同于 HTML 中的 DL 列表

=defn Happy
When you're not blue.

=defn blue
When you're not happy.

将以这种方式呈现:

Happy When you’re not blue.

Blue When you’re not happy.

目前,它可能是一个简单的HTML段落,但将来可能会发生变化。

67.8.3. 多层级列表

列表可以是多级的,使用 =item1=item2=item3 等块指定每个级别的项目。

请注意,=item 只是 =item1 的缩写。

例如:

=item1  Animal
=item2     Vertebrate
=item2     Invertebrate

=item1  Phase
=item2     Solid
=item2     Liquid
=item2     Gas
  • Animal

  • Vertebrate

  • Invertebrate

  • Phase

  • Solid

  • Liquid

  • Gas

67.8.4. 多段落列表

使用 =item 块( =begin item=end item )的分隔形式,我们可以指定包含多个段落的项目。

例如:

Let's consider two common proverbs:

=begin item
I<The rain in Spain falls mainly on the plain.>

This is a common myth and an unconscionable slur on the Spanish
people, the majority of whom are extremely attractive.
=end item

=begin item
I<The early bird gets the worm.>

In deciding whether to become an early riser, it is worth
considering whether you would actually enjoy annelids
for breakfast.
=end item

As you can see, folk wisdom is often of dubious value.

让我们考虑两个常见的谚语:

  • The rain in Spain falls mainly on the plain.

This is a common myth and an unconscionable slur on the Spanish people, the majority of whom are extremely attractive.

  • The early bird gets the worm.

In deciding whether to become an early riser, it is worth considering whether you would actually enjoy annelids for breakfast.

正如你所看到的,民间智慧往往具有可疑的价值。

67.9. 表

查看此页面以获取与相关的文档

67.10. Pod 注释

Pod评论是Pod渲染器忽略的评论。

注释对于元文档(记录文档)很有用。单行注释使用comment关键字:

=comment Add more here about the algorithm

对于多行注释,请使用带分隔符的注释块:

=begin comment
This comment is
multi-line.
=end comment

67.11. 语义块

所有大写块类型名称都保留用于指定标准文档,发布,源组件或元信息。

=NAME
=AUTHOR
=VERSION
=TITLE
=SUBTITLE

67.12. 格式化代码

格式代码提供了一种向一段文本添加内联标记的方法。 所有Pod格式代码都包含一个大写字母,紧接着是一组尖括号。 格式代码可以嵌套其他格式代码。

67.12.1. 粗体

要以粗体格式化文本,请将其括在 B< >

Raku is B<awesome>

Raku is awesome

67.12.2. 斜体

要用斜体格式化文本,请将其括在 `I< >`中

Raku is I<awesome>

Raku is awesome

67.12.3. 下划线

要在文本下划线将其括在 U<>

Raku is U<awesome>

67.12.4. 代码

要将文本标记为代码并将其逐字处理,请将其括在 C< >

C<my $var = 1; say $var;>

my $var = 1; say $var;

67.12.5. 链接

要创建链接,请将其括在 L< >

Raku homepage L<https://raku.org>
L<Raku homepage|https://raku.org>

Raku homepage https://raku.org

要创建指向同一文档中某个部分的链接:

Comments L<#Comments>

Comments Comments

67.12.6. 注释

注释是从不呈现的文本。

要创建注释,请将其括在 Z< >

Raku is awesome Z<Of course it is!>

Raku is awesome

67.12.7. 笔记

注释呈现为脚注。

要创建一个注释,请将其括在 N< >

Raku is multi-paradigmatic N<Supporting Procedural, Object Oriented, and Functional programming>

67.12.8. 键盘输入

要将文本标记为键盘输入,请将其括在 K< >

Enter your name K<John Doe>

67.12.9. 终端输出

要将文本标记为终端输出,请将其括在 T< >

Hello T<John Doe>

67.12.10. Unicode

要在 Pod 文档中包含 Unicode 代码点或 HTML5 字符引用,请将它们包含在 `E< >`中

`E< >`可以包含一个数字,该数字被视为所需代码点的十进制 Unicode 值。它还可以使用 Raku 表示法为显式数字括起显式二进制,八进制,十进制或十六进制数字。

Raku makes considerable use of the E<171> and E<187> characters.

Raku makes considerable use of the E<laquo> and E<raquo> characters.

Raku makes considerable use of the E<0b10101011> and E<0b10111011> characters.

Raku makes considerable use of the E<0o253> and E<0o273> characters.

Raku makes considerable use of the E<0d171> and E<0d187> characters.

Raku makes considerable use of the E<0xAB> and E<0xBB> characters.

Raku makes considerable use of the « and » characters.

67.13. 渲染 Pod

67.13.1. HTML

要从 Pod 生成 HTML,你需要 Pod::To::HTML 模块。

如果尚未安装,请通过运行以下命令进行安装:zef install Pod::To::HTML

使用终端运行以下命令:

raku --doc=HTML input.pod6 > output.html

67.13.2. Markdown

要从 Pod 生 Markdown,你需要 Pod::To::Markdown 模块。

如果尚未安装,请通过运行以下命令进行安装:zef install Pod::To::Markdown

使用终端运行以下命令:

raku --doc=Markdown input.pod6 > output.md

67.13.3. Text

为了从 Pod 生成 Text,你可以使用默认的 Pod::To::Text 模块。

使用终端,运行以下命令:

raku --doc=Text input.pod6 > output.txt

你可以省略 =Text 部分:

raku --doc input.pod6 > output.txt

你甚至可以将 Pod 直接嵌入到你的程序中,并使用 multi MAIN 子例程将传统的 Unix 命令行 "--man" 选项添加到你的程序中,如下所示:

multi MAIN(Bool :$man)
{
    run $*EXECUTABLE, '--doc', $*PROGRAM;
}

现在 myprogram --man 将输出你的 Pod 渲染为手册页。

67.14. 访问 Pod

为了从 Raku 程序中访问 Pod 文档,需要使用特殊的 = twigil,如变量部分所述。

= twigil 提供了对 Pod 结构的内省,提供了一个 Pod::Block 树根,从中可以访问 Pod 文档的整个结构。

例如,以下代码内省了自己的Pod文档:

=begin pod

=head1 This is an head1 title

This is a paragraph.

=head2 Subsection

Here some text for the subsection.

=end pod

for $=pod -> $pod-item {
    for $pod-item.contents -> $pod-block {
      $pod-block.perl.say;
    }
}

产生以下输出:

Pod::Heading.new(level => 1, config => {}, contents => [Pod::Block::Para.new(config => {}, contents => ["This is an head1 title"])]);
Pod::Block::Para.new(config => {}, contents => ["This is a paragraph."]);
Pod::Heading.new(level => 2, config => {}, contents => [Pod::Block::Para.new(config => {}, contents => ["Subsection"])]);
Pod::Block::Para.new(config => {}, contents => ["Here some text for the subsection."]);

68. 要避免的陷阱

在学习一门编程语言时,可能有熟悉另一门编程语言的背景,总有一些事情会让您感到惊讶,并且可能会耗费宝贵的调试和发现时间。

本文件旨在展示常见的误解,以避免它们。

在编写 Raku 的过程中,我们付出了巨大的努力来消除语法中的瑕疵。然而,当你消灭一个瑕疵的时候,有时另一个会突然冒出来。所以我们花了很多时间去寻找最小数量的瑕疵或者试图把它们放在它们很少被看到的地方。正因为如此,Raku 的瑕疵出现在了不同的地方,而不是来自另一种语言时所期望的那样。

68.1. 变量和常量

68.1.1. 常量在编译时计算

常量是在编译时计算的,所以如果在模块中使用它们,请记住,由于模块本身的预编译,它们的值将被冻结:

# WRONG (most likely):
unit module Something::Or::Other;
constant $config-file = "config.txt".IO.slurp;

$config-file 将在预编译时一次性被读入。config.txt 文件的更改不会在你再次启动脚本时重新加载;只有当模块被重新编译时才会重新加载。

避免使用容器,而倾向于将值绑定到提供类似于常量行为的变量上,但允许更新值:

# Good; file gets updated from 'config.txt' file on each script run:
unit module Something::Or::Other;
my $config-file := "config.txt".IO.slurp;

68.1.2. 赋值为 Nil 产生不同的值, 通常是 Any

实际上,赋给 Nil将变量还原为其默认值。所以:

my @a = 4, 8, 15, 16;
@a[2] = Nil;
say @a; # OUTPUT: «[4 8 (Any) 16]␤»

在本例中,AnyArray 元素的默认值。

你可以故意指定 Nil 作为默认值:

my %h is default(Nil) = a => Nil;
say %h; # OUTPUT: «Hash %h = {:a(Nil)}␤»

或者将值绑定到 Nil,如果结果是你想要的:

@a[3] := Nil;
say @a; # OUTPUT: «[4 8 (Any) Nil]␤»

这个陷阱可能隐藏在函数的结果中,比如匹配:

my $result2 = 'abcdef' ~~ / dex /;
say "Result2 is { $result2.^name }"; # OUTPUT: «Result2 is Any␤»

Match 将会是 Nil如果什么也没有找到。但是,如果将 Nil 赋给上面的 $result2,则会得到其默认值,如所示为 Any

68.1.3. 使用块来插入匿名状态变量

程序员打算让代码计数程序被调用的次数,但是计数器没有增加:

sub count-it { say "Count is {$++}" }
count-it;
count-it;

# OUTPUT:
# Count is 0
# Count is 0

当涉及到状态变量时,每当该块的块被重新进入时,声明 vars 的块就会被克隆,vars 也会被重新初始化。这让像下面这样的结构表现得恰当;每次调用子程序时,循环内部的状态变量都会被重新初始化:

sub count-it {
    for ^3 {
        state $count = 0;
        say "Count is $count";
        $count++;
    }
}

count-it;
say "…and again…";
count-it;


# OUTPUT:
# Count is 0
# Count is 1
# Count is 2
# …and again…
# Count is 0
# Count is 1
# Count is 2

同样的布局存在于我们的 bug 程序中。双引号字符串中的 {} 不仅仅是执行一段代码的插值。它实际上是它自己的块,就像在上面的例子中,每次进入子例程时都会被克隆,重新初始化状态变量。为了得到正确的计数,我们需要去掉内部块,使用标量上下文分析器来插入我们的代码:

sub count-it { say "Count is $($++)" }
count-it;
count-it;

# OUTPUT:
# Count is 0
# Count is 1

或者,也可以使用连接运算符:

sub count-it { say "Count is " ~ $++ }

68.2. Blocks

68.2.1. 提防空 "block"

花括号用于声明块。然而,空花括号会声明一个哈希。

$ = {say 42;} # Block
$ = {;}       # Block
$ = {…}       # Block
$ = { }       # Hash

如果你想有效地声明一个空的块,你可以使用第二种形式:

my &does-nothing = {;};
say does-nothing(33); # OUTPUT: «Nil␤»

68.3. 对象

68.3.1. 给属性赋值

新手通常会这样想,因为带有访问器的属性被声明为 has $.x,在类里面它们可以给 $.x 赋值 。事实并非如此。

例如

use v6.c;
class Point {
    has $.x;
    has $.y;
    method double {
        $.x *= 2;   # WRONG
        $.y *= 2;   # WRONG
        self;
    }
}

say Point.new(x => 1, y => -2).double.x
# OUTPUT: «Cannot assign to an immutable value␤»

方法 double 中的第一行标记为 # WRONG,因为 $.x$( self.x ) 的缩写。是对只读访问器的调用。

语法 has $.xhas $!x; method x() { $!x } 的简写,因此实际属性称为$!将自动生成只读访问器方法。

因此,编写方法 double 的正确方法是:

method double {
    $!x *= 2;
    $!y *= 2;
    self;
}

它直接作用于属性。

68.3.2. BUILD 防止从构造函数参数中自动初始化属性

在定义自己的 BUILD 子方法时,必须自己初始化所有属性。例如

use v6.c;
class A {
    has $.x;
    has $.y;
    submethod BUILD {
        $!y = 18;
    }
}

say A.new(x => 42).x;       # OUTPUT: «Any␤»

留下 $!x 未初始化,因为自定义的 BUILD 没有初始化它。

注意:考虑使用 TWEAKRakudo 自发布 2016.11 以来支持 TWEAK 方法。

一种可能的补救方法是显式地初始化 BUILD 中的属性:

submethod BUILD(:$x) {
    $!y = 18;
    $!x := $x;
}

这可以简化为:

submethod BUILD(:$!x) {
    $!y = 18;
}

另一种更普遍的方法是不去管 BUILD,而是与 BUILDALL 机制挂钩:

use v6.c;
class A {
    has $.x;
    has $.y;
    method BUILDALL(|c) {
        callsame;
        $!y = 18;
        self
    }
}

say A.new(x => 42).x;       # OUTPUT: «42␤»

记住 BUILDALL 是一个方法,而不是子方法。这是因为在默认情况下,每个类层次结构只有一个这样的方法,而 BUILD 是每个类显式调用的。这就是为什么为了正确地初始化父对象,需要在 BUILDALL 中使用 callsame,而不是在 BUILD 中(关于该主题的更多信息请参阅对象创建)。

68.4. 空白

68.4.1. regex 中的空白不按字面匹配

say 'a b' ~~ /a b/; # OUTPUT: «False␤»

默认情况下,regexe 中的空白被认为是一种可选的没有语义的填充,就像 Raku 语言的其他部分一样。

匹配空白的方法:

  • \s 匹配任何一个空白,\s+ 匹配至少一个空白

  • ' '(引号中的空格)以匹配单个空格

  • \t\n 匹配特定空格(制表符,换行符)

  • \h\v,用于水平,垂直空白

  • .ws 是一个内建的空白规则,它通常如你所愿

  • 对于 m:s/a b/m:sigspace/a b/, regex 中的空白匹配任意空格

68.4.2. 模棱两可的解析

虽然有些语言允许您删除记号之间尽可能多的空白,但是 Raku 就不那么宽容了。最重要的准则是我们不鼓励使用代码高尔夫,所以不要在空格上浪费时间(这些限制背后更严重的潜在原因是单遍解析和解析 Raku 程序的能力,而实际上不需要回溯)。

你应留意的常见区域是:

块与散列切片的歧义性
# WRONG; trying to hash-slice a Bool:
while ($++ > 5){ .say }
# RIGHT:
while ($++ > 5) { .say }

# EVEN BETTER; Raku does not require parentheses there:
while $++ > 5 { .say }
化简与数组构造函数的歧义性
# WRONG; ambiguity with `[<]` meta op:
my @a = [[<foo>],];
# RIGHT; reductions cannot have spaces in them, so put one in:
my @a = [[ <foo>],];

# No ambiguity here, natural spaces between items suffice to resolve it:
my @a = [[<foo bar ber>],];
小于与单词引用/关联索引
# WRONG; trying to index 3 associatively:
say 3<5>4
# RIGHT; prefer some extra whitespace around infix operators:
say 3 < 5 > 4

68.4.3. 捕获

捕获中的容器与值

69. Pod 6 表格

Raku POD 表的官方规范位于文档规范中:。虽然 Pod 6 的规格尚未完全妥善处理,但仍有几个项目正在进行纠正。一个这样的项目是确保正确处理 Pod 6 表。

作为该工作的一部分,本文档通过示例解释了 Pod 6 表的当前状态:有效表,无效表和丑陋表(即,由于草率构造,可能导致与用户期望的不同的有效表) 。

69.1. Restrictions

1.唯一有效的列分隔符要么是可见的(|+)(注意在可见列分隔符之前和之后至少需要一个空格)或不可见[两个或多个连续的空格(WS)字符(例如, ' ')]。在表格的左侧或右侧通常不会识别列分隔符,但是右侧的列分隔符可能会导致一个或多个空单元格,具体取决于其他行中单元格的数量。(请注意,作为单元格数据一部分的管道或加号字符将导致意外的额外列,除非使用反斜杠转义字符,例如 \|\+。)

2.在同一个表中混合可见和不可见的列分隔符是非法的。

3.唯一有效的行分隔符字符是 _-+' '|=

4.连续的内部行分隔符是非法的。

5.前导和尾随行分隔符会生成警告。

6.当前忽略表格单元格中的格式,并将其视为纯文本。

提示:在开发过程中,使用环境变量 RAKUDO_POD6_TABLE_DEBUG 将向您展示 Rakudo 如何在将 pod 表传递给渲染器之前解释它们,例如 Pod::To::HTMLPod::To::TextPod::To::Markdown

69.2. 最佳实践

提示:由于在表行上进行额外的循环,不遵循以下最佳实践可能需要更多的表处理。

1.对列分隔符使用 WS 很脆弱,它们只能用于简单表。以下 Ugly Tables 部分说明了这个问题。

2.仔细对齐表格列和行。请参阅后面的最佳实践中的示例。

3.不要在表上使用可见的边框。

4.对于具有标题和单行或多行内容的表,在标题后使用一个或多个连续的等号('=')作为行分隔符,并使用一个或多个连续的连字符('-')作为表的内容部分中的行分隔符。例如,

  • 标题和单行或多行内容

=begin table
 hdr col 0 | hdr col 1
 ======================
 row 0     | row 0
 col 0     | col 1
 ----------------------
 row 1     | row 1
 col 0     | col 1
 ----------------------
=end table
  • 标题和单行内容

=begin table
 hdr col 0   | hdr col 1
 ======================
 row 0 col 0 | row 0 col 1
 row 1 col 0 | row 1 col 1
=end table

5.对于没有标题和多行内容的表,请使用一个或多个连续连字符('-')作为表格内容部分中的行分隔符。例如,

=begin table
 row 0       | row 0
 col 0       | col 1
 ----------------------
 row 1 col 0 | row 1 col 1
=end table

6.对于具有许多行且没有多行内容的表,不使用行分隔符就可以了。但是,如果一行或多行包含多行内容,则通过在每个内容行之间使用行分隔线(可见或不可见)来确保正确的结果更容易。

7.确保故意空单元格具有列分隔符,否则会出现关于短行填充空单元格的警告。(表行总是与具有最多单元格的行具有相同数量的单元格。右边用空单元格填充短行并生成警告。)

8.此示例中使用的 =begin table 行可以为表添加标题:

=begin table :caption<My Tasks>
mow lawn
take out trash
=end table

虽然不是一个好的做法,但目前正在使用另一种定义标题的方法,如下例所示:

=begin table :config{caption => "My Tasks"}
mow lawn
take out trash
=end table

请注意,把标题放在 config 哈希中的替代方法必须在实现 :caption 方法之前,但现在认为该方法已被弃用。该练习将在已经发布的 6.d 版本中生成警告,并将在 6.e 版本中引发异常。

69.3. Good tables

以下是有效(好)表的示例(取自当前的规范测试)。

=begin table
        The Shoveller   Eddie Stevens     King Arthur's singing shovel
        Blue Raja       Geoffrey Smith    Master of cutlery
        Mr Furious      Roy Orson         Ticking time bomb of fury
        The Bowler      Carol Pinnsler    Haunted bowling ball
=end table
=table
    Constants           1
    Variables           10
    Subroutines         33
    Everything else     57

=for table
    mouse    | mice
    horse    | horses
    elephant | elephants

=table
    Animal | Legs |    Eats
    =======================
    Zebra  +   4  + Cookies
    Human  +   2  +   Pizza
    Shark  +   0  +    Fish

=table
        Superhero     | Secret          |
                      | Identity        | Superpower
        ==============|=================|================================
        The Shoveller | Eddie Stevens   | King Arthur's singing shovel

=begin table

                        Secret
        Superhero       Identity          Superpower
        =============   ===============   ===================
        The Shoveller   Eddie Stevens     King Arthur's
                                          singing shovel

        Blue Raja       Geoffrey Smith    Master of cutlery

        Mr Furious      Roy Orson         Ticking time bomb
                                          of fury

        The Bowler      Carol Pinnsler    Haunted bowling ball

=end table
=table
    X | O |
   ---+---+---
      | X | O
   ---+---+---
      |   | X

=table
    X   O
   ===========
        X   O
   ===========
            X

=begin table

foo
bar

=end table

69.4. Bad tables

以下是无效(坏)表的示例,它们应在解析期间触发未处理异常。

  • 同一行中不允许混合列分隔符类型:

=begin table
r0c0 +  r0c1 | r0c3
=end table
  • 同一个表中不允许使用混合的可见和空格列分隔符类型:

=begin table
r0c0 +  r0c1 | r0c3
r1c0    r0c1   r0c3
=end table
  • 不允许连续两个行内分隔符:

=begin table
r0c0 |  r0c1
============
============
r1c0 |  r1c1
=end table

69.5. Ugly tables

以下是有效表可能是两列的示例,但列未对齐,因此每个列都将解析为单列表。

  • 带 WS 列分隔符的未对齐列:

请注意,第二行的两个单词仅由一个 WS 字符分隔,而至少需要两个相邻的 WS 字符才能定义列分隔。这是一个有效的表,但将被解析为单列表

=begin table
r0c0    r0c1
 r1c0 r0c1
=end table
  • 带有可见列分隔符的未对齐列:

请注意,第二行有两个单词由可见字符(|)分隔,但该字符不会被识别为列分隔符,因为它的两边都没有相邻的 WS 字符。虽然这是一个合法的表,但结果将不是用户的意图,因为第一行有两列,而第二行只有一列,因此它将有一个空的第二列。

=begin table
r0c0  |  r0c1
 r1c0 |r0c1
=end table

70.

Raku 中的大部分句法结构能归类为项和操作符. 这儿你能找到各种不同类型的项的概览.

70.1. Literals

70.1.1. Int

42
12_300_00
:16<DEAD_BEEF>    #十六进制

Int 字面量由数字组成, 并且能在数字之间包含下划线. 使用 :radix<number> 冒号对儿形式能指定 10 进制外的其它进制.

70.1.2. Rat 有理数

12.34
1_200.345_678

Rat(有理数)字面量由一个点号分割的两部分整数组成. 注意尾部的点号是不允许的, 所以你必须写成 1.0 而非 1. ( 这个规则很重要, 因为有一个以点号开头的中缀操作符, 例如 .. 范围操作符 ).

70.1.3. Num 浮点数

12.3e-32
3e8

Num(浮点数)字面量由 Rat 或 Int 字面量后面再跟着一个字母 e 和 一个指数(可能为负)组成. 3e8 使用 值 3* 10**8 构建了一个 Num.

70.1.4. Str

'a string''I\'m escaped!'
"I don't need to be"
"\"But I still can be,\" he said."
q|Other delimiters can be used too!|

字符串字面量常常使用 '" 创建, 然儿, 字符串在 Raku 中其实是一种强大的子语言.

70.1.5. Regex

/ match some text /
rx/slurp \s rest (.*) $/

这两种会产生字面正则

70.1.6. Pair

 a => 1
'a' => 'b'
:identifier
:!identifier
:identifier<value>
:identifier<value1 value2>
:identifier($value)
:identifier['val1', 'val2']
:identifier{key1 => 'val1', key2 => 'value2'}
:$item
:@array
:%hash
:&callable

Pair 对象的创建要么使用 infix:«⇒» (它会自动括起左边, 如果左边是标识符的话), 要么使用各种冒号对儿形式. 那些总是以一个冒号开头的创建形式, 冒号后面要么跟着一个标识符, 要么跟着一个已经存在的变量(不带符号的变量名作为 pair 的键, 变量的值作为 pair 的键值).

在标识符形式的冒号对儿中, 可选的值可以是任意环缀. 如果没有环缀, 那它的值就是 Bool::True. !:identifier 形式的值是 Bool::False.

如果冒号对儿在参数列表中, 所有的冒号对儿都会作为命名参数, 但是 'quoted string' ⇒ $value 除外.

70.1.7. Parcel

什么是 Parcel? → Immutable sequence of values - 不可变值的序列.

calss Parcel is Cool does Positional { }

Parcel 代表 Parenthesis cell, 例如, 被圆括号环绕的表达式. 除了空的 parcel 之外, 实际上是使用逗号来创建一个 Parcel.

(1 + 2)  # not a Parcel
()       # empty Parcel
(1,)     # Parcel with one element
(1,3)    # Parcel with two element
1, 2, 3  # parenthesis are optional
<a b c>  # word-quoting
«a b c»  # also word-quoting
qw/a b c/

Parcel 字面量有: 空的圆括号 (), 逗号分割的列表, 还有几种引号结构.

> say (1,2,3).WHAT
(Parcel)
> say <a b c>.WHAT
(Parcel)
> say «a b c».WHAT
(Parcel)
> say (qw/a b c/).WHAT
(Parcel)

Parcels 是不可变的, 但是能包含可变容器:

my $x;
my $p = (0, $x, 2); # can assign to $p[1], but not
                    # to any other element of $p

<…​> 这种 Word-quoting 结构也会创建 parcels:

<a b c> # 3-element Parcel

在 flattening 列表上下文中, parcels 被展平并且会消失:

my @flat = <a b>, <c, d>;
say @flat.elems;

70.1.8. term *

* 会创建一个类型为 Whatever 的对象. 详情查看 Whatever.

70.2. Identifier terms

Raku 中有内建的标识符项, 列出如下. 此外, 使用该语法能添加新的标识符项.

sub term:<fourty-two> { 42 };
say fourty-two

或者作为常量:

constant forty-two = 42;
say fourty-two

70.2.1. self

在方法中, self 指向方法的调用者( 例如, 方法被调用的对象). 如果把它用在没有意义的上下文中, 会抛出一个 ` X::Syntax::NoSelf` 类型的编译时错误.

70.2.2. now

返回一个代表当前时间的实例对象.

70.2.3. rand

返回一个范围为 `0..^1`的伪随机浮点数.

70.2.4. pi

返回数值 pi, 例如, 圆的周长和半径之间的比率.

70.2.5. e

返回欧拉数值.

70.2.6. i

返回复数的虚部.

70.3. Variables

变量在变量语言文档中讨论.

71. 测试

测试代码是软件开发不可或缺的一部分。测试提供代码行为的自动,可重复的验证,并确保您的代码按预期工作。

在 Raku 中,Test 模块提供了一个测试框架,也被 Raku 的官方 spectest 套件使用。

测试函数发出符合 Test Anything Protocol 的输出。通常,它们用于 sink 上下文中:

ok check-name($meta, :$relaxed-name), "name has a hyphen rather than '::'"

但是不论测试成功与否,所有函数都会返回布尔值,如果测试失败,可以使用它来打印消息:

ok check-name($meta, :$relaxed-name), "name has a hyphen rather than '::'" \
  or diag "\nTo use hyphen in name, pass :relaxed-name to meta-ok\n";

71.1. 写测试

与任何 Perl 项目一样,测试位于项目基本目录的 t 目录下。

典型的测试文件看起来像这样:

use v6.c;
use Test;      # a Standard module included with Rakudo
use lib 'lib';

plan $num-tests;

#.... tests

done-testing;  # optional with 'plan'

我们确保通过 use v6.c 编译指令使用 Raku,然后加载 Test 模块并指定库的位置。然后我们指定我们*计划*运行多少个测试(这样测试框架可以告诉我们运行的测试是否比我们预期的要多),并且在完成测试后,我们使用*完成测试*来告诉框架我们已经完成。

71.1.1. 线程安全

请注意,Test 模块中的例程*不是*线程安全的。这意味着您不应该同时尝试在多个线程中使用测试例程,因为 TAP 输出可能会出现乱序并且会使解释它的程序迷惑。

目前没有计划使其线程安全。如果线程测试对您至关重要,您可能会发现一些合适的生态系统模块,代替 Test 来满足您的测试需求。

71.2. 运行测试

可以通过在命令行上指定测试文件名来单独运行测试:

$ raku t/test-filename.t

或者通过 Perl 5 中的 prove 命令,其中 --exec 用于指定运行测试的可执行文件:

$ prove --exec raku -r t

要在第一次失败时中止测试套件,请设置 PERL6_TEST_DIE_ON_FAIL 环境变量:

$ PERL6_TEST_DIE_ON_FAIL=1 raku t/test-filename.t

可以在测试文件中使用相同的变量。在加载 Test 模块之前设置它:

BEGIN %*ENV<PERL6_TEST_DIE_ON_FAIL> = 1;
use Test;
...

71.3. 测试计划

测试计划用 plan 声明将要完成的计划数量,或者可能会跳过的计划数量。如果没有声明计划,done-testing 则用于声明测试结束。

71.4. 测试返回值

Test 模块导出各种函数,用于检查给定表达式的返回值并生成标准化测试输出。

在实践中,表达式通常是对要进行单元测试的函数或方法的调用。oknok 将匹配 TrueFalse。但是,在可能的情况下,最好使用下面的一个专门的比较测试函数,因为它们可以在比较失败时打印更有用的诊断输出。

71.4.1. 通过字符串比较

isnok 使用适当的运算符测试相等性,具体取决于它所处理的对象(或类)。

71.4.2. 通过近似数字比较

is-approx 比较具有一定精度的数字,可以是绝对的或相对的。它对于精度取决于内部表示的数值非常有用。

71.4.3. 通过结构比较

也可以使用 is-deeply 比较结构,这将检查所比较的对象的内部结构是否相同。

71.4.4. 任意比较

您可以使用 cmp-ok 进行任何类型的比较,它将您想要用于比较的函数或运算符作为参数。

71.4.5. 通过对象类型比较

isa-ok 测试对象是否属于某种类型。

71.4.6. 通过方法名比较

can-ok 用于对象以检查它们是否具有该特定方法。

71.4.7. 通过角色比较

  • does-ok($variable, $role, $description?)

does-ok 检查给定变量是否可以执行某个角色

71.4.8. 通过正则表达式比较

likeunlike 使用正则表达式检查; 在第一种情况下,如果存在匹配则通过,在第二种情况下则不通过。

71.5. 测试模块

use-ok 实验性地加载模块,如果加载失败则会失败。

71.6. 测试异常

dies-oklives-ok 是相反的测试代码的方法; 第一个检查是它抛出异常,第二个检查它不抛出异常; throws-like 检查代码是否抛出了作为参数传递的特定异常; fails-like 同样,检查代码是否返回特定类型的 Failureeval-dies-okeval-lives-ok 在测试之前计算的字符串上工作类似。

71.7. Grouping tests

只有在所有子测试都是 ok 的时候, 这组子测试的结果才是 ok 的; 他们按使用 subtest 分组。

71.8. 跳过测试

有时测试还没准备好运行,例如某个功能可能尚未实现,在这种情况下,测试可以标记为 todo。或者可能是某个特定功能仅适用于特定平台的情况 - 在这种情况下,可以在其他平台上 skip 这个测试; skip-rest 将跳过剩余的测试,而不是跳过作为参数给出的特定数字的测试; bail-out 将简单地带着一条消息退出测试。

71.9. 手动控制

如果上面记录的便利功能不符合您的需要,您可以使用以下函数手动指导测试套输出; pass 将打印测试已经通过,diag 将打印(可能)信息性消息。

72. Opener graphemes

下表显示了在 Pod6 声明符块之类的结构中可用作开头配对定界符的所有有效字素。 请注意,它们显示在管道符号之间,因此可以看到任何宽度字符的额外边界空间。

该表的来源是 Rakudo grammar 中的角色 STD 中定义的 token opener

Table 1. Opener Graphemes

Char

Hex

Char

Hex

Char

Hex

Char

Hex

0xFF62

«

0x00AB

0x0F3A

0x0F3C

0x169B

0x2018

0x201A

0x201B

0x201C

0x201E

0x201F

0x2039

0x2045

0x207D

0x208D

0x2208

0x2209

0x220A

0x2215

0x223C

0x2243

0x2252

0x2254

0x2264

0x2266

0x2268

0x226A

0x226E

0x2270

0x2272

0x2274

0x2276

0x2278

0x227A

0x227C

0x227E

0x2280

0x2282

0x2284

0x2286

0x2288

0x228A

0x228F

0x2291

0x2298

0x22A2

0x22A6

0x22A8

0x22A9

0x22AB

0x22B0

0x22B2

0x22B4

0x22B6

0x22C9

0x22CB

0x22D0

0x22D6

0x22D8

0x22DA

0x22DC

0x22DE

0x22E0

0x22E2

0x22E4

0x22E6

0x22E8

0x22EA

0x22EC

0x22F0

0x22F2

0x22F3

0x22F4

0x22F6

0x22F7

0x2308

0x230A

0x2329

0x23B4

0x2768

0x276A

0x276C

0x276E

0x2770

0x2772

0x2774

0x27C3

0x27C5

0x27D5

0x27DD

0x27E2

0x27E4

0x27E6

0x27E8

0x27EA

0x2983

0x2985

0x2987

0x2989

0x298B

0x298D

0x298F

0x2991

0x2993

0x2995

0x2997

0x29C0

0x29C4

0x29CF

0x29D1

0x29D4

0x29D8

0x29DA

0x29F8

0x29FC

0x2A2B

0x2A2D

0x2A34

0x2A3C

0x2A64

0x2A79

0x2A7D

⩿

0x2A7F

0x2A81

0x2A83

0x2A8B

0x2A91

0x2A93

0x2A95

0x2A97

0x2A99

0x2A9B

0x2AA1

0x2AA6

0x2AA8

0x2AAA

0x2AAC

0x2AAF

0x2AB3

0x2ABB

0x2ABD

⪿

0x2ABF

0x2AC1

0x2AC3

0x2AC5

0x2ACD

0x2ACF

0x2AD1

0x2AD3

0x2AD5

0x2AEC

0x2AF7

0x2AF9

0x2E02

0x2E04

0x2E09

0x2E0C

0x2E1C

0x2E20

0x2E28

0x3008

0x300A

0x300C

0x300E

0x3010

0x3014

0x3016

0x3018

0x301A

0x301D

0xFD3E

0xFE17

0xFE35

0xFE37

0xFE39

0xFE3B

0xFE3D

︿

0xFE3F

0xFE41

0xFE43

0xFE47

0xFE59

0xFE5B

0xFE5D

0xFF08

0xFF1C

0xFF3B

0xFF5B

0xFF5F

73. 实验特性

在 Raku 开发期间,通常可以在设计完成之前为用户提供新功能。最终,这些功能可能成为 Raku 规范的一部分。要使用这些功能,可以在程序源代码中使用 experimental 指令,例如,如下所示:

use experimental :macros;

这些功能暂时是实验性的。

73.1. pack

Pack 是一种允许二进制序列化一般数据结构的功能,并且继承自 Perl 的packpack 命令通过以包装字符串给出的特定方式打包数据结构来创建Buf,其中包含 unpack 描述中显示的选项。你可以通过在程序开头插入这个指令来打开它:

use experimental :pack;

例如,我们可以打包数字,将它们解释为十六进制(H),重复模式,直到没有更多的元素(*):

use experimental :pack;
say pack("H*", "414243").contents;#  OUTPUT: «(65 66 67)␤»

有一个相应的 unpack 例程正好相反。

use experimental :pack;
my $buf=Buf.new(65,66,67);
say $buf.unpack("H*"); # OUTPUT: «414243␤»

并非所有上述符号都可以保证实现,并且路线图不包含退出该阶段的固定日期。

请参阅 Blob 页面中的 packunpack 文档。

73.2.

) 是代码生成例程,它们在程序执行之前在编译时生成代码。在 Raku 中,它的使用仍然是实验性的,它需要通过编译指示打开

use experimental :macros;

宏处理在解析时发生。宏生成抽象语法树,将其移植到程序语法树中。 quasi 是执行此任务的例程。

macro does-nothing() {
    quasi {}
};
does-nothing; # OUTPUT: «»

宏是一种例程,因此它们可以以完全相同的方式接受参数,并且也以几乎相同的方式起作用。

macro is-mighty( $who ) {
    quasi { "$who is mighty!"}
};
say is-mighty "Freija"; # OUTPUT: « "Freija" is mighty!␤»

“几乎”说明了参数作为文字插入的事实,包括引号。请注意,我们也可以按照与例程相同的规则消除宏调用的括号。你可以使用unquoting构造 {{{}}} 来摆脱这种事情:

macro is-mighty( $who ) {
    quasi { {{{$who}}} ~ " is mighty!"}
};
say is-mighty "Freija";  # OUTPUT: «Freija is mighty!␤»

由于宏扩展是在解析时发生的,因此在使用外部变量时必须小心:

use experimental :macros;
my $called;
macro called() {
    $called++;
    quasi { "Called" }
};
say called() ~ " $called times";
say called() ~ " $called times"; # OUTPUT: «Called 2 times␤Called 2 times␤»

由于宏在分析时被扩展,因此 $called 将是运行时启动时的结果,已打印为 2。 但是,使用 0 初始化 $called 将使此打印调用 0 次,因为在扩展宏的解析阶段之后运行初始化。

当需要进行复杂的计算初始化时,宏非常有用。 然而,他们仍然处于试验中,这是有充分理由的。 虽然上面显示的功能不太可能发生变化,但任何事情,甚至它们的存在,都可能在任何时候都有所改变,这取决于必需品,因此最好让它们远离生产代码。 与此同时,看看 Masak 和 007这篇文章,这是一种新的宏观语言,可能会显示未来的形状。

73.3. cached

以下指令:

use experimental :cached;

打开 is cached trait,它存储例程调用的结果,如果使用相同的参数调用,则返回相同的值。

它可以在涉及大量计算时使用,如本示例中使用的友好数字,取自 2018 年 Advent 日历:

use experimental :cached;

sub aliquot-parts( $number ) is cached {
    (^$number).grep: $number %% *;
}

sub infix:<amic>( $m, $n ) {
    $m == aliquot-parts($n).sum &&
    $n == aliquot-parts($m).sum;
}

# Taken from https://en.wikipedia.org/wiki/Amicable_numbers
my @numbers = [2620, 2924, 5020, 5564, 6232, 6368, 66928, 66992];

say "Aliquot parts of $_ are ", aliquot-parts $_ for @numbers;

for @numbers X @numbers -> @pair {
    say "@pair[0] and @pair[1] are ",
        @pair[0] amic @pair[1]??" "!!"not ", "amicable";
}

这段代码缓存了等分部分的计算,因此当调用 amic 运算符时,它只计算一次;事实上,打印这些等分部件的第一个循环将是唯一一个实际执行计算的循环。

有关其他信息和示例,另请参见特征描述

74. 独立的例程

Routines not defined within any class or role.

这些例程(routines)与一个或几个其他类一起定义在不同的文件中,但实际上并不附属于任何特定的类或角色(role)。

74.1. 例程 EVAL

被定义为:

proto sub EVAL($code where Blob|Cool|Callable, Str() :$lang = 'Raku',
                PseudoStash :$context, *%n)
multi sub EVAL($code, Str :$lang where { ($lang // '') eq 'Perl5' },
                PseudoStash :$context)

这个例程将 Cool $code 强转为 Str。如果 $code 是一个 Blob,它将使用与 $lang 编译器相同的编码进行处理:对于 Raku $lang,使用 utf-8;对于 Perl,使用与 perl 相同的规则进行处理。

这与字面字符串参数的工作原理一样。更复杂的输入,比如一个变量或内嵌代码的字符串,默认情况下是非法的。这可以通过几种方式来覆盖。

use MONKEY-SEE-NO-EVAL; # Or...
use MONKEY;             # shortcut that turns on all MONKEY pragmas
use Test;

# any of the above allows:
EVAL "say { 5 + 5 }";   # OUTPUT: «10␤»

如果 MONKEY-SEE-NO-EVAL 编译指令没有被激活,编译器就会以 EVAL is a very dangerous function!!! 异常来抱怨。而这基本上是正确的,因为这将会运行与程序具有相同权限的任意代码。如果你激活了 MONKEY-SEE-NO-EVAL 编译指令,你应该注意清理将要通过 EVAL 的代码。

请注意,你可以使用引号插值来创建例程名,在这个例子其他插值创建标识符名的方法中可以看到。不过,这只对已经声明的函数和其他对象有效,因此使用起来比较安全。

当前词法作用域内的符号对 EVAL 中的代码是可见的。

my $answer = 42;
EVAL 'say $answer;';    # OUTPUT: «42␤»

然而,由于词法作用域中的符号集在编译后是不可改变的,所以 EVAL 永远不能将符号引入到周围的作用域中。

EVAL 'my $lives = 9'; say $lives;   # error, $lives not declared

此外,EVAL 是在当前的包中进行求值的。

module M {
    EVAL 'our $answer = 42'
}
say $M::answer;         # OUTPUT: «42␤»

而且也在当前的语言中,也就是说任何增加的语法都可以使用。

sub infix:<mean>(*@a) is assoc<list> {
    @a.sum / @a.elems
}

EVAL 'say 2 mean 6 mean 4';     # OUTPUT: «4␤»

EVAL 语句计算为最后一条语句的结果。

sub infix:<mean>(*@a) is assoc<list> {
    @a.sum / @a.elems
}
say EVAL 'say 1; 2 mean 6 mean 4';         # OUTPUT: «1␤4␤»

EVAL 也是执行其他语言代码的通道。

EVAL "use v5.20; say 'Hello from perl!'", :lang<Perl5>;

你需要有 Inline::Perl5 才能正常工作。

74.2. 子例程 EVALFILE

被定义为:

sub EVALFILE($filename where Blob|Cool, :$lang = 'Raku')

读取指定的文件并对其进行计算。在 Blob 解码、作用域和 $lang 参数方面与 EVAL 的行为相同。计算为文件中最后一条语句所产生的值。

EVALFILE "foo.p6";

74.3. 子例程 mkdir

被定义为:

sub    mkdir(IO() $path, Int() $mode = 0o777 --> IO::Path:D)

创建一个新的目录;关于 $mode 的解释和有效值,请参见 mode。成功后返回指向新创建目录的 IO::Path 对象;如果不能创建目录,则以 X::IO::Mkdir 失败

也会根据需要创建父目录(类似于 *nix 实用程序 mkdir-p 选项);也就是说,mkdir "foo/bar/ber/meow" 将创建 foofoo/barfoo/bar/ber 目录(如果它们不存在),以及 foo/bar/ber/meow

74.4. 子例程 chdir

被定义为:

sub chdir(IO() $path, :$d = True, :$r, :$w, :$x --> IO::Path:D)

改变 $*CWD 变量的值到所提供的 $path,同时确保新路径通过几个文件测试。注意: 这个例程不会改变进程的当前目录(参见 &*chdir)。

成功时返回 IO::Path,代表新的 $*CWD。失败时,返回 Failure,并保持 $*CWD 不变。$path 可以是任何有 IO 方法的对象,这些方法可以返回 IO::Path 对象。可用的文件测试有:

  • :d — 检查 .d 返回 True

  • :r — 检查 .r 返回 True

  • :w — 检查 .w 返回 True

  • :x — 检查 .x 返回 True

默认情况下,只进行 :d 测试。

chdir         '/tmp'; # change $*CWD to '/tmp' and check its .d is True
chdir :r, :w, '/tmp'; # … check its .r and .w are True
chdir '/not-there';   # returns Failure

请注意,下面的构造是错误的:

# WRONG! DO NOT DO THIS!
my $*CWD = chdir '/tmp/';

使用 indir 代替。

74.5. 子例程 &*chdir

被定义为:

PROCESS::<&chdir> = sub (IO() $path --> IO::Path:D) { }

$*CWD 变量的值改为提供的 $path,并将进程的当前目录设置为 $path.absolute 的值。注意: 在大多数情况下,你需要使用 chdir 例程来代替。

成功时返回一个代表新的 $*CWDIO::Path。失败时,返回 Failure,并保持 $*CWD 不动。$path 可以是任何带有 IO 方法的对象,该方法可以返回一个 IO::Path 对象。

请注意,与普通的 chdir 不同,没有参数来指定要执行哪些文件测试。

&*chdir('/tmp');  # change $*CWD and process's current directory to '/tmp'
&*chdir('/not-there'); # returns Failure

请注意,下面的构造是错误的:

# WRONG! DO NOT DO THIS!
my $*CWD = &*chdir('/tmp');

使用下面的内容代替;如果你不需要改变进程的当前目录,请参见 indir

temp $*CWD;
&*chdir('/tmp');

74.6. 子例程 chmod

被定义为:

sub chmod(Int() $mode, *@filenames --> List)

将所有 @filenames 强转为 IO::Path 并调用 IO::Path.chmod,并对其进行 $mode 操作。返回一个包含成功执行了 chmod@filenames 子集的列表

chmod 0o755, <myfile1  myfile2>; # make two files executable by the owner

74.7. 子例程 indir

被定义为:

sub indir(IO() $path, &code, :$d = True, :$r, :$w, :$x --> Mu)

接收 Callable &code,并在本地(对 &code)将 $*CWD 变量改为基于 $pathIO::Path 对象后执行,同时确保新路径通过几个文件测试。如果 $path 是相对的,那么即使给定了一个 IO::Path 对象,它也会变成一个绝对路径。注意: 这个例程不会改变进程的当前目录(参见 &*chdir)。在 &code 之外的 $*CWD 不受影响,即使 &code 显式地给 $*CWD 分配了一个新的值。

成功时返回 &code 的返回值。在未能成功改变 $*CWD 时,返回失败警告: 请记住,被惰性计算的东西在实际计算时,可能最终没有在动态作用域中被 indir 设置的 $*CWD。要么确保生成器有它们的 $*CWD 集,要么在从 indir 返回结果之前急切地计算它们。

say indir("/tmp", {
    gather { take ".".IO }
})».CWD; # OUTPUT: «(/home/camelia)␤»

say indir("/tmp", {
    eager gather { take ".".IO }
})».CWD; # OUTPUT: «(/tmp)␤»

say indir("/tmp", {
    my $cwd = $*CWD;
    gather { temp $*CWD = $cwd; take ".".IO }
})».CWD; # OUTPUT: «(/tmp)␤»

例程的 $path 参数可以是任何具有 IO 方法的对象,该方法返回一个 IO::Path 对象。可用的文件测试有:

  • :d — 检查 .d 返回 True

  • :r — 检查 .r 返回 True

  • :w — 检查 .w 返回 True

  • :x — 检查 .x 返回 True

默认情况下,只进行 :d 测试。

say $*CWD;                   # OUTPUT: «"/home/camelia".IO␤»
indir '/tmp', { say $*CWD }; # OUTPUT: «"/tmp".IO␤»
say $*CWD;                   # OUTPUT: «"/home/camelia".IO␤»

indir '/not-there', {;};     # returns Failure; path does not exist

74.8. 子例程 print

被定义为:

multi sub print(**@args --> True)
multi sub print(Junction:D --> True)

将给定的文本打印在标准输出上($*OUT 文件句柄),通过调用 .Str 方法将非 Str 对象强转为 StrJunction 参数自动线程化,且不保证打印字符串的顺序。

print "Hi there!\n";       # OUTPUT: «Hi there!␤»
print "Hi there!";         # OUTPUT: «Hi there!»
print [1, 2, 3];           # OUTPUT: «1 2 3»
print "Hello" | "Goodbye"; # OUTPUT: «HelloGoodbye»

要打印文本并包含尾部的换行,使用 put

74.9. 子例程 put

被定义为:

multi sub put()
multi sub put(**@args --> True)
multi sub put(Junction:D --> True)
multi sub put(Str:D \x)
multi sub put(\x)

print 一样,只是在结尾处使用 print-nl(默认打印一个换行)。Junction 参数自动线程化,且不保证打印字符串的顺序。

put "Hi there!\n";   # OUTPUT: «Hi there!␤␤»
put "Hi there!";     # OUTPUT: «Hi there!␤»
put [1, 2, 3];       # OUTPUT: «1 2 3␤»
put "Hello" | "Goodbye"; # OUTPUT: «Hello␤Goodbye␤»

本身,put() 将打印一行新的内容:

put "Hey"; put(); put("Hey"); # OUTPUT: «Hey␤␤Hey␤»

但请注意,我们在 put 后面使用了括号。它本身会引发一个异常(6.d及以后的版本)。如果在 for 前面用过这种方式,它也会引发一个异常;请使用 .put 这种方法来代替。

.put for <1 2 3>;             # # OUTPUT: «1␤2␤3␤»

74.10. 子例程 say

被定义为:

multi sub say(**@args --> True)

打印给定对象的 "gist";当对象是 Str 的子类时,它总是调用 .gist。除了使用 .gist 方法来获取对象的字符串表示之外,与 put 一样,它将自动线程化 Junction

注意: 某些对象的 .gist 方法,如 List,只返回对象的部分信息(因此称为 "gist")。如果你想打印文本信息,你很可能要用 put 来代替。

say Range;        # OUTPUT: «(Range)␤»
say class Foo {}; # OUTPUT: «(Foo)␤»
say 'I ♥ Raku';   # OUTPUT: «I ♥ Raku␤»
say 1..Inf;       # OUTPUT: «1..Inf␤»

74.11. 子例程 note

被定义为:

method note(Mu: -->Bool:D)
multi sub note(            --> Bool:D)
multi sub note(Str:D $note --> Bool:D)
multi sub note(**@args     --> Bool:D)

就像 say(在意义上,它将调用打印对象的 .gist 方法),除了它打印输出到 $*ERR 句柄(STDERR)。如果没有给子例程形式的参数,将使用字符串 "Noted"

note;       # STDERR OUTPUT: «Noted␤»
note 'foo'; # STDERR OUTPUT: «foo␤»
note 1..*;  # STDERR OUTPUT: «1..Inf␤»

这个命令也会在 Junction 上自动线程化,如果对象是 Str 的子类,保证会在对象上调用 gist

74.12. 子例程 prompt

multi sub prompt()
multi sub prompt($msg)

如果提供了 $msg,则将 $msg 打印$*OUT 句柄,然后从 $*IN 句柄获取一行输入。默认情况下,这相当于将 $msg 打印到 STDOUT,从 STDIN 中读取一行,删除后面的新行,然后返回结果字符串。从 Rakudo 2018.08 开始,prompt 将为数值创建同质异形,相当于调用 val prompt

my $name = prompt "What's your name? ";
say "Hi, $name! Nice to meet you!";
my $age = prompt("Say your age (number)");
my Int $years = $age;
my Str $age-badge = $age;

在上面的代码中,如果正确地输入了数字,$age 将被鸭式输入(duck-typed)到同质异形 IntStr 中。

74.13. 子例程 open

multi sub open(IO() $path, |args --> IO::Handle:D)

用给定的 $path 创建一个句柄,并调用 IO::Handle.open,将剩余的参数传递给它。请注意 IO::Path 类型提供了许多从文件中读写的方法,所以在许多常见的情况下,你不需要直接打开(open)文件或处理 IO::Handle 类型。

my $fh = open :w, '/tmp/some-file.txt';
$fh.say: 'I ♥ writing Perl code';
$fh.close;

$fh = open '/tmp/some-file.txt';
print $fh.readchars: 4;
$fh.seek: 7, SeekFromCurrent;
say $fh.readchars: 4;
$fh.close;

# OUTPUT: «I ♥ Perl␤»

74.14. 子例程 slurp

被定义为:

multi sub slurp(IO::Handle:D $fh = $*ARGFILES, |c)
multi sub slurp(IO() $path, |c)

将整个文件的内容 Slurps 成一个 Str(如果是 :bin,则为 Buf)。接受 :bin:enc 可选的命名参数,与 open() 意义相同;可能的编码与所有其他 IO 方法相同,并在 encoding 例程中列出。如果文件不存在,或者是一个目录,该例程将失败(fail)。在没有任何参数的情况下,子例程 slurp$*ARGFILES 进行操作,在没有任何文件名的情况下,默认为 $*IN

# read entire file as (Unicode) Str
my $text_contents   = slurp "path/to/file";

# read entire file as Latin1 Str
my $text_contents   = slurp "path/to/file", enc => "latin1";

# read entire file as Buf
my $binary_contents = slurp "path/to/file", :bin;

74.15. 子例程 spurt

被定义为:

multi spurt(IO() $path, |c)

$path 可以是任何带有 IO 方法的对象,该方法返回一个 IO::Path 对象。在 $path 上调用 IO::Path.spurt,转发剩余的任何参数。

选项

  • :enc

要写的内容的编码。

  • :append

布尔值,表示是否要追加到(潜在的)现有文件。如果文件还不存在,则会创建。默认值为 False

  • :createonly

布尔值,表示是否在文件已经存在的情况下失败。默认值为 False

例子:

# write directly to a file
spurt 'path/to/file', 'default text, directly written';

# write directly with a non-Unicode encoding
spurt 'path/to/latin1_file', 'latin1 text: äöüß', :enc<latin1>;

spurt 'file-that-already-exists', 'some text';           # overwrite file's contents:
spurt 'file-that-already-exists', ' new text', :append;  # append to file's contents:
say slurp 'file-that-already-exists';                    # OUTPUT: «some text new text␤»

# fail when writing to a pre-existing file
spurt 'file-that-already-exists', 'new text', :createonly;
# OUTPUT: «Failed to open file /home/camelia/file-that-already-exists: file already exists …»

74.16. 子例程 run

被定义为:

sub run(
    *@args ($, *@),
    :$in = '-',
    :$out = '-',
    :$err = '-',
    Bool :$bin = False,
    Bool :$chomp = True,
    Bool :$merge = False,
    Str:D :$enc = 'UTF-8',
    Str:D :$nl = "\n",
    :$cwd = $*CWD,
    Hash() :$env = %*ENV,
    :$win-verbatim-args = False
--> Proc:D)

在不涉及 shell 的情况下运行外部命令,并返回一个 Proc 对象,默认情况下,外部命令将打印到标准输出和错误,并从标准输入中读取。

run 'touch', '--', '*.txt'; # Create a file named “*.txt”

run <rm -- *.txt>; # Another way to use run, using word quoting for the
                   # arguments

如果你想传递一些变量,你仍然可以使用 < >,但尽量避免使用 « »,因为如果你忘了给变量加引号,它将会进行分词。

my $file = ‘--my arbitrary filename’;
run ‘touch’, ‘--’, $file;  # RIGHT
run <touch -->, $file;     # RIGHT

run «touch -- "$file"»;    # RIGHT but WRONG if you forget quotes
run «touch -- $file»;      # WRONG; touches ‘--my’, ‘arbitrary’ and ‘filename’
run ‘touch’, $file;        # WRONG; error from `touch`
run «touch "$file"»;       # WRONG; error from `touch`

请注意,许多程序都需要 -- 来区分命令行参数和以连字符开头的文件名

一个进程的sunk Proc 对象如果退出不成功,会抛出。如果你想忽略这样的失败,只需在非沉没上下文中使用 run

run 'false';     # SUNK! Will throw
run('false').so; # OK. Evaluates Proc in Bool context; no sinking

如果您想捕获标准输出或错误,而不是直接将其打印出来,您可以使用 :out:err 参数,这将使它们可以使用各自的方法。Proc.outProc.err

my $proc = run 'echo', 'Raku is Great!', :out, :err;
$proc.out.slurp(:close).say; # OUTPUT: «Raku is Great!␤»
$proc.err.slurp(:close).say; # OUTPUT: «␤»

你可以使用这些参数将它们重定向到一个文件句柄上,从而创建一种管道。

my $ls-alt-handle = open :w, '/tmp/cur-dir-ls-alt.txt';
my $proc = run "ls", "-alt", :out($ls-alt-handle);
# (The file will contain the output of the ls -alt command)

这些参数是相当灵活的,例如,接纳句柄来重定向它们。详见 ProcProc::Async

关于所有参数的更多例子和解释,也请参见 newspwn

74.17. 子例程 shell

multi sub shell($cmd, :$in = '-', :$out = '-', :$err = '-',
                Bool :$bin, Bool :$chomp = True, Bool :$merge,
                Str :$enc, Str:D :$nl = "\n", :$cwd = $*CWD, :$env)

通过系统 shell 运行命令,在 Windows 中默认为 %*ENV<ComSpec> /c,否则为 /bin/sh -c。所有 shell 元字符都会被 shell 解释,包括管道、重定向、环境变量替换等。Shell 转义符是一个严重的安全问题,可能会与不寻常的文件名产生混淆。为了安全起见,请使用 run

返回值的类型是 Proc

shell 'ls -lR | gzip -9 > ls-lR.gz';

详见 Proc,例如如何捕获输出。

74.18. 例程 unpolar

被定义为:

method unpolar(Real $angle)
multi sub unpolar(Real $mag, Real $angle)

返回一个 Complex,其坐标与弧度的角度相对应,以及与对象值相对应的幅度,如果是作为 sub 对象使用,则返回 $mag

say 1.unpolar(⅓*pi);
# OUTPUT: «0.5000000000000001+0.8660254037844386i␤»

74.19. 例程 printf

被定义为:

method printf (*@args)
multi sub printf(Cool:D $format, *@args)

作为方法,以对象为格式,使用与 Str.sprintf 相同的语言;作为 sub,它的第一个参数将是格式字符串,其余参数将按照格式约定代入格式。

"%s is %s".printf("þor", "mighty");    # OUTPUT: «þor is mighty»
printf( "%s is %s", "þor", "mighty");  # OUTPUT: «þor is mighty»

Junction 上,也会自动线程化,不保证顺序。

printf( "%.2f ", ⅓ | ¼ | ¾ ); # OUTPUT: «0.33 0.25 0.75 »

74.20. 例程 sprintf

被定义为:

method sprintf(*@args)
multi sub sprintf(Cool:D $format, *@args)

按照与 Str.sprintf 相同的语言来格式化并返回一个字符串,使用这样的格式,要么是对象(如果以方法形式调用),要么是第一个参数(如果以例程形式调用)。

sprintf( "%s the %d%s", "þor", 1, "st").put; # OUTPUT: «þor the 1st␤»
sprintf( "%s is %s", "þor", "mighty").put;   # OUTPUT: «þor is mighty␤»
"%s's weight is %.2f %s".sprintf( "Mjölnir", 3.3392, "kg").put;
# OUTPUT: «Mjölnir's weight is 3.34 kg␤»

这个函数与 C 库的 sprintfprintf 函数基本相同。这两个函数的唯一区别是 sprintf 返回的是一个字符串,而 printf 函数写入到一个文件句柄。sprintf 返回的是一个 Str,而不是一个字面值。

$format 会扫描 % 字符。任何 % 都会引入一个格式标记。指令引导参数的使用(如果有的话)。当使用 % 以外的指令时,它指示下一个传递的参数如何被格式化到要创建的字符串中。在格式标记中也可以使用参数索引。它们的形式为 N$,下面将详细解释。

可以用单引号或双引号来定义 $format。双引号的 $format 字符串在扫描前会进行内插,任何内插值包含 % 字符的嵌入式字符串都会引起异常。例如:

my $prod = "Ab-%x-42";
my $cost = "30";
sprintf("Product $prod; cost: \$%d", $cost).put;
# OUTPUT: «Your printf-style directives specify 2 arguments, but 1 argument was supplied␤»
          «  in block <unit> at <unknown file> line 1␤»

当处理未知的输入时,你应该避免使用这样的语法,把所有的变量放在 *@args 数组中,并在 $format 中为每个变量设置一个 %。如果你需要在格式字符串中包含一个 $ 符号(即使是作为参数索引),要么转义,要么使用单引号形式。例如,以下任何一种形式都可以无误地工作。

sprintf("2 x \$20 = \$%d", 2*20).put; # OUTPUT: «2 x $20 = $40␤»
sprintf('2 x $20 = $%d', 2*20).put;   # OUTPUT: «2 x $20 = $40␤»

总而言之,除非你需要一些非常特殊的东西,否则使用单引号的格式字符串和不在格式字符串中使用插值字符串,你会减少意外的问题。

74.20.1. 指令

%

百分号

c

代码点的字符

s

字符串

d

有符号整数,十进制

u

无符号整数,十进制

o

无符号整数,八进制

x

无符号整数,十六进制

e

浮点数,科学计数法

f

浮点数,固定的十进制记法

g

浮点数,记法为 %e 或 %f

X

像 x 那样, 但是使用大写字母

E

像 e 那样, 但是使用大写的 "E"

G

像 g 那样, 但是有一个大写的 "E" (如果适用的话)

b

二进制无符号整数

兼容性:

i

%d 的同义词

D

%ld 的同义词

U

%lu 的同义词

O

%lo 的同义词

F

%f 的同义词

74.21. 修饰符

修饰符改变了格式指令的含义,但基本上是无操作的(语义还在确定中)。

h

将整数解释为原生的 "short"(通常为 int16)

NYI

l

将整数解释为原生的 "long"(通常为 int32 或 int64)

NYI

ll

将整数解释为原生的 "long long"(通常为 int64)

NYI

L

将整数解释为原生的 "long long"(通常为 uint64)

NYI

q

将整数解释为原生的 "quads"(通常为 int64 或更大)

% 和格式字母之间,您可以指定几个附加属性来控制格式的解释。按顺序,这些属性是:

74.21.1. NYI 格式参数索引, 使用 '$' 符号

在指令前有一个明确的格式化参数索引(范围从1到N个参数),比如 %2$d。默认情况下,sprintf 会格式化列表中下一个未使用的参数,但是参数索引允许你不按顺序使用参数(注意,除非你转义 $,否则需要单引号)。

没有索引的情况下,sprintf 会格式化列表中下一个未使用的参数。

sprintf '%d %d', 12, 34;      # OUTPUT: «12 34␤»
sprintf '%d %d %d', 1, 2, 3;  # OUTPUT: «1 2 3␤»

NYI 带索引:

当我们对所有的指令进行索引时,第一个例子就像我们期望的那样工作。

sprintf '%2$d %1$d', 12, 34;      # OUTPUT: «34 12␤»

但请注意第二个例子中混合索引和非索引指令时的效果(小心你要求的东西)。第二条非索引指令得到了第一个参数,但在最后一条指令中也被特别要求。

sprintf '%3$d %d %1$d', 1, 2, 3;  # OUTPUT: «3 1 1␤»

标志

其中一项或多项:

space

在非负数前面置一个空格

+

在非负数前面置一个加号

-

栏内左对齐

0

使用前导零,而不是空格,作为所需的填充。

#

确保任何八进制的前导 "0",非零的十六进制前缀为 "0x" 或 "0X",非零的二进制前缀为 "0b" 或 "0B"

v

NYI 向量标志(仅与指令 "d" 一起使用),见下文说明。

例如:

sprintf '<% d>',  12;   # OUTPUT: «< 12>␤»
sprintf '<% d>',   0;   # OUTPUT: «< 0>"»
sprintf '<% d>', -12;   # OUTPUT: «<-12>␤»
sprintf '<%+d>',  12;   # OUTPUT: «<+12>␤»
sprintf '<%+d>',   0;   # OUTPUT: «<+0>"»
sprintf '<%+d>', -12;   # OUTPUT: «<-12>␤»
sprintf '<%6s>',  12;   # OUTPUT: «<    12>␤»
sprintf '<%-6s>', 12;   # OUTPUT: «<12    >␤»
sprintf '<%06s>', 12;   # OUTPUT: «<000012>␤»
sprintf '<%#o>',  12;   # OUTPUT: «<014>␤»
sprintf '<%#x>',  12;   # OUTPUT: «<0xc>␤»
sprintf '<%#X>',  12;   # OUTPUT: «<0XC>␤»
sprintf '<%#b>',  12;   # OUTPUT: «<0b1100>␤»
sprintf '<%#B>',  12;   # OUTPUT: «<0B1100>␤»

当同时给出空格和加号作为标志时,空格将被忽略。

sprintf '<%+ d>', 12;   # OUTPUT: «<+12>␤»
sprintf '<% +d>', 12;   # OUTPUT: «<+12>␤»

当在 %o 转换中给出 # 标志和一个精度时,会在开头加上必要数量的 0。如果数值为0,精度为0,则不输出任何内容;精度为0或小于实际元素数,则返回带0的数到左边。

say sprintf '<%#.5o>', 0o12;     # OUTPUT: «<00012>␤»
say sprintf '<%#.5o>', 0o12345;  # OUTPUT: «<012345>␤»
say sprintf '<%#.0o>', 0;        # OUTPUT: «<>␤» zero precision and value 0
                                 #               results in no output!
say sprintf '<%#.0o>', 0o1       # OUTPUT: «<01>␤»

向量标志 'v'

这个特殊的标志(v,后面是指令 d)告诉 Raku 将所提供的字符串解释为整数向量,字符串中的每个字符都有一个整数向量(ord 例程用于转换为整数)。Raku 依次将格式应用于每个整数,然后用分隔符(默认为点 '.')将生成的字符串连接起来。这对于在任意字符串中显示字符的序数值非常有用。

NYI sprintf "%vd", "AB\x[100]";           # OUTPUT: «65.66.256␤»

你也可以通过使用带有参数索引的星号(例如 *2$v)来明确指定用于分隔符的参数编号;例如:

NYI sprintf '%*4$vX %*4$vX %*4$vX',       # 3 IPv6 addresses
        @addr[1..3], ":";

宽度 (minimum)

缺省情况下,参数通常被格式化为只有显示给定值所需的宽度。你可以在这里指定一个最小宽度,通过在这里输入一个数字来覆盖默认宽度,或者从下一个参数(用 *)或从指定的参数(例如用 *2$)中获取所需的宽度。

sprintf "<%s>", "a";           # OUTPUT: «<a>␤»
sprintf "<%6s>", "a";          # OUTPUT: «<     a>␤»
sprintf "<%*s>", 6, "a";       # OUTPUT: «<     a>␤»
NYI sprintf '<%*2$s>', "a", 6; # OUTPUT: «<     a>␤»
sprintf "<%2s>", "long";       # OUTPUT: «<long>␤»   (does not truncate)

在所有情况下,指定的宽度将根据需要增加,以适应给定的积分数值或字符串。如果通过 * 获得的字段宽度为负值,它的效果与 - 标志相同:左对齐。

精度,或最大宽度

您可以通过指定一个 . 和一个数字来指定一个精度(用于数字转换)或最大宽度(用于字符串转换)。对于浮点格式,除了 gG 之外,指定小数点右边多少位(默认为 6)。例如::

# These examples are subject to system-specific variation.
sprintf '<%f>', 1;    # OUTPUT: «"<1.000000>"␤»
sprintf '<%.1f>', 1;  # OUTPUT: «"<1.0>"␤»
sprintf '<%.0f>', 1;  # OUTPUT: «"<1>"␤»
sprintf '<%e>', 10;   # OUTPUT: «"<1.000000e+01>"␤»
sprintf '<%.1e>', 10; # OUTPUT: «"<1.0e+01>"␤»

对于 "g" 和 "G",这指定了要显示的最大数字,包括小数点之前的数字和小数点之后的数字;例如。

# These examples are subject to system-specific variation.
sprintf '<%g>', 1;        # OUTPUT: «<1>␤»
sprintf '<%.10g>', 1;     # OUTPUT: «<1>␤»
sprintf '<%g>', 100;      # OUTPUT: «<100>␤»
sprintf '<%.1g>', 100;    # OUTPUT: «<1e+02>␤»
sprintf '<%.2g>', 100.01; # OUTPUT: «<1e+02>␤»
sprintf '<%.5g>', 100.01; # OUTPUT: «<100.01>␤»
sprintf '<%.4g>', 100.01; # OUTPUT: «<100>␤»

对于整数转换,指定一个精度意味着数字本身的输出应该被零填充到这个宽度(其中0标志被忽略)。

(注意,这个功能目前适用于无符号整数转换,但不适用于有符号整数。)

sprintf '<%.6d>', 1;         # OUTPUT: «<000001>␤»
NYI sprintf '<%+.6d>', 1;    # OUTPUT: «<+000001>␤»
NYI sprintf '<%-10.6d>', 1;  # OUTPUT: «<000001    >␤»
sprintf '<%10.6d>', 1;       # OUTPUT: «<    000001>␤»
NYI sprintf '<%010.6d>', 1;  # OUTPUT: «<    000001>␤»
NYI sprintf '<%+10.6d>', 1;  # OUTPUT: «<   +000001>␤»
sprintf '<%.6x>', 1;         # OUTPUT: «<000001>␤»
sprintf '<%#.6x>', 1;        # OUTPUT: «<0x000001>␤»
sprintf '<%-10.6x>', 1;      # OUTPUT: «<000001    >␤»
sprintf '<%10.6x>', 1;       # OUTPUT: «<    000001>␤»
sprintf '<%010.6x>', 1;      # OUTPUT: «<    000001>␤»
sprintf '<%#10.6x>', 1;      # OUTPUT: «<  0x000001>␤»

对于字符串转换,指定一个精度可以截断字符串以适应指定的宽度。

sprintf '<%.5s>', "truncated";   # OUTPUT: «<trunc>␤»
sprintf '<%10.5s>', "truncated"; # OUTPUT: «<     trunc>␤»

你也可以使用 .* 从下一个参数中获取精度,或者从指定的参数中获取精度(例如,使用 .*2$)。

sprintf '<%.6x>', 1;           # OUTPUT: «<000001>␤»
sprintf '<%.*x>', 6, 1;        # OUTPUT: «<000001>␤»
NYI sprintf '<%.*2$x>', 1, 6;  # OUTPUT: «<000001>␤»
NYI sprintf '<%6.*2$x>', 1, 4; # OUTPUT: «<  0001>␤»

如果通过 * 得到的精度为负值,则算作没有精度。

sprintf '<%.*s>',  7, "string";   # OUTPUT: «<string>␤»
sprintf '<%.*s>',  3, "string";   # OUTPUT: «<str>␤»
sprintf '<%.*s>',  0, "string";   # OUTPUT: «<>␤»
sprintf '<%.*s>', -1, "string";   # OUTPUT: «<string>␤»
sprintf '<%.*d>',  1, 0;          # OUTPUT: «<0>␤»
sprintf '<%.*d>',  0, 0;          # OUTPUT: «<>␤»
sprintf '<%.*d>', -1, 0;          # OUTPUT: «<0>␤»

Size

对于数字转换,您可以使用 lhVqLll 指定解释数字的大小。对于整数转换(d u o x X b i D U O),数字通常被假定为你的平台上默认的整数大小(通常是32或64位),但是你可以覆盖它来使用标准的C类型,正如用于构建 Raku 的编译器所支持的那样。

(注:以下类型均未实现。)

hh

将整数解释为C类型的 "char" 或 "unsigned char"。

h

解释为C型 "short" 或 "unsigned short" 的整数

j

将整数解释为C类型的 "intmax_t",只有在C99编译器上才可以使用(不可移植)

l

解释为C类型的 "long" 或 "unsigned long" 的整数。

q, L, or ll

将整数解释为C类型的 "long long"、"unsigned long long" 或 "quad"(典型的64位整数)。

t

将整数解释为C类型的 "ptrdiff_t"

z

将整数解释为C类型的 "size_t"

参数的顺序

通常情况下,sprintf 在每一个格式规范中都会取下一个未被使用的参数作为要格式化的值,如果格式规范使用了 * 来要求额外的参数,那么这些参数就会从参数列表中按它们在格式规范中出现的顺序被消耗掉。如果参数是由显式索引指定的,这不会影响参数的正常顺序,即使显式指定的索引本来是下一个参数。

所以:

my $a = 5; my $b = 2; my $c = 'net';
sprintf "<%*.*s>", $a, $b, $c; # OUTPUT: «<   ne>␤»

$a 表示宽度,用 $b 表示精度,用 $c 表示要格式化的值;而:

NYI sprintf '<%*1$.*s>', $a, $b;

将使用 $a 作为宽度和精度,使用 $b 作为格式化的值。

下面是一些更多的例子;请注意,当使用显式索引时,如果格式字符串是双引号,则 $ 需要转义。

sprintf "%2\$d %d\n",      12, 34;         # OUTPUT: «34 12␤␤»
sprintf "%2\$d %d %d\n",   12, 34;         # OUTPUT: «34 12 34␤␤»
sprintf "%3\$d %d %d\n",   12, 34, 56;     # OUTPUT: «56 12 34␤␤»
NYI sprintf "%2\$*3\$d %d\n",  12, 34,  3; # OUTPUT: « 34 12␤␤»
NYI sprintf "%*1\$.*f\n",       4,  5, 10; # OUTPUT: «5.0000␤␤»

其他例子:

NYI sprintf "%ld a big number", 4294967295;
NYI sprintf "%%lld a bigger number", 4294967296;
sprintf('%c', 97);                  # OUTPUT: «a␤»
sprintf("%.2f", 1.969);             # OUTPUT: «1.97␤»
sprintf("%+.3f", 3.141592);         # OUTPUT: «+3.142␤»
sprintf('%2$d %1$d', 12, 34);       # OUTPUT: «34 12␤»
sprintf("%x", 255);                 # OUTPUT: «ff␤»

特殊情况:sprintf("<b>%s</b>n", "Raku") 将无法工作,但以下情况之一可以:

sprintf Q:b "<b>%s</b>\n",  "Raku"; # OUTPUT: «<b>Raku</b>␤␤»
sprintf     "<b>\%s</b>\n", "Raku"; # OUTPUT: «<b>Raku</b>␤␤»
sprintf     "<b>%s\</b>\n", "Raku"; # OUTPUT: «<b>Raku</b>␤␤»

74.22. 子例程 flat

被定义为:

multi flat(**@list)
multi flat(Iterable \a)

构造一个包含所提供的任何参数的列表,并返回对该列表或 Iterable 调用 .flat 方法(继承自 Any)的结果。

say flat 1, (2, (3, 4), $(5, 6)); # OUTPUT: «(1 2 3 4 (5 6))␤»

74.23. 子例程 unique

被定义为:

multi sub unique(+values, |c)

从调用者/参数列表中返回一个唯一值的序列,这样在结果列表中只保留每个重复值的第一次出现。unique 使用 === 操作符的语义来决定两个对象是否相同,除非用另一个比较器指定了可选的 :with 参数。即使删除了重复值,原始列表的顺序也会被保留。

示例:

say <a a b b b c c>.unique;   # OUTPUT: «(a b c)␤»
say <a b b c c b a>.unique;   # OUTPUT: «(a b c)␤»

(如果你知道输入的对象是相邻的,那么就用 squish 来代替。)

可选的 :as 参数允许你在 unique-ing 之前对元素进行标准化/规范化。为了比较的目的,这些值会被转换,但是进入结果列表的仍然是原始值;但是,只有第一次出现的值会出现在列表中。

例如:

say <a A B b c b C>.unique(:as(&lc))      # OUTPUT: «(a B c)␤»

我们也可以用可选的 :with 参数指定比较器。例如,如果想要一个唯一的哈希列表,可以使用 eqv 比较器。

例子:

my @list = %(a => 42), %(b => 13), %(a => 42);
say @list.unique(:with(&[eqv]))           # OUTPUT: «({a => 42} {b => 13})␤»

注意: 由于 :with Callable 必须对列表中的所有项进行尝试,这使得 unique 跟随路径的算法复杂度更高。你应该尽可能地使用 :as 参数来代替。

74.24. 子例程 repeated

被定义为:

multi sub    repeated(+values, |c)

它从调用者/参数列表中返回一个重复的值序列。它的参数与 unique 相同,但不是在元素第一次出现时通过,而是在第二次(或更多次)出现时才通过。

例子:

say <a a b b b c c>.repeated;                   # OUTPUT: «(a b b c)␤»
say <a b b c c b a>.repeated;                   # OUTPUT: «(b c b a)␤»
say <a A B b c b C>.repeated(:as(&lc));         # OUTPUT: «(A b b C)␤»

my @list = %(a => 42), %(b => 13), %(a => 42);
say @list.repeated(:with(&[eqv]))               # OUTPUT: «({a => 42})␤»

unique 的情况一样,关联参数 :as 接收一个 Callable,在比较前对元素进行归一化,而 :with 接收一个将要使用的相等比较函数。

74.25. 例程 squish

被定义为:

sub          squish( +values, |c)

从调用者/参数列表中返回一个值的序列,其中一个或多个值的运行只被第一个实例替换。和 unique 一样,squish 使用 === 运算符的语义来决定两个对象是否相同。与 unique 不同的是,这个函数只删除相邻的重复值;相隔较远的相同值仍然被保留。即使删除重复的对象,原始列表的顺序也会被保留。

例子:

say <a a b b b c c>.squish; # OUTPUT: «(a b c)␤»
say <a b b c c b a>.squish; # OUTPUT: «(a b c b a)␤»

可选的 :as 参数,就像 unique 参数一样,允许在比较前对值进行临时转换。

可选的 :with 参数用于设置一个合适的比较运算符。

say [42, "42"].squish;                      # OUTPUT: «(42 42)␤»
# Note that the second item in the result is still Str
say [42, "42"].squish(with => &infix:<eq>); # OUTPUT: «(42)␤»
# The resulting item is Int

74.26. 子例程 emit

被定义为:

sub emit(\value --> Nil)

如果在任何供应或 react 块之外使用,则会抛出一个异常 emit without supply or react。在 Supply 块中,它将向流中添加一条消息。

my $supply = supply {
  for 1 .. 10 {
      emit($_);
  }
}
$supply.tap( -> $v { say "First : $v" });

也请看 emit 方法的页面

74.27. 子例程 undefine

被定义为:

multi sub undefine(Mu    \x)
multi sub undefine(Array \x)
multi sub undefine(Hash  \x)

在6.d语言版本中被取消(DEPRECATED),在 6.e 中将被删除。对于数组散列,它将变得等同于赋值为空(Empty);对于其他一切,在数组或哈希的情况下,等同于赋值为 NilEmpty,建议使用。

74.28. 控制例程

改变程序流程的例程,或许返回一个值。

74.29. 子例程 exit

被定义为:

multi sub exit()
multi sub exit(Int(Any) $status)

退出当前进程,返回代码为 $status,如果没有指定值,则为零。当退出值($status)与零不同时,必须从捕获它的进程(如 shell)中择机评估;它是从 Main 中返回与零不同的退出代码的唯一方法。

exit 可以防止 LEAVE phaser 被执行,但它会运行 &*EXIT 变量中的代码。

exit 应该作为最后的手段,只用于向父进程发出与零不同的退出代码的信号,而不是例外地终止一个方法或子:使用异常来代替。

74.30. 子例程 done

被定义为:

sub done(--> Nil)

如果在任何供应或 react 块之外使用,会抛出一个 done without supply or react 的异常。在 Supply 块中,它将表明该 Supply 将不再发出任何东西。也请参见关于方法 done 的文档。

my $supply = supply {
    for 1 .. 3 {
        emit($_);
    }
    done;
}
$supply.tap( -> $v { say "Second : $v" }, done => { say "No more" });
# OUTPUT: OUTPUT: «Second : 1␤Second : 2␤Second : 3␤No More␤»

传递给 done 命名参数的块将在 supply 块中调用 done 时运行。

75. 角色 Associative

Object that supports looking up values by key

role Associative[::TValue = Mu, ::TKey = Str(Any)] { }

对于通过 postcircumfix:<{ }> 支持基于名称查找的类型来说,是一个常见的角色,例如 HashMap。它用于操作符中的类型检查,这些操作符期望找到要调用的特定方法。详情请参见 Subscripts

% 魔符将变量限制在做 Associative 的对象上,所以如果你想在你的类中使用它,你必须混入这个角色。

class Whatever {};
my %whatever := Whatever.new;
# OUTPUT: «Type check failed in binding; expected Associative but got Whatever

请注意,我们在这里使用的是绑定 :=,因为默认情况下,% 赋值期望在右侧有一个 Hash。但是,有了 Associative 角色:

class Whatever is Associative {};
my %whatever := Whatever.new;

在语法上将是正确的。

75.1. 方法

75.1.1. 方法 of

被定义为:

method of()

Associative 其实是一个参数化的角色,它可以使用不同的类作为键和值。从文档顶部可以看到,默认情况下,它的键强转为 Str,而值则使用非常通用的 Mu

my %any-hash;
say %any-hash.of;#  OUTPUT: «(Mu)␤»

该值是你用特定类实例化 Associative 时使用的第一个参数:

class DateHash is Hash does Associative[Cool,DateTime] {};
my %date-hash := DateHash.new;
say %date-hash.of; # OUTPUT: «(Cool)␤»

75.1.2. 方法 keyof

被定义为:

method keyof()

返回用于 Associative 角色的参数化键,默认为 Any 强转为 Str。当你使用 Associative 的参数化版本时,这是被用作第二个参数的类。

my %any-hash;
%any-hash.keyof; #OUTPUT: «(Str(Any))␤»

75.2. 应提供的方法

如果你想让你的类实现 Associative 角色,你需要提供这些方法。

75.2.1. 方法 AT-KEY

method AT-KEY(\key)

应该返回给定键的值/容器。

75.2.2. 方法 EXISTS-KEY

method EXISTS-KEY(\key)

应该返回一个 Bool,表示给定的键是否真的有值。

75.2.3. 方法 STORE

method STORE(\values, :$initialize)

只有当你想支持下面的语法的时候,才应该提供这个方法:

my %h is Foo = a => 42, b => 666;

该语法用于绑定你的 Associative 角色的实现。

应该接受要(重新)初始化对象的值,这些值可以是 Pair,也可以是单独的 key/value 对。当该方法第一次在对象上被调用时,可选的命名参数将包含一个 True 值。应该返回调用者。

75.3. 另见

请参阅关联下标的实现方法,了解可以为 Associative 角色实现的其他方法。

75.4. 类型图

Associative 的类型关系

type graph Associative

76. 类 Attribute

Member variable

class Attribute { }

在 Raku 的行话中,属性指的是每个实例/对象的存储槽。Attribute 是用来谈论元层面的类和角色的属性。

属性的正常使用不需要用户明确地使用这个类。

76.1. 特性

76.1.1. 特性 is default

被赋值为 Nil 的属性将恢复到用特性 is default 设置的默认值。对于数组或关联体,is default 的参数将设置默认项的值或哈希值。

class C {
    has $.a is default(42) is rw = 666
}
my $c = C.new;
say $c;
$c.a = Nil;
say $c;
# OUTPUT: «C.new(a => 666)␤C.new(a => 42)␤»
class Foo {
    has @.bar is default(42) is rw
};
my $foo = Foo.new( bar => <a b c> );
$foo.bar =Nil;
say $foo; # OUTPUT: «Foo.new(bar => [42])␤»

76.1.2. 特性 is required

被定义为:

multi sub trait_mod:<is> (Attribute $attr, :$required!)

特性 is required 将在实例化对象时标记该属性为必须用一个值来填充。如果没有这样做,将导致运行时错误。

class C {
    has $.a is required
}
my $c = C.new;
CATCH{ default { say .^name, ': ', .Str } }
# OUTPUT: «X::Attribute::Required: The attribute '$!a' is required, but you did not provide a value for it.␤»

这个特性也允许属性的类型有 :D 笑脸,而不给它们一个默认值:

class Power {
    has Numeric:D $.base     is required;
    has Numeric:D $.exponent is required;
    multi method Numeric(::?CLASS:D: --> Numeric:D) {
        $!base ** $!exponent
    }
}

从 6.d 语言版本开始提供(Rakudo 编译器 2018.08+ 中存在早期实现)。你可以指定为什么需要这个属性的原因。

class D {
    has $.a is required("it is a good idea");
}
my $d = D.new;
CATCH{ default { say .^name, ': ', .Str } }
# OUTPUT: «X::Attribute::Required: The attribute '$!a' is required because it is a good idea,␤but you did not provide a value for it.␤»

is required 不只是影响默认构造函数,它还会检查较低层级的属性,所以它将适用于使用 bless 编写的自定义构造函数。

76.1.3. 特性 is DEPRECATED

multi sub trait_mod:<is>(Attribute:D $r, :$DEPRECATED!)

将一个属性标记为废弃的属性,可选择使用什么来代替该废弃属性。

class C {
    has $.foo is DEPRECATED("'bar'");
}
my $c = C.new( foo => 42 );  # doesn't trigger with initialization (yet)
say $c.foo;                  # does trigger on usage

程序完成后,在 STDERR 上会显示这样的内容:

# Saw 1 occurrence of deprecated code.
# =====================================
# Method foo (from C) seen at:
# script.p6, line 5
# Please use 'bar' instead.

76.1.4. 特性 is rw

被定义为:

multi sub trait_mod:<is> (Attribute:D $attr, :$rw!)

将一个属性标记为可读/可写,而不是默认的 readonly。属性的默认访问器将返回一个可写的值。

class Boo {
   has $.bar is rw;
   has $.baz;
};

my $boo = Boo.new;
$boo.bar = 42; # works
$boo.baz = 42;
CATCH { default { put .^name, ': ', .Str } };
# OUTPUT: «X::Assignment::RO: Cannot modify an immutable Any␤»

76.1.5. 特性 is built

被定义为:

multi sub trait_mod:<is>(Attribute:D $a, :$built!)

默认情况下,这个特性允许在构建对象时通过 .new 设置私有属性。同样的特性也可以通过传递布尔值 False 来阻止通过 .new 设置公共属性。

class Foo {
    has $!bar is built; # same as `is built(True)`
    has $.baz is built(False);

    method bar {
        $!bar
    }
}

my $foo = Foo.new(bar => 1, baz => 2);
say $foo.bar; # «1␤»
say $foo.baz; # «Any␤»

76.2. 方法

获取 Attribute 类型对象的通常方法是通过自省:

class Useless {
    has @!things;
}
my $a = Useless.^attributes(:local)[0];
say $a.raku;            # OUTPUT: «Attribute.new␤»
say $a.name;            # OUTPUT: «@!things␤»
say $a.package;         # OUTPUT: «(Useless)␤»
say $a.has_accessor;    # OUTPUT: «False␤»

从外部修改一个私有属性通常是不可能的,但由于 Attribute 是在元类的层次上,所以一切都很公平。

76.2.1. 方法 name

被定义为:

method name(Attribute:D: --> Str:D)

返回属性的名称。请注意,这总是私有名称,所以如果一个属性被声明为 has $.a,那么返回的名称是 $!a

class Foo {
    has @!bar;
}
my $a = Foo.^attributes(:local)[0];
say $a.name;            # OUTPUT: «@!bar␤»

76.2.2. 方法 package

被定义为:

method package()

返回该属性所属的包(类/grammar/角色)。

class Boo {
    has @!baz;
}
my $a = Boo.^attributes(:local)[0];
say $a.package;         # OUTPUT: «(Boo)␤»

76.2.3. 方法 has_accessor

被定义为:

method has_accessor(Attribute:D: --> Bool:D)

如果属性有公共访问器方法,则返回 True

class Container {
    has $!private;
    has $.public;
}
my $private = Container.^attributes(:local)[0];
my $public = Container.^attributes(:local)[1];
say $private.has_accessor; # OUTPUT: «False␤»
say $public.has_accessor;  # OUTPUT: «True␤»

76.2.4. 方法 rw

被定义为:

method rw(Attribute:D: --> Bool:D)

对应用了 is rw 特性的属性返回 True

class Library {
    has $.address; # Read-only value
    has @.new-books is rw;
}
my $addr = Library.^attributes(:local)[0];
my $new-books = Library.^attributes(:local)[1];
say $addr.rw;      # OUTPUT: «False␤»
say $new-books.rw; # OUTPUT: «True␤»

76.2.5. 方法 readonly

被定义为:

method readonly(Attribute:D: --> Bool:D)

对于只读属性返回 True,这是默认值,对于标记为 is rw 的属性返回 False

class Library {
    has $.address; # Read-only value
    has @.new-books is rw;
}
my $addr = Library.^attributes(:local)[0];
my $new-books = Library.^attributes(:local)[1];
say $addr.readonly;      # OUTPUT: «True␤»
say $new-books.readonly; # OUTPUT: «False␤»

76.2.6. 方法 required

被定义为:

method required(Attribute:D: --> Any:D)

对于应用了 is required 特性的属性返回 1,如果没有应用该特性,则返回 Mu。如果应用了 is required 特性的属性有一个字符串,那么将返回该字符串,而不是 1

class Library {
    has $.address is required;
    has @.new-books is required("we always need more books");
}
my $addr = Library.^attributes(:local)[0];
my $new-books = Library.^attributes(:local)[1];
say $addr.required;      # OUTPUT: «1␤»
say $new-books.readonly; # OUTPUT: «"we always need more books"␤»

76.2.7. 方法 type

被定义为:

method type(Attribute:D: --> Mu)

返回属性的类型约束。

class TypeHouse {
    has Int @.array;
    has $!scalar;
    has @.mystery;
}
my @types = TypeHouse.^attributes(:local)[0..2];
for 0..2 { say @types[$_].type }
# OUTPUT: «(Positional[Int])
# (Mu)
# (Positional)␤»

76.2.8. 方法 get_value

被定义为:

method get_value(Mu $obj)

返回存储在对象 $obj 这个属性中的值。

class Violated {
    has $!private-thing = 5;
}
my $private = Violated.^attributes(:local)[0];
say $private.get_value(Violated.new); # OUTPUT: «5␤»

注意,此方法违反了对象的封装,使用时应谨慎。恶龙出没。

76.2.9. 方法 set_value

被定义为:

method set_value(Mu $obj, Mu \new_val)

将值 new_val 与对象 $obj 的这个属性绑定。

class A {
    has $!a = 5;
    method speak() { say $!a; }
}
my $attr = A.^attributes(:local)[0];
my $a = A.new;
$a.speak; # OUTPUT: «5␤»
$attr.set_value($a, 42);
$a.speak; # OUTPUT: «42␤»

注意,此方法违反了对象的封装,使用时应谨慎。恶龙出没。

76.2.10. 方法 gist

被定义为:

multi method gist(Attribute:D:)

返回类型的名称和属性的名称。

class Hero {
    has @!inventory;
    has Str $.name;
    submethod BUILD( :$name, :@inventory ) {
        $!name = $name;
        @!inventory = @inventory
    }
}
say Hero.^attributes(:local)[0]; # OUTPUT: «Positional @!inventory»

由于 say 隐式调用 .gist,这就是这里所产生的输出。

76.3. 可选的内省

76.3.1. DEPRECATED

如果一个属性被标记为 DEPRECATED,那么就可以调用 DEPRECATED 方法,并将返回 something else(如果没有指定具体的原因)或用 DEPRECATED 特性指定的字符串。

如果一个属性没有标记为 DEPRECATED,就不能调用 DEPRECATED 方法。因此,应该使用 .? 方法语法。

class Hangout {
    has $.table;
    has $.bar is DEPRECATED("the patio");
}
my $attr-table = Hangout.^attributes(:local)[0];
my $attr-bar = Hangout.^attributes(:local)[1];
with $attr-table.?DEPRECATED -> $text {     # does not trigger
    say "Table is deprecated with '$text'";
    # OUTPUT:
}
with $attr-bar.?DEPRECATED -> $text {
    say "Bar is deprecated with '$text'";
    # OUTPUT: «Bar is deprecated with 'the patio'"␤»
}

76.4. 类型图

Attribute 的类型关系

Attribute

77. 角色 Callable

Invocable code object

role Callable { ... }

角色,用于支持调用它们的对象。它用于 BlockRoutineSubMethodSubmethodMacro 类型。

可调用的对象可以存储在 & 魔符化的容器中,这种容器的默认类型约束是 Callable

my &a = {;}; # Empty block needs a semicolon
my &b = -> {};
my &c = sub () {};
sub foo() {};
my &d = &foo;

77.1. 方法

77.1.1. 方法 CALL-ME

method CALL-ME(Callable:D $self: |arguments)

这个方法是 postfix:«( )»postfix:«.( )» 所需要的。它是使一个对象真正可调用的方法,需要重载,让一个给定的对象像一个例程一样行动。如果对象需要存储在一个 & 魔符化的容器中,则必须实现 Callable

class A does Callable {
    submethod CALL-ME(|c){ 'called' }
}
my &a = A;
say a(); # OUTPUT: «called␤»

应用 Callable 角色并不是使对象可调用的必要条件,如果一个类只是想在常规标量容器中添加类似子程序的语义,可以使用子方法 CALL-ME 来实现。

class A {
    has @.values;
    submethod CALL-ME(Int $x where 0 <= * < @!values.elems) {
        @!values[$x]
    }
}
my $a = A.new: values => [4,5,6,7];
say $a(2); # OUTPUT: «6␤»

77.1.2. 方法 Capture

被定义为:

method Capture()

抛出 X::Cannot::Capture

77.2. 类型图

Callable 的类型关系

Callable

78. 类 Channel

Thread-safe queue for sending values from producers to consumers

class Channel {}

Channel 是一个线程安全的队列,它可以帮助你将一系列对象从一个或多个生产者发送到一个或多个消费者。每个对象将只到达一个这样的消费者,由调度器选择。如果只有一个消费者和一个生产者,对象的顺序就会被保证保留。在 Channel 上发送是非阻塞的。

my $c = Channel.new;
await (^10).map: {
    start {
        my $r = rand;
        sleep $r;
        $c.send($r);
    }
}
$c.close;
say $c.list;

更多的例子可以在并发页面找到。

78.1. 方法

78.1.1. 方法 send

被定义为:

method send(Channel:D: \item)

将一个项加入到 Channel 中。如果通道已经关闭,则抛出一个类型为 X::Channel::SendOnClosed 的异常。这个调用不会阻塞等待消费者获取对象。对可以排队的项的数量没有设定限制,所以应该注意防止排队失控。

my $c = Channel.new;
$c.send(1);
$c.send([2, 3, 4, 5]);
$c.close;
say $c.list; # OUTPUT: «(1 [2 3 4 5])␤»

78.1.2. 方法 receive

被定义为:

method receive(Channel:D:)

从通道中接收和删除一项。如果没有项存在,它就会阻塞,等待另一个线程的发送(send)。

如果通道已经关闭,并且最后一项已经被移除,或者在 receive 等待项到达时调用了 close,则抛出一个类型为 X::Channel::ReceiveOnClosed 的异常。

如果通道已经被方法 fail 标记为不稳定的,并且最后一项已经被移除,则抛出给 fail 的参数作为异常。

参见方法 poll,它是一个不会抛出异常的非阻塞版本。

my $c = Channel.new;
$c.send(1);
say $c.receive; # OUTPUT: «1␤»

78.1.3. 方法 poll

被定义为:

method poll(Channel:D:)

从通道中接收并删除一项。如果没有项,则返回 Nil,而不是等待。

my $c = Channel.new;
Promise.in(2).then: { $c.close; }
^10 .map({ $c.send($_); });
loop {
    if $c.poll -> $item { $item.say };
    if $c.closed  { last };
    sleep 0.1;
}

参见方法 receive,以获得正确响应通道关闭和失败的阻塞版本。

78.1.4. 方法 close

被定义为:

method close(Channel:D:)

正常关闭通道。这使得后续的 send 调用以 X::Channel::SendOnClosed 终止。随后对 .receive 的调用仍然可能会耗尽之前发送的剩余项,但如果队列是空的,将抛出一个 X::Channel::ReceiveOnClosed 异常。由于你可以通过用 @() 上下文化为数组或调用 .list 方法从一个通道中产生一个 Seq,这些方法不会终止,直到通道被关闭。一个 whenever 块儿也会在一个关闭的通道上正确终止。

my $c = Channel.new;
$c.close;
$c.send(1);
CATCH { default { put .^name, ': ', .Str } };
# OUTPUT: «X::Channel::SendOnClosed: Cannot send a message on a closed channel␤»

请注意,任何抛出的异常都可能阻止 .close 被调用,这可能会挂起接收线程。在这种情况下,请使用 LEAVE phaser 来强制调用 .close

78.1.5. 方法 list

被定义为:

method list(Channel:D: --> List:D)

返回一个基于 Seq 的列表,它将迭代队列中的项,并在迭代过程中从队列中删除每个项。只有在调用 close 方法后才能终止。

my $c = Channel.new; $c.send(1); $c.send(2);
$c.close;
say $c.list; # OUTPUT: «(1 2)␤»

78.1.6. 方法 closed

被定义为:

method closed(Channel:D: --> Promise:D)

返回一个 Promise,一旦通道被调用 close 方法关闭,这个 Promise 将被保留。

my $c = Channel.new;
$c.closed.then({ say "It's closed!" });
$c.close;
sleep 1;

78.1.7. 方法 fail

被定义为:

method fail(Channel:D: $error)

关闭通道(也就是让后续的 send 调用死掉),并将错误作为通道中的最后一个元素从队列中抛出。方法 receive 将把这个错误作为异常抛出。如果通道已经被关闭或 .fail 已经被调用,则不做任何事情。

my $c = Channel.new;
$c.fail("Bad error happens!");
$c.receive;
CATCH { default { put .^name, ': ', .Str } };
# OUTPUT: «X::AdHoc: Bad error happens!␤»

78.1.8. 方法 Capture

被定义为:

method Capture(Channel:D --> Capture:D)

相当于在调用者上调用 .List.Capture

78.1.9. 方法 Supply

被定义为:

method Supply(Channel:D:)

这将返回一个按需供应,它为通道上接收到的每一个值发出一个值。当通道关闭时,将在供应(Supply)上调用 done

my $c = Channel.new;
my Supply $s1 = $c.Supply;
my Supply $s2 = $c.Supply;
$s1.tap(-> $v { say "First $v" });
$s2.tap(-> $v { say "Second $v" });
^10 .map({ $c.send($_) });
sleep 1;

对该方法的多次调用会产生多个 Supply 实例,这些实例会对来自 Channel 的值进行竞争。

78.1.10. 子例程 await

被定义为:

multi sub await(Channel:D)
multi sub await(*@)

等到一个或多个通道都有可用的值,然后返回这些值(它在通道上调用 .receive)。也适用于 Promise

my $c = Channel.new;
Promise.in(1).then({$c.send(1)});
say await $c;

从 6.d 开始,它不再在等待时阻塞线程。

78.2. 类型图

Channel 的类型关系

Channel

79. 类 Date

Calendar date

class Date { }

Date 是一个不可改变的对象,用于标识公历中的某一天。

Date 对象支持整数的加减法,其中整数被解释为天数。你可以用数字比较运算符 ==<>>=!= 来比较 Date 对象,它们的字符串化是 YYYY-MM-DD 格式,这意味着用字符串运算符 eq, lt, le 等来比较它们也能得到正确的结果。

Date.today 根据系统时钟创建一个当前日期的对象。

my $d = Date.new(2015, 12, 24); # Christmas Eve!
say $d;                         # OUTPUT: «2015-12-24␤»
say $d.year;                    # OUTPUT: «2015␤»
say $d.month;                   # OUTPUT: «12␤»
say $d.day;                     # OUTPUT: «24␤»
say $d.day-of-week;             # OUTPUT: «4␤» (Thursday)
say $d.later(days => 20);       # OUTPUT: «2016-01-13␤»
my $n = Date.new('2015-12-31'); # New Year's Eve
say $n - $d;                    # OUTPUT: «7␤», 7 days between New Years/Christmas Eve
say $n + 1;                     # OUTPUT: «2016-01-01␤»

注意 从 6.d 版开始,.raku 可以在 Date 上调用。它也会拒绝接受合成数字,如 7̈。

79.1. 方法

79.1.1. 方法 new

被定义为:

multi method new($year, $month, $day, :&formatter --> Date:D)
multi method new(:$year!, :$month = 1, :$day = 1  --> Date:D)
multi method new(Str $date                        --> Date:D)
multi method new(Instant:D $dt                    --> Date:D)
multi method new(DateTime:D $dt                   --> Date:D)

创建一个新的 Date 对象,可以从一个可以强转为整数的三联(年、月、日)中创建,也可以从 YYYY-MM-DD(ISO 8601) 形式的字符串中创建,或者从一个 InstantDateTime 对象中创建。可选择接受一个 formatter 作为命名参数。

my $date = Date.new(2042, 1, 1);
$date = Date.new(year => 2042, month => 1, day => 1);
$date = Date.new("2042-01-01");
$date = Date.new(Instant.from-posix: 1482155532);
$date = Date.new(DateTime.now);

79.1.2. 方法 new-from-daycount

被定义为:

method new-from-daycount($daycount,:&formatter --> Date:D)

创建一个新的 Date 对象,给定 $daycount,即从1858年11月17日开始的天数,即修正的朱利安日。也可以接受一个 formatter 作为命名参数。

say Date.new-from-daycount(49987);          # OUTPUT: «1995-09-27␤»

79.1.3. 方法 last-date-in-month

被定义为:

method last-date-in-month(Date:D: --> Date:D)

返回 Date 对象当月的最后一个日期。否则,如果日期值已经是该月的最后一天,则返回调用者。

say Date.new('2015-11-24').last-date-in-month; # OUTPUT: «2015-11-30␤»

这应该允许更容易的范围,如:

$date .. $date.last-date-in-month

列出本月所有剩余日期。

79.1.4. 方法 first-date-in-month

被定义为:

method first-date-in-month(Date:D: --> Date:D)

返回 Date 对象当月的第一个日期。否则,如果日值已经是当月的第一天,则返回调用者。

say Date.new('2015-11-24').first-date-in-month; # OUTPUT: «2015-11-01␤»

79.1.5. 方法 clone

被定义为:

method clone(:$year, :$month, :$day, :&formatter)

基于调用者创建一个新的 Date 对象,但给定的参数会覆盖调用者的值。

say Date.new('2015-11-24').clone(month => 12);    # OUTPUT: «2015-12-24␤»

79.1.6. 方法 today

被定义为:

method today(:&formatter --> Date:D)

返回当前日期的 Date 对象。也可以接受一个 formatter 作为命名参数。

say Date.today;

79.1.7. 方法 later

被定义为:

method later(Date:D: *%unit)

返回一个基于当前对象的 Date 对象,但应用了一个日期 delta。日期 delta 可以作为一个命名参数传递,参数名是单位。

允许的单位是 day, days, week, weeks, month, months, year, years。请注意,复数形式只能用在 later 方法中。

请注意,特殊的 ":2nd" 命名参数语法可以是一种紧凑的、自文档化的方式来指定 delta 参数。

say Date.new('2015-12-24').later(:2years);  # OUTPUT: «2017-12-24␤»

由于几个不同时间单位的加法不具有换算性,所以只能传递一个单位。

my $d = Date.new('2015-02-27');
say $d.later(month => 1).later(:2days);  # OUTPUT: «2015-03-29␤»
say $d.later(days => 2).later(:1month);  # OUTPUT: «2015-04-01␤»
say $d.later(days => 2).later(:month);   # same, as +True === 1

负偏移是允许的,不过方法 earlier 更符合习惯。

79.1.8. 方法 earlier

被定义为:

method earlier(Date:D: *%unit)

返回一个基于当前日期的 Date 对象,但应用了一个朝向过去的日期 delta。使用方法请参见方法 later

my $d = Date.new('2015-02-27');
say $d.earlier(month => 5).earlier(:2days);  # OUTPUT: «2014-09-25␤»

79.1.9. 方法 truncated-to

被定义为:

method truncated-to(Date:D: Cool $unit)

返回截断到年、月或周的第一天的 Date。例如:

my $c = Date.new('2012-12-24');
say $c.truncated-to('year');     # OUTPUT: «2012-01-01␤»
say $c.truncated-to('month');    # OUTPUT: «2012-12-01␤»
say $c.truncated-to('week');     # OUTPUT: «2012-12-24␤», because it's Monday already

79.1.10. 方法 succ

被定义为:

method succ(Date:D: --> Date:D)

返回下一天的 Date。"succ" 是 "successor" 的缩写。

say Date.new("2016-02-28").succ;   # OUTPUT: «2016-02-29␤»

79.1.11. 方法 pred

被定义为:

method pred(Date:D: --> Date:D)

返回前一天的 Date。"pred" 是 "predecessor" 的缩写。

say Date.new("2016-01-01").pred;   # OUTPUT: «2015-12-31␤»

79.1.12. 方法 Str

被定义为:

multi method Str(Date:D: --> Str:D)

返回由 formatter 指定的调用者的字符串表示。如果没有指定 formatter,将返回一个(ISO 8601) 日期。

say Date.new('2015-12-24').Str;                     # OUTPUT: «2015-12-24␤»
my $fmt = { sprintf "%02d/%02d/%04d", .month, .day, .year };
say Date.new('2015-12-24', formatter => $fmt).Str;  # OUTPUT: «12/24/2015␤»

79.1.13. 方法 gist

被定义为:

multi method gist(Date:D: --> Str:D)

返回 YYYY-MM-DD 格式的日期(ISO 8601)。

say Date.new('2015-12-24').gist;                    # OUTPUT: «2015-12-24␤»

79.1.14. 方法 Date

被定义为:

method Date(--> Date)

返回调用者。

say Date.new('2015-12-24').Date;  # OUTPUT: «2015-12-24␤»
say Date.Date;                    # OUTPUT: «(Date)␤»

79.1.15. 方法 DateTime

被定义为:

multi method DateTime(Date:U --> DateTime:U)
multi method DateTime(Date:D --> DateTime:D)

将调用者转换为 DateTime

say Date.new('2015-12-24').DateTime; # OUTPUT: «2015-12-24T00:00:00Z␤»
say Date.DateTime;                   # OUTPUT: «(DateTime)␤»

79.2. 函数

79.2.1. 子例程 sleep

sub sleep($seconds = Inf --> Nil)

尝试在给定的 $seconds 秒数内休眠。完成后返回 Nil。接受 IntNumRatDuration 类型作为参数,因为所有这些类型也都遵循 Real

sleep 5;                # Int
sleep 5.2;              # Num
sleep (5/2);            # Rat
sleep (now - now + 5);  # Duration

因此,可以休眠一个非整数的时间。例如,下面的代码显示,sleep (5/2) 休眠时间为 2.5 秒,sleep 5.2 休眠时间为 5.2 秒。

my $before = now;
sleep (5/2);
my $after = now;
say $after-$before;  # OUTPUT: «2.502411561␤»

$before = now;
sleep 5.2;
$after = now;
say $after-$before;  # OUTPUT: «5.20156987␤»

79.2.2. 子例程 sleep-timer

sub sleep-timer(Real() $seconds = Inf --> Duration:D)

这个函数的实现方式和 sleep 一样,但与前者不同的是,它确实会返回一个 Duration 实例,其中包含系统没有休眠的秒数。

特别是当进程被一些外部事件(如虚拟机或操作系统事件)唤醒后,返回的 Duration 将处理剩余的秒数。在正常情况下,当休眠没有被中断时,返回的 Duration 的值为 0,意味着没有额外的秒数还在休眠中。因此,在正常情况下:

say sleep-timer 3.14;  # OUTPUT: «0␤»

同样的结果也适用于边缘情况,当一个负的或零的休眠时间作为参数传递时:

say sleep-timer -2; # OUTPUT: 0
say sleep-timer 0;  # OUTPUT: 0

另见 sleep-until

79.2.3. 子例程 sleep-until

sub sleep-until(Instant $until --> Bool)

工作原理类似于 sleep,但检查当前时间,并持续休眠,直到达到未来所需的瞬间。它在内部使用循环中的 sleep-timer 方法,以确保如果不小心被提前唤醒,它将再次等待指定的剩余时间来达到指定的瞬间。回到休眠状态。

如果未来的瞬时已经实现,则返回 True(通过休眠或因为它是现在),如果指定了过去的瞬时,则返回 False

如果要休眠到未来10秒,可以写这样的代码:

say sleep-until now+10;   # OUTPUT: «True␤»

想休眠到过去的某个时间是不行的:

my $instant = now - 5;
say sleep-until $instant; # OUTPUT: «False␤»

然而如果我们把瞬间放在足够远的未来,休眠应该会运行。

my $instant = now + 30;
# assuming the two commands are run within 30 seconds of one another...
say sleep-until $instant; # OUTPUT: «True␤»

要指定未来的一个确切的瞬间,首先在适当的时间点创建一个 DateTime,然后转换成一个 Instant

my $instant = DateTime.new(
    year => 2020,
    month => 9,
    day => 1,
    hour => 22,
    minute => 5);
say sleep-until $instant.Instant; # True (eventually...)

这可以作为一种原始的闹钟使用。例如,假设你需要在2015年9月4日早上7点起床,但由于某些原因,你常用的闹钟坏了,你只有笔记本电脑。你可以把起床时间(要注意时区,因为 DateTime.new 默认使用 UTC)指定为一个 Instant,然后把这个时间传递给 sleep-until,之后你就可以播放一个 mp3 文件来代替你平时的闹钟叫醒你。这个方案大致是这样的:

# DateTime.new uses UTC by default, so get time zone from current time
my $timezone = DateTime.now.timezone;
my $instant = DateTime.new(
    year => 2015,
    month => 9,
    day => 4,
    hour => 7,
    minute => 0,
    timezone => $timezone
).Instant;
sleep-until $instant;
qqx{mplayer wake-me-up.mp3};

79.2.4. 子例程 infix:<→

multi sub infix:<-> (Date:D, Int:D --> Date:D)
multi sub infix:<-> (Date:D, Date:D --> Int:D)

取一个要减去的日期和一个 Int(代表要减去的天数)或另一个 Date 对象。分别返回一个新的 Date 对象或两个日期之间的天数。

say Date.new('2016-12-25') - Date.new('2016-12-24'); # OUTPUT: «1␤»
say Date.new('2015-12-25') - Date.new('2016-11-21'); # OUTPUT: «-332␤»
say Date.new('2016-11-21') - 332;                    # OUTPUT: «2015-12-25␤»

79.2.5. 子例程 infix:<+>

multi sub infix:<+> (Date:D, Int:D --> Date:D)
multi sub infix:<+> (Int:D, Date:D --> Date:D)

取一个 Int,并将该天数加到给定的 Date 对象上。

say Date.new('2015-12-25') + 332; # OUTPUT: «2016-11-21␤»
say 1 + Date.new('2015-12-25');   # OUTPUT: «2015-12-26␤»

79.3. 类型图

Date 的类型关系

Date

79.4. 角色 Dateish 提供的例程

Date 做的角色是 Dateish,它提供了以下例程。

79.4.1. 方法 year

被定义为:

method year(Date:D: --> Int:D)

返回日期的年份。

say Date.new('2015-12-31').year;                                  # OUTPUT: «2015␤»
say DateTime.new(date => Date.new('2015-12-24'), hour => 1).year; # OUTPUT: «2015␤»

79.4.2. 方法 month

被定义为:

method month(Date:D: --> Int:D)

返回日期的月份(1..12).

say Date.new('2015-12-31').month;                                  # OUTPUT: «12␤»
say DateTime.new(date => Date.new('2015-12-24'), hour => 1).month; # OUTPUT: «12␤»

79.4.3. 方法 day

被定义为:

method day(Date:D: --> Int:D)

返回日期的月日(1…​31)。

say Date.new('2015-12-31').day;                                  # OUTPUT: «31␤»
say DateTime.new(date => Date.new('2015-12-24'), hour => 1).day; # OUTPUT: «24␤»

79.4.4. 方法 formatter

被定义为:

method formatter(Dateish:D:)

返回用于转换为 Str 的格式化函数。如果在构建对象时没有提供任何格式化函数,则使用一个默认的格式化函数。在这种情况下,该方法将返回一个 Callable 类型的对象。

格式化函数由 DateTime 方法 Str 调用,调用者为其唯一参数。

my $dt = Date.new('2015-12-31');  # (no formatter specified)
say $dt.formatter.^name;          # OUTPUT: «Callable␤»
my $us-format = sub ($self) { sprintf "%02d/%02d/%04d", .month, .day, .year given $self; };
$dt = Date.new('2015-12-31', formatter => $us-format);
say $dt.formatter.^name;           # OUTPUT: «Sub␤»
say $dt;                          # OUTPUT: «12/31/2015␤»

79.4.5. 方法 is-leap-year

被定义为:

method is-leap-year(--> Bool:D)

如果 Dateish 对象的年份是闰年,则返回 True

say DateTime.new(:year<2016>).is-leap-year; # OUTPUT: «True␤»
say Date.new("1900-01-01").is-leap-year;    # OUTPUT: «False␤»

79.4.6. 方法 day-of-month

被定义为:

method day-of-month(Date:D: --> Int:D)

返回日期的月日(1..31)。与 day 方法同义。

say Date.new('2015-12-31').day-of-month;                                  # OUTPUT: «31␤»
say DateTime.new(date => Date.new('2015-12-24'), hour => 1).day-of-month; # OUTPUT: «24␤»

79.4.7. 方法 day-of-week

被定义为:

method day-of-week(Date:D: --> Int:D)

返回一周中的第几天,其中1为周一,2为周二,周日为7。

say Date.new('2015-12-31').day-of-week;                                  # OUTPUT: «4␤»
say DateTime.new(date => Date.new('2015-12-24'), hour => 1).day-of-week; # OUTPUT: «4␤»

79.4.8. 方法 day-of-year

被定义为:

method day-of-year(Date:D: --> Int:D)

返回一年中的第几天(1..366)。

say Date.new('2015-12-31').day-of-year;                                  # OUTPUT: «365␤»
say DateTime.new(date => Date.new('2015-03-24'), hour => 1).day-of-year; # OUTPUT: «83␤»

79.4.9. 方法 days-in-month

被定义为:

method days-in-month(Dateish:D: --> Int:D)

返回 Dateish 对象所代表的月份的天数。

say Date.new("2016-01-02").days-in-month;                # OUTPUT: «31␤»
say DateTime.new(:year<10000>, :month<2>).days-in-month; # OUTPUT: «29␤»

79.4.10. 方法 week

被定义为:

method week()

返回两个整数的列表:年份和星期数。这是因为在一年的开始或结束时,周数可能属于另一年。

my ($year, $week) = Date.new("2014-12-31").week;
say $year;                       # OUTPUT: «2015␤»
say $week;                       # OUTPUT: «1␤»
say Date.new('2015-01-31').week; # OUTPUT: «(2015 5)␤»

79.4.11. 方法 week-number

被定义为:

method week-number(Date:D: --> Int:D)

返回调用者指定日期的周数(1..53)。ISO 将一年的第一周定义为包含一月第四天的一周。因此,1月初的日期往往被放在上一年的最后一周,同样,12月的最后几天也可能被放在下一年的第一周。

say Date.new("2014-12-31").week-number;   # 1  (first week of 2015)
say Date.new("2016-01-02").week-number;   # 53 (last week of 2015)

79.4.12. 方法 week-year

被定义为:

method week-year(Date:D: --> Int:D)

返回调用者指定日期的星期年。通常 week-year 等于 Date.year。但是请注意,一月初的日期通常会被放在上一年的最后一周,同样,十二月的最后几天也可能被放在下一年的第一周。

say Date.new("2015-11-15").week-year;   # 2015
say Date.new("2014-12-31").week-year;   # 2015 (date belongs to the first week of 2015)
say Date.new("2016-01-02").week-year;   # 2015 (date belongs to the last week of 2015)

79.4.13. 方法 weekday-of-month

被定义为:

method weekday-of-month(Date:D: --> Int:D)

返回一个数字(1..5),表示当月某一周的某一天到目前为止发生的次数,包括这一天本身。

say Date.new("2003-06-09").weekday-of-month;  # 2  (second Monday of the month)

79.4.14. 方法 yyyy-mm-dd

被定义为:

method yyyy-mm-dd(str $sep = "-" --> Str:D)

返回 YYYY-MM-DD 格式的日期(ISO 8601)。可选的位置参数 $sep,默认为 -,是日期不同部分之间的一个字符分隔符。

say Date.new("2015-11-15").yyyy-mm-dd;   # OUTPUT: «2015-11-15␤»
say DateTime.new(1470853583).yyyy-mm-dd; # OUTPUT: «2016-08-10␤»
say Date.today.yyyy-mm-dd("/");          # OUTPUT: «2020/03/14␤»

79.4.15. 方法 mm-dd-yyyy

被定义为:

method mm-dd-yyyy(str $sep = "-" --> Str:D)

返回 MM-DD-YYYY 格式的日期(ISO 8601)。可选的位置参数 $sep,默认为 -,是日期不同部分之间的一个字符分隔符。

say Date.new("2015-11-15").mm-dd-yyyy;   # OUTPUT: «11-15-2015␤»
say DateTime.new(1470853583).mm-dd-yyyy; # OUTPUT: «08-10-2016␤»
say Date.today.mm-dd-yyyy("/");          # OUTPUT: «03/14/2020␤»

79.4.16. 方法 dd-mm-yyyy

被定义为:

method dd-mm-yyyy(str $sep = "-" --> Str:D)

返回 DD-MM-YYYY 格式的日期(ISO 8601)。可选的位置参数 $sep,默认为 -,是日期不同部分之间的一个字符分隔符。

say Date.new("2015-11-15").dd-mm-yyyy;    # OUTPUT: «15-11-2015␤»
say DateTime.new(1470853583).dd-mm-yyyy;  # OUTPUT: «10-08-2016␤»
say Date.today.dd-mm-yyyy("/");           # OUTPUT: «14/03/2020␤»

79.4.17. 方法 daycount

被定义为:

method daycount(Dateish:D: --> Int:D)

返回从1858年11月17日到调用者的日子的天数。本方法返回的日数是 MJD,即修正的朱利安日,它被天文学家、大地测量学家、科学家和其他人士经常使用。MJD 约定是为了方便简化年表计算。

say Date.new('1995-09-27').daycount;    # OUTPUT: «49987␤»

79.4.18. 方法 IO

被定义为:

method IO(Dateish:D: --> IO::Path:D)

返回一个 IO::Path 对象,表示 Dateish 对象的字符串化值。

Date.today.IO.say;   # OUTPUT: «"2016-10-03".IO␤»
DateTime.now.IO.say; # OUTPUT: «"2016-10-03T11:14:47.977994-04:00".IO␤»

移植注意: 某些操作系统(如 Windows)不允许在文件名中使用冒号(:),而在从 DateTime 对象创建的 IO::Path 中会有冒号。

80. 角色 Dateish

Object that can be treated as a date

role Dateish { ... }

DateDateTime 都支持访问年、月和月日,以及相关功能,如计算星期几。

80.1. 方法

80.1.1. 方法 year

被定义为:

method year(Date:D: --> Int:D)

返回日期的年份。

say Date.new('2015-12-31').year;                                  # OUTPUT: «2015␤»
say DateTime.new(date => Date.new('2015-12-24'), hour => 1).year; # OUTPUT: «2015␤»

80.1.2. 方法 month

被定义为:

method month(Date:D: --> Int:D)

返回日期的月份(1..12).

say Date.new('2015-12-31').month;                                  # OUTPUT: «12␤»
say DateTime.new(date => Date.new('2015-12-24'), hour => 1).month; # OUTPUT: «12␤»

80.1.3. 方法 day

被定义为:

method day(Date:D: --> Int:D)

返回日期的月日(1…​31)。

say Date.new('2015-12-31').day;                                  # OUTPUT: «31␤»
say DateTime.new(date => Date.new('2015-12-24'), hour => 1).day; # OUTPUT: «24␤»

80.1.4. 方法 formatter

被定义为:

method formatter(Dateish:D:)

返回用于转换为 Str 的格式化函数。如果在构建对象时没有提供任何格式化函数,则使用一个默认的格式化函数。在这种情况下,该方法将返回一个 Callable 类型的对象。

格式化函数由 DateTime 方法 Str 调用,调用者为其唯一参数。

my $dt = Date.new('2015-12-31');  # (no formatter specified)
say $dt.formatter.^name;          # OUTPUT: «Callable␤»
my $us-format = sub ($self) { sprintf "%02d/%02d/%04d", .month, .day, .year given $self; };
$dt = Date.new('2015-12-31', formatter => $us-format);
say $dt.formatter.^name;           # OUTPUT: «Sub␤»
say $dt;                          # OUTPUT: «12/31/2015␤»

80.1.5. 方法 is-leap-year

被定义为:

method is-leap-year(--> Bool:D)

如果 Dateish 对象的年份是闰年,则返回 True

say DateTime.new(:year<2016>).is-leap-year; # OUTPUT: «True␤»
say Date.new("1900-01-01").is-leap-year;    # OUTPUT: «False␤»

80.1.6. 方法 day-of-month

被定义为:

method day-of-month(Date:D: --> Int:D)

返回日期的月日(1..31)。与 day 方法同义。

say Date.new('2015-12-31').day-of-month;                                  # OUTPUT: «31␤»
say DateTime.new(date => Date.new('2015-12-24'), hour => 1).day-of-month; # OUTPUT: «24␤»

80.1.7. 方法 day-of-week

被定义为:

method day-of-week(Date:D: --> Int:D)

返回一周中的第几天,其中1为周一,2为周二,周日为7。

say Date.new('2015-12-31').day-of-week;                                  # OUTPUT: «4␤»
say DateTime.new(date => Date.new('2015-12-24'), hour => 1).day-of-week; # OUTPUT: «4␤»

80.1.8. 方法 day-of-year

被定义为:

method day-of-year(Date:D: --> Int:D)

返回一年中的第几天(1..366)。

say Date.new('2015-12-31').day-of-year;                                  # OUTPUT: «365␤»
say DateTime.new(date => Date.new('2015-03-24'), hour => 1).day-of-year; # OUTPUT: «83␤»

80.1.9. 方法 days-in-month

被定义为:

method days-in-month(Dateish:D: --> Int:D)

返回 Dateish 对象所代表的月份的天数。

say Date.new("2016-01-02").days-in-month;                # OUTPUT: «31␤»
say DateTime.new(:year<10000>, :month<2>).days-in-month; # OUTPUT: «29␤»

80.1.10. 方法 week

被定义为:

method week()

返回两个整数的列表:年份和星期数。这是因为在一年的开始或结束时,周数可能属于另一年。

my ($year, $week) = Date.new("2014-12-31").week;
say $year;                       # OUTPUT: «2015␤»
say $week;                       # OUTPUT: «1␤»
say Date.new('2015-01-31').week; # OUTPUT: «(2015 5)␤»

80.1.11. 方法 week-number

被定义为:

method week-number(Date:D: --> Int:D)

返回调用者指定日期的周数(1..53)。ISO 将一年的第一周定义为包含一月第四天的一周。因此,1月初的日期往往被放在上一年的最后一周,同样,12月的最后几天也可能被放在下一年的第一周。

say Date.new("2014-12-31").week-number;   # 1  (first week of 2015)
say Date.new("2016-01-02").week-number;   # 53 (last week of 2015)

80.1.12. 方法 week-year

被定义为:

method week-year(Date:D: --> Int:D)

返回调用者指定日期的星期年。通常 week-year 等于 Date.year。但是请注意,一月初的日期通常会被放在上一年的最后一周,同样,十二月的最后几天也可能被放在下一年的第一周。

say Date.new("2015-11-15").week-year;   # 2015
say Date.new("2014-12-31").week-year;   # 2015 (date belongs to the first week of 2015)
say Date.new("2016-01-02").week-year;   # 2015 (date belongs to the last week of 2015)

80.1.13. 方法 weekday-of-month

被定义为:

method weekday-of-month(Date:D: --> Int:D)

返回一个数字(1..5),表示当月某一周的某一天到目前为止发生的次数,包括这一天本身。

say Date.new("2003-06-09").weekday-of-month;  # 2  (second Monday of the month)

80.1.14. 方法 yyyy-mm-dd

被定义为:

method yyyy-mm-dd(str $sep = "-" --> Str:D)

返回 YYYY-MM-DD 格式的日期(ISO 8601)。可选的位置参数 $sep,默认为 -,是日期不同部分之间的一个字符分隔符。

say Date.new("2015-11-15").yyyy-mm-dd;   # OUTPUT: «2015-11-15␤»
say DateTime.new(1470853583).yyyy-mm-dd; # OUTPUT: «2016-08-10␤»
say Date.today.yyyy-mm-dd("/");          # OUTPUT: «2020/03/14␤»

80.1.15. 方法 mm-dd-yyyy

被定义为:

method mm-dd-yyyy(str $sep = "-" --> Str:D)

返回 MM-DD-YYYY 格式的日期(ISO 8601)。可选的位置参数 $sep,默认为 -,是日期不同部分之间的一个字符分隔符。

say Date.new("2015-11-15").mm-dd-yyyy;   # OUTPUT: «11-15-2015␤»
say DateTime.new(1470853583).mm-dd-yyyy; # OUTPUT: «08-10-2016␤»
say Date.today.mm-dd-yyyy("/");          # OUTPUT: «03/14/2020␤»

80.1.16. 方法 dd-mm-yyyy

被定义为:

method dd-mm-yyyy(str $sep = "-" --> Str:D)

返回 DD-MM-YYYY 格式的日期(ISO 8601)。可选的位置参数 $sep,默认为 -,是日期不同部分之间的一个字符分隔符。

say Date.new("2015-11-15").dd-mm-yyyy;    # OUTPUT: «15-11-2015␤»
say DateTime.new(1470853583).dd-mm-yyyy;  # OUTPUT: «10-08-2016␤»
say Date.today.dd-mm-yyyy("/");           # OUTPUT: «14/03/2020␤»

80.1.17. 方法 daycount

被定义为:

method daycount(Dateish:D: --> Int:D)

返回从1858年11月17日到调用者的日子的天数。本方法返回的日数是 MJD,即修正的朱利安日,它被天文学家、大地测量学家、科学家和其他人士经常使用。MJD 约定是为了方便简化年表计算。

say Date.new('1995-09-27').daycount;    # OUTPUT: «49987␤»

80.1.18. 方法 IO

被定义为:

method IO(Dateish:D: --> IO::Path:D)

返回一个 IO::Path 对象,表示 Dateish 对象的字符串化值。

Date.today.IO.say;   # OUTPUT: «"2016-10-03".IO␤»
DateTime.now.IO.say; # OUTPUT: «"2016-10-03T11:14:47.977994-04:00".IO␤»

移植注意: 某些操作系统(如 Windows)不允许在文件名中使用冒号(:),而在从 DateTime 对象创建的 IO::Path 中会有冒号。

80.2. 类型图

Dateish 的类型关系

Dateish

81. 类 DateTime

Calendar date with time

class DateTime does Dateish {}

为了处理民用时间点,DateTime 对象存储了年、月、日、时、分(全部为 Int)、秒(可能是小数)和一个时区。

它提供了用于计算日期和时间的方法。

DateTime 方法是不可改变的;如果你想修改 DateTime,请创建一个修改的副本。

时区的处理是以 UTC 偏移的秒数为单位的整数,而不是以时区名称为单位。

my $dt = DateTime.new(
    year    => 2015,
    month   => 11,
    day     => 21,
    hour    => 16,
    minute  => 1,
);

say $dt;                            # OUTPUT: «2015-11-21T16:01:00Z␤»
say $dt.later(days => 20);          # OUTPUT: «2015-12-11T16:01:00Z␤»
say $dt.truncated-to('hour');       # OUTPUT: «2015-11-21T16:00:00Z␤»
say $dt.in-timezone(-8 * 3600);     # OUTPUT: «2015-11-21T08:01:00-0800␤»

my $now = DateTime.now(formatter => { sprintf "%02d:%02d", .hour, .minute });
say $now;                           # 12:45 (or something like that)

从 6.d 版开始,使用合成代码点如 7̈ 会导致错误。

81.1. 方法

81.1.1. new

被定义为:

multi method new(Int :$year!, Int :$month = 1, Int :$day = 1,
                 Int :$hour = 0, Int :$minute = 0, :$second = 0,
                 Int :$timezone = 0, :&formatter)
multi method new(Date :$date!,
                 Int :$hour = 0, Int :$minute = 0, :$second = 0,
                 Int :$timezone = 0, :&formatter)
multi method new(Int() $year, Int() $month, Int() $day,
                 Int() $hour, Int $minute, $second,
                 Int() :$timezone = 0, :&formatter)
multi method new(Instant:D $i,  :$timezone=0, :&formatter)
multi method new(Int:D $posix,  :$timezone=0, :&formatter)
multi method new(Str:D $format, :$timezone=0, :&formatter)

创建一个新的 DateTime 对象。创建一个新的 DateTime 对象的一个选择是分别从组件(年、月、日、小时…​)。另一种方法是为日期组件传递一个 Date 对象,并指定时间组件。还有一种方法是从一个 Instant 中获取时间,只提供时区和格式器。或者你可以提供一个 Int 作为 UNIX 时间戳,而不是一个 Instant

你也可以提供一个 ISO 8601 时间戳符号格式的 Str,或者作为一个完整的 RFC 3339 日期和时间。字符串的格式应该是 yyyy-mm-ddThh:mm:ssZyyyy-mm-ddThh:mm:ss+0100。我们比 ISO 8601 标准的限制要少一些,因为我们允许 Unicode 数字和混合压缩和扩展时间格式。

无效的输入字符串会引发一个类型为 X::Temporal::InvalidFormat 的异常。如果你提供了一个包含时区的字符串,并提供了名为 timezone 的参数,则会抛出一个类型为 X::DateTime::TimezoneClash 的异常。

my $datetime = DateTime.new(year => 2015,
                            month => 1,
                            day => 1,
                            hour => 1,
                            minute => 1,
                            second => 1,
                            timezone => 1);
$datetime = DateTime.new(date => Date.new('2015-12-24'),
                         hour => 1,
                         minute => 1,
                         second => 1,
                         timezone => 1);
$datetime = DateTime.new(2015, 1, 1, # First January of 2015
                         1, 1, 1);   # Hour, minute, second with default time zone
$datetime = DateTime.new(now);                       # Instant.
# from a Unix timestamp
say $datetime = DateTime.new(1470853583);            # OUTPUT: «2016-08-10T18:26:23Z␤»
$datetime = DateTime.new("2015-01-01T03:17:30+0500") # Formatted string

81.1.2. 方法 now

被定义为:

method now(:$timezone = $*TZ, :&formatter --> DateTime:D)

从当前系统时间创建一个新的 DateTime 对象。可以提供一个自定义的格式时区:$timezoneGMT 的偏移量,以秒为单位,默认为 $*TZ 变量的值。

say DateTime.now; # OUTPUT: «2018-01-08T13:05:32.703292-06:00␤»

需要注意的是,我们可以使用下面显示的与 .now 链接的方法来方便地表达当前的值,例如:

say DateTime.now.year; # OUTPUT: «2018␤»

81.1.3. 方法 clone

被定义为:

method clone(:$year, :$month, :$day, :$hour, :$minute, :$second, :$timezone, :&formatter)

基于调用者创建一个新的 DateTime 对象,但给定的参数会覆盖调用者的值。

say DateTime.new('2015-12-24T12:23:00Z').clone(hour => 0);
# OUTPUT: «2015-12-24T00:23:00Z␤»

请注意,这在某些情况下可能会导致无效的日期:

say DateTime.new("2012-02-29T12:34:56Z").clone(year => 2015);
CATCH { default { put .^name, ': ', .Str } };
# OUTPUT: «X::OutOfRange: Day out of range. Is: 29, should be in 1..28␤»

81.1.4. 方法 hh-mm-ss

被定义为:

method hh-mm-ss(DateTime:D: --> Str:D)

返回对象以24小时 HH:MM:SS 格式的字符串表示的时间。

say DateTime.new("2052-02-29T22:34:56Z").hh-mm-ss;
# OUTPUT: «22:34:56␤»

81.1.5. 方法 hour

被定义为:

method hour(DateTime:D: --> Int:D)

返回小时分量。

say DateTime.new('2012-02-29T12:34:56Z').hour;      # OUTPUT: «12␤»

81.1.6. 方法 minute

被定义为:

method minute(DateTime:D: --> Int:D)

返回分钟分量。

say DateTime.new('2012-02-29T12:34:56Z').minute;     # OUTPUT: «34␤»

81.1.7. 方法 second

被定义为:

method second(DateTime:D:)

返回秒数分量,包括可能的小数秒。

say DateTime.new('2012-02-29T12:34:56Z').second;     # OUTPUT: «56␤»
say DateTime.new('2012-02-29T12:34:56.789Z').second; # OUTPUT: «56.789␤»
say DateTime.new('2012-02-29T12:34:56,789Z').second; # comma also ok

81.1.8. 方法 whole-second

被定义为:

method whole-second(DateTime:D:)

返回秒数分量,四舍五入至 Int

say DateTime.new('2012-02-29T12:34:56.789Z').whole-second;      # OUTPUT: «56␤»

81.1.9. 方法 timezone

被定义为:

method timezone(DateTime:D: --> Int:D)

以秒为单位返回与 UTC 偏移的时区。

say DateTime.new('2015-12-24T12:23:00+0200').timezone;          # OUTPUT: «7200␤»

81.1.10. 方法 offset

被定义为:

method offset(DateTime:D: --> Int:D)

以秒为单位返回与 UTC 偏移的时区。这是方法 timezone 的别称。

say DateTime.new('2015-12-24T12:23:00+0200').offset;            # OUTPUT: «7200␤»

81.1.11. 方法 offset-in-minutes

被定义为:

method offset-in-minutes(DateTime:D: --> Real:D)

以分钟为单位返回与 UTC 偏移的时区。

say DateTime.new('2015-12-24T12:23:00+0200').offset-in-minutes; # OUTPUT: «120␤»

81.1.12. 方法 offset-in-hours

被定义为:

method offset-in-hours(DateTime:D: --> Real:D)

返回以小时为单位的时区与 UTC 的偏移量。

say DateTime.new('2015-12-24T12:23:00+0200').offset-in-hours;   # OUTPUT: «2␤»

81.1.13. 方法 Str

被定义为:

method Str(DateTime:D: --> Str:D)

返回由 formatter 完成的调用者的字符串表示。如果没有指定 formatter,则返回 ISO 8601 时间戳。

say DateTime.new('2015-12-24T12:23:00+0200').Str;
# OUTPUT: «2015-12-24T12:23:00+02:00␤»

81.1.14. 方法 Instant

被定义为:

method Instant(DateTime:D: --> Instant:D)

返回一个基于调用者的 Instant 对象。

say DateTime.new('2015-12-24T12:23:00+0200').Instant; # OUTPUT: «Instant:1450952616␤»

81.1.15. 方法 posix

被定义为:

method posix(Bool:D: $ignore-timezone = False --> Int:D)

返回 POSIX/UNIX 时间戳的日期和时间(自1970年1月1日UTC纪元以来的秒数)。

如果 $ignore-timezoneTrueDateTime 对象将被视为时区偏移量为零。

say DateTime.new('2015-12-24T12:23:00Z').posix;       # OUTPUT: «1450959780␤»

81.1.16. 方法 later

被定义为:

method later(DateTime:D: *%unit)

返回一个基于当前对象的 DateTime 对象,但应用了时间 delta。时间 delta 可以作为一个命名参数传递,参数名是单位。

除非给定的单位是秒或秒,否则给定的值将被转换为一个 Int

允许的单位是 secondsecondsminuteminuteshourhoursdaydaysweekweeksmonthmonthsyearyears。请注意,复数形式只能用于 laterearlier 方法。

冒号对的 :2nd 形式可以作为一种紧凑的和自我记录的方式来指定 delta。

say DateTime.new('2015-12-24T12:23:00Z').later(:2years);
# OUTPUT: «2017-12-24T12:23:00Z␤»

由于几个不同时间单位的加法不具有换算性,所以只能传递一个单位。

my $d = DateTime.new(date => Date.new('2015-02-27'));
say $d.later(month => 1).later(:2days);  # OUTPUT: «2015-03-29T00:00:00Z␤»
say $d.later(days => 2).later(:1month);  # OUTPUT: «2015-04-01T00:00:00Z␤»
say $d.later(days => 2).later(:month);   # same, as +True === 1

如果结果时间的秒值为 60,但实际上该时间并不存在闰秒,则秒将被设置为 59

say DateTime.new('2008-12-31T23:59:60Z').later: :1day;
# OUTPUT: «2009-01-01T23:59:59Z␤»

负数偏移是允许的,不过方法 earlier 更符合习惯。

81.1.17. 方法 earlier

被定义为:

method earlier(DateTime:D: *%unit)

返回一个基于当前时间的 DateTime 对象,但应用了一个朝向过去的时间 delta。除非给定的单位是秒,否则给定的值将被转换为一个 Int。使用方法请参见方法 later

my $d = DateTime.new(date => Date.new('2015-02-27'));
say $d.earlier(month => 1).earlier(:2days);  # OUTPUT: «2015-01-25T00:00:00Z␤»

如果结果时间的秒值为 60,但实际上该时间并不存在闰秒,则秒将被设置为 59

say DateTime.new('2008-12-31T23:59:60Z').earlier: :1day;
# OUTPUT: «2008-12-30T23:59:59Z␤»

负数偏移是允许的,不过方法 later 更符合习惯。

81.1.18. 方法 truncated-to

被定义为:

method truncated-to(DateTime:D: Cool $unit)

返回调用者的副本,所有小于指定单位的内容都被截断为尽可能小的值。

my $d = DateTime.new("2012-02-29T12:34:56.946314Z");
say $d.truncated-to('second');      # OUTPUT: «2012-02-29T12:34:56Z␤»
say $d.truncated-to('minute');      # OUTPUT: «2012-02-29T12:34:00Z␤»
say $d.truncated-to('hour');        # OUTPUT: «2012-02-29T12:00:00Z␤»
say $d.truncated-to('day');         # OUTPUT: «2012-02-29T00:00:00Z␤»
say $d.truncated-to('month');       # OUTPUT: «2012-02-01T00:00:00Z␤»
say $d.truncated-to('year');        # OUTPUT: «2012-01-01T00:00:00Z␤»

使用 .truncated-to('second') 可以将小数秒的 DateTime 截断为整数秒。

81.1.19. 方法 Date

被定义为:

multi method Date(DateTime:U --> Date:U)
multi method Date(DateTime:D --> Date:D)

将调用者转换为 Date

say DateTime.new("2012-02-29T12:34:56.946314Z").Date; # OUTPUT: «2012-02-29␤»
say DateTime.Date;                                    # OUTPUT: «(Date)␤»

81.1.20. 方法 DateTime

被定义为:

method DateTime(--> DateTime)

返回调用者。

say DateTime.new("2012-02-29T12:34:56.946314Z").DateTime;
# OUTPUT: «2012-02-29T12:34:56.946314Z␤»
say DateTime.DateTime;
# OUTPUT: «(DateTime)␤»

81.1.21. 方法 utc

被定义为:

method utc(DateTime:D: --> DateTime:D)

返回同一时间的 DateTime 对象,但使用 UTC 时区。

say DateTime.new('2015-12-24T12:23:00+0200').utc;
# OUTPUT: «2015-12-24T10:23:00Z␤»

81.1.22. 方法 in-timezone

被定义为:

method in-timezone(DateTime:D: Int(Cool) $timezone = 0 --> DateTime:D)

返回同一时间的 DateTime 对象,但在指定的 $timezone 中,即与 GMT 的偏移量(秒)。

say DateTime.new('2015-12-24T12:23:00Z').in-timezone(3600 + 1800); # OUTPUT: «2015-12-24T13:53:00+0130␤»

根据 RFC 7164,闰秒不遵守当地时间,总是发生在 UTC 日的末尾。

say DateTime.new: '2017-01-01T00:59:60+01:00'
# OUTPUT: «2017-01-01T00:59:60+01:00␤»

81.1.23. 方法 local

被定义为:

method local(DateTime:D: --> DateTime:D)

返回同一时间的 DateTime 对象,但以当地时区($*TZ)。

my $*TZ = -3600;
say DateTime.new('2015-12-24T12:23:00+0200').local; # OUTPUT: «2015-12-24T09:23:00-0100␤»

81.1.24. 子例程 infix:<→

multi sub infix:<-> (DateTime:D, Duration:D --> DateTime:D)
multi sub infix:<-> (DateTime:D, DateTime:D --> Duration:D)

取一个 DateTime 减去一个 Duration 或另一个 DateTime 对象。分别返回一个新的 DateTime 对象或两个日期之间的 Duration。当减去 Duration 时,在返回的 DateTime 对象中保留了原始 DateTime 的时区。

say perl DateTime.new(:2016year) - DateTime.new(:2015year):;
# OUTPUT: «Duration.new(31536001.0)␤»
say DateTime.new(:2016year, :3600timezone) - Duration.new(31536001.0);
# OUTPUT: «2015-01-01T00:00:00+01:00␤»

81.1.25. 子例程 infix:<+>

multi sub infix:<+> (DateTime:D, Duration:D --> DateTime:D)
multi sub infix:<+> (Duration:D, DateTime:D --> DateTime:D)

接收一个 DateTime 并加上给定的 Duration,保留时区。

say DateTime.new(:2015year) + Duration.new(31536001.0);
# OUTPUT: «2016-01-01T00:00:00Z␤»
say Duration.new(42) + DateTime.new(:2015year, :3600timezone);
# OUTPUT: «2015-01-01T00:00:42+01:00␤»

81.1.26. 子例程 infix:«<⇒»

multi sub infix:«<=>»(DateTime:D \a, DateTime:D \b --> Order:D)

比较等值的瞬间, 返回 Order

say DateTime.now <=> DateTime.now; # OUTPUT: «Less␤»

81.1.27. 子例程 infix:<cmp>

multi sub infix:<cmp>(DateTime:D \a, DateTime:D \b --> Order:D)

比较等值的瞬间, 返回 Order

81.1.28. 子例程 infix:«<»

multi sub infix:«<»(DateTime:D \a, DateTime:D \b --> Bool:D)

比较等值的瞬间, 返回 Bool

81.1.29. 子例程 infix:«>»

multi sub infix:«>»(DateTime:D \a, DateTime:D \b --> Bool:D)

比较等值的瞬间, 返回 Bool

81.1.30. 子例程 infix:«⇐»

multi sub infix:«<=»(DateTime:D \a, DateTime:D \b --> Bool:D)

比较等值的瞬间, 返回 Bool

81.1.31. 子例程 infix:«>=»

multi sub infix:«>=»(DateTime:D \a, DateTime:D \b --> Bool:D)

比较等值的瞬间, 返回 Bool

81.1.32. 子例程 infix:«==»

multi sub infix:«==»(DateTime:D \a, DateTime:D \b --> Bool:D)

比较等值的瞬间, 返回 Bool

81.1.33. 子例程 infix:«!=»

multi sub infix:«!=»(DateTime:D \a, DateTime:D \b --> Bool:D)

比较等值的瞬间, 返回 Bool

81.2. 类型图

DateTime 的类型关系

DateTime

81.3. 角色 Dateish 提供的例程

81.3.1. 方法 year

被定义为:

method year(Date:D: --> Int:D)

返回日期的年份。

say Date.new('2015-12-31').year;                                  # OUTPUT: «2015␤»
say DateTime.new(date => Date.new('2015-12-24'), hour => 1).year; # OUTPUT: «2015␤»

81.3.2. 方法 month

被定义为:

method month(Date:D: --> Int:D)

返回日期的月份(1..12).

say Date.new('2015-12-31').month;                                  # OUTPUT: «12␤»
say DateTime.new(date => Date.new('2015-12-24'), hour => 1).month; # OUTPUT: «12␤»

81.3.3. 方法 day

被定义为:

method day(Date:D: --> Int:D)

返回日期的月日(1…​31)。

say Date.new('2015-12-31').day;                                  # OUTPUT: «31␤»
say DateTime.new(date => Date.new('2015-12-24'), hour => 1).day; # OUTPUT: «24␤»

81.3.4. 方法 formatter

被定义为:

method formatter(Dateish:D:)

返回用于转换为 Str 的格式化函数。如果在构建对象时没有提供任何格式化函数,则使用一个默认的格式化函数。在这种情况下,该方法将返回一个 Callable 类型的对象。

格式化函数由 DateTime 方法 Str 调用,调用者为其唯一参数。

my $dt = Date.new('2015-12-31');  # (no formatter specified)
say $dt.formatter.^name;          # OUTPUT: «Callable␤»
my $us-format = sub ($self) { sprintf "%02d/%02d/%04d", .month, .day, .year given $self; };
$dt = Date.new('2015-12-31', formatter => $us-format);
say $dt.formatter.^name;           # OUTPUT: «Sub␤»
say $dt;                          # OUTPUT: «12/31/2015␤»

81.3.5. 方法 is-leap-year

被定义为:

method is-leap-year(--> Bool:D)

如果 Dateish 对象的年份是闰年,则返回 True

say DateTime.new(:year<2016>).is-leap-year; # OUTPUT: «True␤»
say Date.new("1900-01-01").is-leap-year;    # OUTPUT: «False␤»

81.3.6. 方法 day-of-month

被定义为:

method day-of-month(Date:D: --> Int:D)

返回日期的月日(1..31)。与 day 方法同义。

say Date.new('2015-12-31').day-of-month;                                  # OUTPUT: «31␤»
say DateTime.new(date => Date.new('2015-12-24'), hour => 1).day-of-month; # OUTPUT: «24␤»

81.3.7. 方法 day-of-week

被定义为:

method day-of-week(Date:D: --> Int:D)

返回一周中的第几天,其中1为周一,2为周二,周日为7。

say Date.new('2015-12-31').day-of-week;                                  # OUTPUT: «4␤»
say DateTime.new(date => Date.new('2015-12-24'), hour => 1).day-of-week; # OUTPUT: «4␤»

81.3.8. 方法 day-of-year

被定义为:

method day-of-year(Date:D: --> Int:D)

返回一年中的第几天(1..366)。

say Date.new('2015-12-31').day-of-year;                                  # OUTPUT: «365␤»
say DateTime.new(date => Date.new('2015-03-24'), hour => 1).day-of-year; # OUTPUT: «83␤»

81.3.9. 方法 days-in-month

被定义为:

method days-in-month(Dateish:D: --> Int:D)

返回 Dateish 对象所代表的月份的天数。

say Date.new("2016-01-02").days-in-month;                # OUTPUT: «31␤»
say DateTime.new(:year<10000>, :month<2>).days-in-month; # OUTPUT: «29␤»

81.3.10. 方法 week

被定义为:

method week()

返回两个整数的列表:年份和星期数。这是因为在一年的开始或结束时,周数可能属于另一年。

my ($year, $week) = Date.new("2014-12-31").week;
say $year;                       # OUTPUT: «2015␤»
say $week;                       # OUTPUT: «1␤»
say Date.new('2015-01-31').week; # OUTPUT: «(2015 5)␤»

81.3.11. 方法 week-number

被定义为:

method week-number(Date:D: --> Int:D)

返回调用者指定日期的周数(1..53)。ISO 将一年的第一周定义为包含一月第四天的一周。因此,1月初的日期往往被放在上一年的最后一周,同样,12月的最后几天也可能被放在下一年的第一周。

say Date.new("2014-12-31").week-number;   # 1  (first week of 2015)
say Date.new("2016-01-02").week-number;   # 53 (last week of 2015)

81.3.12. 方法 week-year

被定义为:

method week-year(Date:D: --> Int:D)

返回调用者指定日期的星期年。通常 week-year 等于 Date.year。但是请注意,一月初的日期通常会被放在上一年的最后一周,同样,十二月的最后几天也可能被放在下一年的第一周。

say Date.new("2015-11-15").week-year;   # 2015
say Date.new("2014-12-31").week-year;   # 2015 (date belongs to the first week of 2015)
say Date.new("2016-01-02").week-year;   # 2015 (date belongs to the last week of 2015)

81.3.13. 方法 weekday-of-month

被定义为:

method weekday-of-month(Date:D: --> Int:D)

返回一个数字(1..5),表示当月某一周的某一天到目前为止发生的次数,包括这一天本身。

say Date.new("2003-06-09").weekday-of-month;  # 2  (second Monday of the month)

81.3.14. 方法 yyyy-mm-dd

被定义为:

method yyyy-mm-dd(str $sep = "-" --> Str:D)

返回 YYYY-MM-DD 格式的日期(ISO 8601)。可选的位置参数 $sep,默认为 -,是日期不同部分之间的一个字符分隔符。

say Date.new("2015-11-15").yyyy-mm-dd;   # OUTPUT: «2015-11-15␤»
say DateTime.new(1470853583).yyyy-mm-dd; # OUTPUT: «2016-08-10␤»
say Date.today.yyyy-mm-dd("/");          # OUTPUT: «2020/03/14␤»

81.3.15. 方法 mm-dd-yyyy

被定义为:

method mm-dd-yyyy(str $sep = "-" --> Str:D)

返回 MM-DD-YYYY 格式的日期(ISO 8601)。可选的位置参数 $sep,默认为 -,是日期不同部分之间的一个字符分隔符。

say Date.new("2015-11-15").mm-dd-yyyy;   # OUTPUT: «11-15-2015␤»
say DateTime.new(1470853583).mm-dd-yyyy; # OUTPUT: «08-10-2016␤»
say Date.today.mm-dd-yyyy("/");          # OUTPUT: «03/14/2020␤»

81.3.16. 方法 dd-mm-yyyy

被定义为:

method dd-mm-yyyy(str $sep = "-" --> Str:D)

返回 DD-MM-YYYY 格式的日期(ISO 8601)。可选的位置参数 $sep,默认为 -,是日期不同部分之间的一个字符分隔符。

say Date.new("2015-11-15").dd-mm-yyyy;    # OUTPUT: «15-11-2015␤»
say DateTime.new(1470853583).dd-mm-yyyy;  # OUTPUT: «10-08-2016␤»
say Date.today.dd-mm-yyyy("/");           # OUTPUT: «14/03/2020␤»

81.3.17. 方法 daycount

被定义为:

method daycount(Dateish:D: --> Int:D)

返回从1858年11月17日到调用者的日子的天数。本方法返回的日数是 MJD,即修正的朱利安日,它被天文学家、大地测量学家、科学家和其他人士经常使用。MJD 约定是为了方便简化年表计算。

say Date.new('1995-09-27').daycount;    # OUTPUT: «49987␤»

81.3.18. 方法 IO

被定义为:

method IO(Dateish:D: --> IO::Path:D)

返回一个 IO::Path 对象,表示 Dateish 对象的字符串化值。

Date.today.IO.say;   # OUTPUT: «"2016-10-03".IO␤»
DateTime.now.IO.say; # OUTPUT: «"2016-10-03T11:14:47.977994-04:00".IO␤»

移植注意: 某些操作系统(如 Windows)不允许在文件名中使用冒号(:),而在从 DateTime 对象创建的 IO::Path 中会有冒号。

82. 类 HyperWhatever

Placeholder for multiple unspecified values/arguments

class HyperWhatever { }

HyperWhatever 在功能上与 Whatever 非常相似。不同之处在于 HyperWhatever 代表的是多个值,而不是单一的值。

82.1. 独立术语

就像用 Whatever 一样,如果一个 HyperWhatever 单独作为一个项使用,则不会进行任何柯里化,HyperWhatever 对象将按原样使用。

sub foo ($arg) { say $arg.^name }
foo **; # OUTPUT: «HyperWhatever␤»

你可以选择将这样的值解释为代表你自己例程中的多个值。在 core 中,当与 List 进行智能匹配时,HyperWhatever 可以使用这种含义。

say (1, 8)                ~~ (1, **, 8); # OUTPUT: «True␤»
say (1, 2, 4, 5, 6, 7, 8) ~~ (1, **, 8); # OUTPUT: «True␤»
say (1, 2, 8, 9)          ~~ (1, **, 8); # OUTPUT: «False␤»

只要右侧列表中出现一个 HyperWhatever,就意味着任何数量的元素都可以填满列表中被智能匹配的那个空格。

82.2. 柯里化

在柯里化方面,HyperWhateverWhatever 遵循相同的规则。唯一不同的是 HyperWhatever 会产生一个以 *@ slurpy 为签名的 Callable

say (**²)(1, 2, 3, 4, 5); # OUTPUT: «(1 4 9 16 25)␤»

HyperWhatever 闭包可以想象成一个 Whatever 闭包,里面包裹着另一个子例程,只是将参数中的每个元素映射过来。

my &hyper-whatever = sub (*@args) { map *², @args }
say hyper-whatever(1, 2, 3, 4, 5); # OUTPUT: «(1 4 9 16 25)␤»

柯里化时,不允许将 HyperWhateverWhatever 混合。

82.3. 类型图

HyperWhatever 的类型关系

HyperWhatever

83. 角色 Iterable

role Iterable { }

Iterable 是一个 API,用于处理可以使用 for 和相关迭代结构(如赋值给 Positional 变量)进行迭代的对象。

嵌套在其他 Iterable 对象中的 Iterable 对象(但不是在标量容器内)在某些情况下会扁平化,例如当传递给一个 slurpy 参数(*@a)时,或者在显式调用 flat 时。

它最重要的方面是 iterator 的方法存根。

class DNA does Iterable {
    has $.chain;
    method new ($chain where { $chain ~~ /^^ <[ACGT]>+ $$ / } ) {
        self.bless( :$chain );
    }

    method iterator(DNA:D:) {
        $!chain.comb.rotor(3).iterator;
    }
}

my $a := DNA.new('GAATCC');
.say for $a; # OUTPUT: «(G A A)␤(T C C)␤»

这个例子混合了 Iterable 角色,提供了一种新的方式来迭代本质上是一个字符串(由 where 约束为只有四个DNA字母)。在最后一条语句中,for 实际上钩住了迭代器角色,以3个字母为一组打印字母。

83.1. 方法

83.1.1. 方法 iterator

被定义为:

method iterator(--> Iterator:D)

方法存根,确保所有遵循 Iterable 角色的类都有一个 iterator 方法。

它应该返回一个迭代器

say (1..10).iterator;

83.1.2. 方法 flat

被定义为:

method flat(--> Iterable)

返回另一个 Iterable,将第一个 Iterable 返回的所有迭代物展平。

例如:

say (<a b>, 'c').elems;         # OUTPUT: «2␤»
say (<a b>, 'c').flat.elems;    # OUTPUT: «3␤»

因为 <a b> 是一個 List,因此可以迭代,所以 (<a b>, 'c').flat 返回 ('a', 'b', 'c'),它有三个元素。

注意扁平化是递归的,所以 ((("a", "b"), "c"), "d").flat 返回 ("a", "b", "c", "d"),但它不扁平化分项化的子列表:

say ($('a', 'b'), 'c').raku;    # OUTPUT: «($("a", "b"), "c")␤»

你可以使用 hyper 方法调用 .List 方法,对所有内部的逐项子列表进行调用,从而去容器化,这样 flat 就可以将其扁平化。

say ($('a', 'b'), 'c')>>.List.flat.elems;    # OUTPUT: «3␤»

83.1.3. 方法 lazy

被定义为:

method lazy(--> Iterable)

返回一个包装调用者的懒惰迭代。

say (1 ... 1000).is-lazy;      # OUTPUT: «False␤»
say (1 ... 1000).lazy.is-lazy; # OUTPUT: «True␤»

83.1.4. 方法 hyper

被定义为:

method hyper(Int(Cool) :$batch = 64, Int(Cool) :$degree = 4)

返回另一个可能并行迭代的 Iterable,给定批次大小和并行度。

保留元素的顺序。

say ([1..100].hyper.map({ $_ +1 }).list);

在可以并行处理项的情况下,使用 hyper,输出顺序应保持与输入顺序相对。在并行处理项目且输出顺序不重要的情况下,请参见 race

Options degree and batch

degree 选项("degree of parallelism"(并行度)的缩写)可以配置应该启动多少个并行 worker。要启动4个 worker(例如最多使用4个内核),请将 :4degree 传递给 hyperrace 方法。请注意,在某些情况下,选择比可用的 CPU 核数更高的度数是有意义的,例如 I/O 约束的工作或延迟较高的任务,如网络抓取。但对于CPU绑定的工作,选择高于CPU核数的数字是没有意义的。

batch 大小选项配置了一次发送至给定并行 worker 的项的数量。它允许进行吞吐量/延迟的权衡。例如,如果一个操作每个项都是长期运行的,而你需要尽快得到第一个结果,就把它设置为1,这意味着每个并行 worker 每次得到1个项来处理,并尽快报告结果。这样一来,线程间通信的开销就被最大化了。在另一个极端情况下,如果你有1000个项要处理,10个worker,给每个worker一批100个项,你会因为调度项而产生最小的开销,但只有当100个项被最快的worker处理完时(或者,对于 hyper 来说,当得到第一批项的 worker 返回时,你才会得到第一批结果。)另外,如果不是所有的项都需要同样的时间来处理,你可能会遇到这样的情况:有些 worker 已经处理完了,坐在这里却无法帮助处理剩余的工作。在不是所有的项都需要同样的时间来处理,而你又不想有太多的线程间通信开销的情况下,选择一个介于中间的数字是有意义的。你的目的可能是让所有的 worker 都均匀地忙碌起来,以便最好地利用可用资源。

你也可以看看这篇关于 hyper 和 race 的语义的博文

83.1.5. 方法 race

被定义为:

method race(Int(Cool) :$batch = 64, Int(Cool) :$degree = 4 --> Iterable)

返回另一个可能并行迭代的 Iterable,给定的批次大小和并行度(并行 worker 的数量)。

hyper 不同,race 不保留元素的顺序。

say ([1..100].race.map({ $_ +1 }).list);

在可以并行处理项的情况下使用 race,输出顺序并不重要。如果你希望并行处理项,并且输出顺序应该与输入顺序相对应,请参见 hyper

关于 :$batch:$degree 的解释请参见 hyper

83.2. 类型图

Iterable 的类型关系

type-graph-Iterable

84. 角色 Iterator

constant IterationEnd
role Iterator { }

Iterator 是一个可以生成或提供序列元素的对象。用户通常不必关心迭代器,它们的用法隐藏在迭代 API 后面,如 for @list { }mapgrepheadtailskip 和用 .[$idx] 进行列表索引。

主要的 API 是 pull-one 方法,它要么返回下一个值,要么返回哨兵值 IterationEnd,如果没有更多的元素可用。每个实现 Iterator 的类都必须提供一个 pull-one 方法。所有其他非可选的 Iterator API 方法都是以 pull-one 的方式实现的,但也可以被消费类出于性能或其他原因而重写。还有一些可选的 Iterator API 方法只有在消费类实现的情况下才会被调用:这些方法不是由 Iterator 角色实现的。

84.1. IterationEnd

迭代器只允许在整个序列上进行一次迭代。一旦产生了 IterationEnd,就禁止进行获取更多数据的尝试,而且这样做的行为是未定义的。例如,下面的 Seq 在正常使用下不会导致 die 被调用,因为在它返回 IterationEnd 之后,pull-one 永远不会被调用:

class SkippingArray is Array {
    # skip all undefined values while iterating
    method iterator {
        class :: does Iterator {
            has $.index is rw = 0;
            has $.array is required;
            method pull-one {
                $.index++ while !$.array.AT-POS($.index).defined && $.array.elems > $.index;
                $.array.elems > $.index ?? $.array.AT-POS($.index++) !! IterationEnd
            }
        }.new(array => self)
    }
}

my @a := SkippingArray.new;

@a.append: 1, Any, 3, Int, 5, Mu, 7;

for @a -> $a, $b {
    say [$a, $b];
}

# OUTPUT: «[1 3]␤[5 7]␤»

在程序中,哨兵值 IterationEnd 的唯一有效用途是与迭代器 API 中一个方法的结果进行同一性比较(使用 =:=)。任何其他行为都是未定义的,并且依赖于实现。

请记住,IterationEnd 是一个常量,所以如果你要将它与一个变量的值进行比较,这个变量必须被绑定,而不是赋值。直接与 pull-one 的输出进行比较就可以了。

my $it = (1,2).iterator;
$it.pull-one for ^2;
say $it.pull-one =:= IterationEnd; # OUTPUT: «True␤»

然而,如果我们使用一个变量, 我们分配它,结果将是错误的。

my $it = (1,2).iterator;
$it.pull-one for ^2;
my $is-it-the-end = $it.pull-one;
say $is-it-the-end =:= IterationEnd; # OUTPUT: «False␤»

所以我们要绑定变量才行:

my $is-it-the-end := $it.pull-one;
say $is-it-the-end =:= IterationEnd; # OUTPUT: «True␤»

84.2. 方法

84.2.1. 方法 pull-one

被定义为:

method pull-one(Iterator:D: --> Mu)

这个方法存根确保实现 Iterator 角色的类提供一个名为 pull-one 的方法。

如果可能的话,pull-one 方法应该产生并返回下一个值,如果不能产生更多的值,则返回哨兵值 IterationEnd

my $i = (1 .. 3).iterator;
say $i.pull-one;       # OUTPUT: «1␤»
say $i.pull-one;       # OUTPUT: «2␤»
say $i.pull-one;       # OUTPUT: «3␤»
say $i.pull-one.raku;  # OUTPUT: «IterationEnd␤»

作为其使用的一个更有说服力的例子,这里是一个倒数迭代器以及 for 循环的一个简单子程序的重新实现。

# works the same as (10 ... 1, 'lift off')
class CountDown does Iterator {
    has Int:D $!current = 10;

    method pull-one ( --> Mu ) {
        my $result = $!current--;
        if $result ==  0 { return 'lift off' }
        if $result == -1 { return IterationEnd }

        # calling .pull-one again after it returns IterationEnd is undefined
        if $result <= -2 {
            # so for fun we will give them nonsense data
            return (1..10).pick;
        }

        return $result;
    }
}

sub for( Iterable:D $sequence, &do --> Nil ) {
    my Iterator:D $iterator = $sequence.iterator;

    loop {
        # must bind the result so that =:= works
        my Mu $pulled := $iterator.pull-one;

        # always check the result and make sure that .pull-one
        # is not called again after it returns IterationEnd
        if $pulled =:= IterationEnd { last }

        do( $pulled );
    }
}

for( Seq.new(CountDown.new), &say );  # OUTPUT: «10␤9␤8␤7␤6␤5␤4␤3␤2␤1␤lift off␤»

如果用 whileuntil,以及一个无符号的变量,会更习惯。

until IterationEnd =:= (my \pulled = $iterator.pull-one) {
    do( pulled );
}

84.2.2. 方法 push-exactly

被定义为:

method push-exactly(Iterator:D: $target, int $count --> Mu)

应该产生 $count 元素,对于每个元素,调用 $target.push($value)

如果从迭代器中得到的元素少于 $count,它应该返回哨兵值 IterationEnd。否则它应该返回 $count

my @array;
say (1 .. ∞).iterator.push-exactly(@array, 3); # OUTPUT: «3␤»
say @array; # OUTPUT: «[1 2 3]␤»

迭代器角色以 pull-one 的方式实现这个方法。一般来说,这是一个不打算从最终用户直接调用的方法,而应该在混合迭代器角色的类中实现它。例如,这个类就实现了该角色:

class DNA does Iterable does Iterator {
    has $.chain;
    has Int $!index = 0;

    method new ($chain where {
                       $chain ~~ /^^ <[ACGT]>+ $$ / and
                       $chain.chars %% 3 } ) {
        self.bless( :$chain );
    }

    method iterator( ){ self }

    method pull-one( --> Mu){
      if $!index < $.chain.chars {
         my $codon = $.chain.comb.rotor(3)[$!index div 3];
         $!index += 3;
         return $codon;
      } else {
        return IterationEnd;
      }
    }

    method push-exactly(Iterator:D: $target, int $count --> Mu) {
        return IterationEnd if $.chain.elems / 3 < $count;
        for ^($count) {
            $target.push: $.chain.comb.rotor(3)[ $_ ];
        }
    }

};

my $b := DNA.new("AAGCCT");
for $b -> $a, $b, $c { say "Never mind" }; # Does not enter the loop
my $þor := DNA.new("CAGCGGAAGCCT");
for $þor -> $first, $second {
    say "Coupled codons: $first, $second";
    # OUTPUT: «Coupled codons: C A G, C G G␤Coupled codons: A A G, C C T␤»
}

这段代码将 DNA 链以三联体(通常称为密码子)分组,当在循环中请求时,返回这些密码子;如果请求的密码子太多,比如第一种情况下的 for $b → $a, $b, $c,它就干脆不进入循环,因为 push-exactly 将返回 IterationEnd,因为它无法满足正好3个密码子的请求。然而,在第二种情况下,它在循环的每次迭代中正好请求两个密码子;push-exactly 是以循环变量的数量作为 $count 变量被调用的。

84.2.3. 方法 push-at-least

被定义为:

method push-at-least(Iterator:D: $target, int $count --> Mu)

应该至少产生 $count 个元素,对于每个元素,调用 $target.push($value)

如果从迭代器中得到的元素少于 $count,它应该返回哨兵值 IterationEnd。否则它应该返回 $count

有副作用的迭代器应该准确地产生 $count 个元素;没有副作用的迭代器(如 Range 迭代器)可以产生更多的元素以获得更好的性能。

my @array;
say (1 .. ∞).iterator.push-at-least(@array, 10); # OUTPUT: «10␤»
say @array; # OUTPUT: «[1 2 3 4 5 6 7 8 9 10]␤»

Iterator 角色以 pull-one 的方式实现这个方法。一般来说,也不打算像上面的例子那样直接调用它。如果对这个默认实现不满意,可以由使用这个角色的人实现。具体实现方式请参考 push-exactly 的文档

84.2.4. 方法 push-all

被定义为:

method push-all(Iterator:D: $target)

应该从迭代器中产生所有元素,并将它们推送到 $target 中。

my @array;
say (1 .. 1000).iterator.push-all(@array); # All 1000 values are pushed

Iterator 角色用 push-at-least 的方式实现了这个方法。和其他 push-* 方法一样,它主要是为实现这个角色的开发人员准备的。比如说,当把具有这个角色的对象分配给一个数组时,就会调用 push-all,就像在这个例子中一样:

class DNA does Iterable does Iterator {
    has $.chain;
    has Int $!index = 0;

    method new ($chain where {
                       $chain ~~ /^^ <[ACGT]>+ $$ / and
                       $chain.chars %% 3 } ) {
        self.bless( :$chain );
    }

    method iterator( ){ self }
    method pull-one( --> Mu){
      if $!index < $.chain.chars {
         my $codon = $.chain.comb.rotor(3)[$!index div 3];
         $!index += 3;
         return $codon;
      } else {
        return IterationEnd;
      }
    }

    method push-all(Iterator:D: $target) {
        for $.chain.comb.rotor(3) -> $codon {
            $target.push: $codon;
        }
    }

};

my $b := DNA.new("AAGCCT");
my @dna-array = $b;
say @dna-array; # OUTPUT: «[(A A G) (C C T)]␤»

实现的 push-all 方法以三个氨基酸表示的列表推送到目标迭代器;当我们将 $b 赋值给 @dna-array 时,这个方法在底层被调用。

84.2.5. 方法 push-until-lazy

被定义为:

method push-until-lazy(Iterator:D: $target --> Mu)

应该产生值,直到它认为自己是懒惰的,并把它们推送到 $target 上。

如果 is-lazy 返回一个 True 值,迭代器角色就把这个方法作为一个 no-op 实现,如果不是,则作为 push-all 的同义词实现。

这主要是对那些嵌入了其他迭代器的迭代器而言,有些迭代器可能是懒惰的,而其他迭代器则不是。

84.2.6. 方法 is-lazy

被定义为:

method is-lazy(Iterator:D: --> Bool:D)

对于认为自己是懒惰的迭代器应该返回 True,否则返回 False

知道自己可以产生无限多值的内置操作在这里返回 True,例如 (1..6).roll(*)

say (1 .. 100).is-lazy; # OUTPUT: «False␤»
say (1 .. ∞).is-lazy; # OUTPUT: «True␤»

Iterator 角色实现了这个方法,返回 False,表示一个非惰性迭代器。

84.2.7. 方法 sink-all

被定义为:

method sink-all(Iterator:D: --> IterationEnd)

应该纯粹为了产生值的副作用而耗尽迭代器,而不以任何方式实际保存它们。应该总是返回 IterationEnd。如果没有与产生值相关的副作用,那么它可以被消耗类实现为一个虚拟的无操作(no-op)。

say (1 .. 1000).iterator.sink-all;  # OUTPUT: «IterationEnd␤»

Iterator 角色将这个方法实现为一个循环,调用 pull-one,直到用完为止。

84.2.8. 方法 skip-one

被定义为:

method skip-one(Iterator:D: $target --> Mu)

应该跳过产生一个值。如果跳过成功,返回值应该是 truthy,如果没有值要跳过,返回值应该是 falsy。

my $i = <a b>.iterator;
say $i.skip-one; say $i.pull-one; say $i.skip-one
# OUTPUT: «1␤b␤0␤»

Iterator 角色以调用 pull-one 的方式实现该方法,返回得到的值是否不是 IterationEnd

84.2.9. 方法 skip-at-least

被定义为:

method skip-at-least(Iterator:D: $target, int $to-skip --> Mu)

应该跳过产生 $to-skip 的值,如果跳过成功,返回值应该是 truthy,如果没有足够的值被跳过,返回值应该是 falsy:

my $i = <a b c>.iterator;
say $i.skip-at-least(2); say $i.pull-one; say $i.skip-at-least(20);
# OUTPUT: «1␤c␤0␤»

Iterator 角色将此方法实现为循环调用 skip-one,并返回是否返回足够次数的 truthy 值。

84.2.10. 方法 skip-at-least-pull-one

被定义为:

method skip-at-least-pull-one(Iterator:D: $target, int $to-skip --> Mu)

应该跳过产生 $to-skip 值,如果迭代器仍未用尽,产生并返回下一个值。如果迭代器在任何时候被用尽,应该返回 IterationEnd:

my $i = <a b c>.iterator;
say $i.skip-at-least-pull-one(2);
say $i.skip-at-least-pull-one(20) =:= IterationEnd;
# OUTPUT: «c␤True␤»

Iterator 角色把这个方法实现为调用 skip-at-least,如果还没有用完,再调用 pull-one

84.3. 预测性迭代器(predictive iterators)

如果你的 Iterator 可以在不实际产生值的情况下知道还能产生多少值,请看 PredictiveIterator 角色。

84.4. 类型图

迭代器的类型关系

type-graph-Iterator

85. 类 Junction

class Junction is Mu { }

Junction 是由零或多个值组成的无序复合值。Junction 在许多操作上自动线程化,这意味着对每个 junction 元素(也称为本征态)进行操作,结果是所有这些操作的返回值的 junction。

Junction 在布尔语境中折叠成一个单一的值,所以当在条件中使用时,通过 so? 前缀运算符对 Bool 进行否定或显式强制转换。这种折叠的语义取决于 junction 的类型,它可以是 all, any, onenone

type

constructor

operator

True if …​

all

all

&

没有值计算为 False

any

any

|

至少有一个值计算为 True

one

one

^

恰好有一个值计算为 True

none

none

没有值计算为 True

如表所示,为了创建 junction,你使用代表类型的字符串跟在任何对象后面,否则在对象上调用 .all.none.one

say so 3 == (1..30).one;         # OUTPUT: «True␤»
say so ("a" ^ "b" ^ "c") eq "a"; # OUTPUT: «True␤»

Junction 是非常特殊的对象,它们不属于 Any 层次,只是和其他对象一样,是 Mu 的子类。这使得大多数方法有了一个特性:自动线程化。当一个 junction 被绑定到一个不接受 junction 类型值的代码对象的参数上时,就会发生自动线程化。签名绑定不是产生一个错误,而是对 junction 的每个值重复进行。

例如:

my $j = 1|2;
if 3 == $j + 1 {
    say 'yes';
}

首先在 infix:<+> 运算符上进行自动线程化,产生 junction 2|3。下一步自动线程化在 infix:⇐⇒ 上,产生 False|Trueif 条件在布尔语境中评估 junction,将其折叠为 True。所以代码打印的是 yes\n

Junction 的类型并不影响自动线程化后产生的 Junction 中的项数。例如,在 Hash 键查找过程中使用 one Junction,仍然会得到一个有多个项目的 Junction。只有在布尔语境中,Junction 的类型才会发挥作用:

my %h = :42foo, :70bar;
say    %h{one <foo meow>}:exists; # OUTPUT: «one(True, False)␤»
say so %h{one <foo meow>}:exists; # OUTPUT: «True␤»
say    %h{one <foo  bar>}:exists; # OUTPUT: «one(True, True)␤»
say so %h{one <foo  bar>}:exists; # OUTPUT: «False␤»

请注意,编译器是允许但不需要并行化自动线程化(以及一般的 Junction 行为),所以在有副作用的代码上自动线程化 Junction 通常是一个错误。

自动线程化意味着被自动线程化的函数也会返回一个通常会返回的值的 Junction

(1..3).head( 2|3 ).say; # OUTPUT: «any((1 2), (1 2 3))␤»

由于 .head 返回的是一个列表,所以自动线程化的版本会返回一个列表的 Junction

(1..3).contains( 2&3 ).say; # OUTPUT: «all(True, True)␤»

同样,.contains 也会返回一个布尔值;因此,自动线程化的版本会返回一个布尔值的 junction。一般来说,所有接受 T 类型的参数并返回 TT 类型的方法和例程,也会接受 T 的 junction,返回 TT 的 junction。

允许实现对 junction 进行短路。例如下面代码中的一个或多个例程调用(a()b()c())可能根本不会被执行,如果条件的结果已经从已经执行的例程调用中完全确定(只有一个 truthy 的返回值就足以知道整个 junction 为真)。

if a() | b() | c() {
    say "At least one of the routines was called and returned a truthy value"
}

Junction 是用来作为布尔语境中的匹配器,不支持 junction 的自省。如果你觉得想要反省一个 junction,请使用 Set 或相关类型代替。

使用示例:

my @list = <1 2 "Great">;
@list.append(True).append(False);
my @bool_or_int = grep Bool|Int, @list;

sub is_prime(Int $x) returns Bool {
    # 'so' is for boolean context
    so $x %% none(2..$x.sqrt);
}
my @primes_ending_in_1 = grep &is_prime & / 1$ /, 2..100;
say @primes_ending_in_1;        # OUTPUT: «[11 31 41 61 71]␤»

my @exclude = <~ .git>;
for dir(".") { say .Str if .Str.ends-with(none @exclude) }

当使用带参数的 all 时,应特别注意可能产生一个空列表:

my @a = ();
say so all(@a) # True, because there are 0 False's

要表达 "全部, 但至少有一个",可以使用 @a && all(@a)

my @a = ();
say so @a && all(@a);   # OUTPUT: «False␤»

否定运算符在自动线程化中是特殊情况。$a !op $b 在内部被重写为 !($a op $b)。外否定符会折叠任何 junction,所以返回值总是一个纯 Bool

my $word = 'yes';
my @negations = <no none never>;
if $word !eq any @negations {
    say '"yes" is not a negation';
}

请注意,如果没有这种特殊的封装,像 $word ne any @words 这样的表达式对于单边的非平凡列表总是会计算为 True

为此,infix:<ne> 算作 infix:<eq> 的否定。

一般来说,使用正比较运算符和否定 junction 更易读。

my $word = 'yes';
my @negations = <no none never>;
if $word eq none @negations {
    say '"yes" is not a negation';
}

85.1. 失败和异常

失败就像其他的数值一样,就 junction 而言:

my $j = +any "not a number", "42", "2.1";
my @list = gather for $j -> $e {
    take $e if $e.defined;
}
@list.say; # OUTPUT: «[42 2.1]␤»

上文中,我们已经在 Junction 上使用了前缀 + 操作符来将其内部的字符串强制转换为 Numeric,由于该操作符在一个不包含数字的 Str 中被强制转换为 Numeric 时返回一个 Failure,所以 Junction 中的一个元素就是 FailureFailure 在被使用或沉没之前不会变成异常,但我们可以检查定义性来避免这种情况。这就是我们在循环中所做的,循环运行在 junction 的元素上,只有在它们被定义的情况下才会将它们添加到列表中。

如果你试图使用 Failure 作为一个值,就会抛出异常-就像这个 Failure 是独立的,而不是 Junction 的一部分一样。

my $j = +any "not a number", "42", "2.1";
try say $j == 42;
$! and say "Got exception: $!.^name()";
# OUTPUT: «Got exception: X::Str::Numeric␤»

请注意,如果在计算 Junction 中的任何一个值时发生了异常,它将会被抛出,就像有问题的值是单独计算的而不是用 Junction 计算的一样;你不能只计算有效的值而忽略异常。

sub calc ($_) { die when 13 }
my $j = any 1..42;
say try calc $j; # OUTPUT: «Nil␤»

上面只有一个值会引起异常,但 try 块的结果仍然是一个 Nil。一个可能的解决方法是作弊,单独评估 Junction 的值,然后根据结果重新创建 Junction

sub calc ($_) { die when 13 }
my $j = any 1..42;
$j = any (gather $j».take).grep: {Nil !=== try calc $_};
say so $j == 42; # OUTPUT: «True␤»

85.2. 智能匹配

请注意,在 ~~ 的右侧使用 Junction 与使用 Junctions 与其他运算符的工作方式略有不同。

请看这个例子。

say 25 == (25 | 42);    # OUTPUT: «any(True, False)␤» – Junction
say 25 ~~ (25 | 42);    # OUTPUT: «True␤»             – Bool

原因是 == (和大多数其他操作符) 是受制于自动线程化的,因此你会得到一个 Junction 作为结果。另一方面,~~ 会在右侧调用 .ACCEPTS(在这种情况下,在一个 Junction 上),结果将是一个 Bool

85.3. 方法

85.3.1. 方法 new

被定义为:

multi method new(Junction: \values, Str :$type!)
multi method new(Junction: Str:D \type, \values)

构造函数,用于从定义一个 junction 和一组值的类型中定义一个新的 junction。

my $j = Junction.new(<Þor Oðinn Loki>, type => "all");
my $n = Junction.new( "one", 1..6 )

85.3.2. 方法 defined

被定义为:

multi method defined(Junction:D:)

检查定义性,而不是布尔值。

say ( 3 | Str).defined ;   # OUTPUT: «True␤»
say (one 3, Str).defined;  # OUTPUT: «True␤»
say (none 3, Str).defined; # OUTPUT: «False␤»

失败也被认为是未定义的。

my $foo=Failure.new;
say (one 3, $foo).defined; # OUTPUT: «True␤»

从 6.d 开始,这个方法会自动线程化。

85.3.3. 方法 Bool

被定义为:

multi method Bool(Junction:D:)

折叠 junction,并根据类型和它所拥有的值返回一个单一的布尔值。每一个元素都被转换为 Bool

my $n = Junction.new( "one", 1..6 );
say $n.Bool;                         # OUTPUT: «False␤»

在这种情况下,所有的元素都转换为 True,所以断言只有其中一个元素是假的。

my $n = Junction.new( "one", <0 1> );
say $n.Bool;                         # OUTPUT: «True␤»

本例中只要有一个是 true,1,所以对 Bool 的强制转换会返回 True

85.3.4. 方法 Str

被定义为:

multi method Str(Junction:D:)

在其元素上自动线程化 .Str 方法,并以 junction 形式返回结果。使用 .Str 方法的输出方法(printput)尽管可以接受 Mu 类型,但对自动线程化 junction 来说是特例。

85.3.5. 方法 gist

被定义为:

multi method gist(Junction:D:)

折叠 junction,并返回一个由 junction 的类型和其组件的 gist 组成的 Str

<a 42 c>.all.say; # OUTPUT: «all(a, 42, c)␤»

85.3.6. 方法 raku

被定义为:

multi method raku(Junction:D:)

折叠 junction,并返回一个由其组件的 raku 组成的 Str,该 Str 的值为具有等价组件的等价 junction。

<a 42 c>.all.raku.put; # OUTPUT: «all("a", IntStr.new(42, "42"), "c")␤»

85.3.7. infix ~

被定义为:

multi sub infix:<~>(Str:D $a, Junction:D $b)
multi sub infix:<~>(Junction:D $a, Str:D $b)
multi sub infix:<~>(Junction:D \a, Junction:D \b)

infix ~ 可以用来将 junction 合并成一个 junction,或者将 junction 与字符串合并。所产生的 junction 将有所有元素合并,就像它们被连接到一个嵌套循环中一样。

my $odd  = 1|3|5;
my $even = 2|4|6;

my $merged = $odd ~ $even;
say $merged; #OUTPUT: «any(12, 14, 16, 32, 34, 36, 52, 54, 56)␤»

say "Found 34!" if 34 == $merged; #OUTPUT: «Found 34!␤»
my $prefixed = "0" ~ $odd;
say "Found 03" if "03" == $prefixed; #OUTPUT: «Found 03!␤»

my $postfixed = $odd ~ "1";
say "Found 11" if 11 == $postfixed; #OUTPUT: «Found 11!␤»

另一方面,使用字符串作为一个参数的 ~ 的版本只会将字符串连接到 Junction 的每个成员,创建另一个具有相同数量元素的 Junction

85.5. 类型图

Junction 的类型关系

type-graph-Junction

86. 类 Pair

Key/value pair

class Pair does Associative {}

由两部分组成,一个键和一个值。Pair 可以看作是 Hash 中的原子单位,它们也可以和命名参数和形式参数一起使用。

创建 Pair 的语法有很多:

Pair.new('key', 'value'); # The canonical way
'key' => 'value';         # this...
:key<value>;              # ...means the same as this
:key<value1 value2>;      # But this is  key => <value1 value2>
:foo(127);                # short for  foo => 127
:127foo;                  # the same   foo => 127

请注意,最后一种形式也支持非 ASCII 数字。

# use MATHEMATICAL DOUBLE-STRUCK DIGIT THREE
say (:𝟛math-three);         # OUTPUT: «math-three => 3␤»

但不支持合成的数字(即由一个数字和附加的 Unicode 标记形成)。

say :7̈a

你也可以使用类似标识符的字面值作为键,只要遵循普通标识符的语法,就不需要引号:

(foo => 127)              # the same   foo => 127

这方面的变体有:

:key;                     # same as   key => True
:!key;                    # same as   key => False

而这另一个变体,将用于常规调用中。

sub colon-pair( :$key-value ) {
    say $key-value;
}
my $key-value = 'value';
colon-pair( :$key-value );               # OUTPUT: «value␤»
colon-pair( key-value => $key-value );   # OUTPUT: «value␤»

冒号对可以在没有逗号的情况下串联起来,以创建一个列表对。根据上下文,您可能需要在分配冒号列表时明确。

sub s(*%h){ say %h.raku };
s :a1:b2;
# OUTPUT: «{:a1, :b2}␤»

my $manna = :a1:b2:c3;
say $manna.^name;
# OUTPUT: «Pair␤»

$manna = (:a1:b2:c3);
say $manna.^name;
# OUTPUT: «List␤»

任何变量都可以把它的名字和它的值变成一个 Pair

my $bar = 10;
my $p   = :$bar;
say $p; # OUTPUT: «bar => 10␤»

值得注意的是,当把一个 Scalar 赋值给一个 Pair 的值时,这个值是值本身的容器。这意味着可以从 Pair 本身的外部改变值:

my $v = 'value A';
my $pair = a => $v;
$pair.say;  # OUTPUT: «a => value A␤»

$v = 'value B';
$pair.say;  # OUTPUT: «a => value B␤»

还请注意,这种行为与用于构建 Pair 本身的方式(即显式使用 new、使用冒号、胖箭头),以及 Pair 是否绑定到一个变量上完全无关。

可以通过方法 freeze 来改变上述行为,迫使 Pair 删除标量容器,自己保持有效值:

my $v = 'value B';
my $pair = a => $v;
$pair.freeze;
$v = 'value C';
$pair.say; # OUTPUT: «a => value B␤»

由于 Pair 实现了 Associative 角色,它的值可以使用 Associative 下标运算符来访问,但是,由于 Pair 的单一性,Pair 的值将只对该对儿的键返回。对于任何其他键,将返回 Nil 对象。在 Pair 上可以使用 :exists 这样的下标副词。

my $pair = a => 5;
say $pair<a>;           # OUTPUT: «5␤»
say $pair<a>:exists;    # OUTPUT: «True␤»
say $pair<no-such-key>; # OUTPUT: «Nil␤»

86.1. 方法

86.1.1. 方法 new

被定义为:

multi method new(Pair: Mu  $key, Mu  $value)
multi method new(Pair: Mu :$key, Mu :$value)

构造一个新的 Pair 对象。

86.1.2. 方法 ACCEPTS

被定义为:

multi method ACCEPTS(Pair:D $: %topic)
multi method ACCEPTS(Pair:D $: Pair:D $topic)
multi method ACCEPTS(Pair:D $: Mu $topic)

如果 %topicAssociative,则使用其中调用者的键查找值,并检查调用者的值 .ACCEPTS 该值:

say %(:42a) ~~ :42a; # OUTPUT: «True␤»
say %(:42a) ~~ :10a; # OUTPUT: «False␤»

如果 $topic 是另一个 Pair,检查调用者的值 .ACCEPTS $topic 的值。注意,不考虑键,并且可以不同:

say :42a ~~ :42a; # OUTPUT: «True␤»
say :42z ~~ :42a; # OUTPUT: «True␤»
say :10z ~~ :42a; # OUTPUT: «False␤»

如果 $topic 是任何其他值,则调用者 Pair 的键被视为方法名。这个方法在 $topic 上被调用,其布尔结果与调用者 Pair布尔值进行比较。例如,可以使用智能匹配来测试素数:

say 3 ~~ :is-prime;             # OUTPUT: «True␤»
say 3 ~~  is-prime => 'truthy'; # OUTPUT: «True␤»
say 4 ~~ :is-prime;             # OUTPUT: «False␤»

这种形式也可以用来检查同一个对象上的多个方法的 Bool 值,比如 IO::Path,通过使用 Junction 来检查。

say "foo" .IO ~~ :f & :rw; # OUTPUT: «False␤»
say "/tmp".IO ~~ :!f;      # OUTPUT: «True␤»
say "."   .IO ~~ :f | :d;  # OUTPUT: «True␤»

86.1.3. 方法 antipair

被定义为:

method antipair(--> Pair:D)

返回一个新的 Pair 对象,其中包含键和值的交换。

my $p = (6 => 'Perl').antipair;
say $p.key;         # OUTPUT: «Perl␤»
say $p.value;       # OUTPUT: «6␤»

86.1.4. 方法 key

被定义为:

multi method key(Pair:D:)

返回 Pair 的键部分。

my $p = (Perl => 6);
say $p.key; # OUTPUT: «Perl␤»

86.1.5. 方法 value

被定义为:

multi method value(Pair:D:) is rw

返回 Pair 的值部分。

my $p = (Perl => 6);
say $p.value; # OUTPUT: «6␤»

86.1.6. infix cmp

被定义为:

multi sub infix:<cmp>(Pair:D, Pair:D)

类型无关的比较器;比较两个 Pair。首先比较它们的键值部分,如果键值相同,则比较值部分。

my $a = (Apple => 1);
my $b = (Apple => 2);

say $a cmp $b; # OUTPUT: «Less␤»

86.1.7. 方法 fmt

被定义为:

multi method fmt(Pair:D: Str:D $format --> Str:D)

接收一个格式化字符串,并返回 Pair 的键和值部分的格式化字符串。下面是一个例子:

my $pair = :Earth(1);
say $pair.fmt("%s is %.3f AU away from the sun")
# OUTPUT: «Earth is 1.000 AU away from the sun␤»

关于格式字符串的更多信息,请参见 sprintf

86.1.8. 方法 kv

被定义为:

multi method kv(Pair:D: --> List:D)

按照 Pair 的键和值的顺序返回一个双元素 List。本方法是 Hash 中同名方法的一个特例,它以键和值的列表形式返回所有条目。

my $p = (Perl => 6);
say $p.kv[0]; # OUTPUT: «Perl␤»
say $p.kv[1]; # OUTPUT: «6␤»

86.1.9. 方法 pairs

被定义为:

multi method pairs(Pair:D:)

返回一个 Pair 的列表,即这一个。

my $p = (Perl => 6);
say $p.pairs.^name; # OUTPUT: «List␤»
say $p.pairs[0];    # OUTPUT: «Perl => 6␤»

86.1.10. 方法 antipairs

被定义为:

multi method antipairs(Pair:D:)

返回一个包含调用者的 antipairList

my $p = (6 => 'Perl').antipairs;
say $p.^name;                                     # OUTPUT: «List␤»
say $p.first;                                     # OUTPUT: «Perl => 6␤»
say $p.first.^name;                               # OUTPUT: «Pair␤»

86.1.11. 方法 invert

被定义为:

method invert(Pair:D: --> Seq:D)

返回一个 Seq。如果调用者的 .value 不是 IterableSeq 将包含一个单一的 Pair,其 .key 是调用者的 .value,其 .value 是调用者的 .key

:foo<bar>.invert.raku.say; # OUTPUT: «(:bar("foo"),).Seq»

如果调用者的 .value 是一个 Iterable,那么返回的 Seq 将包含与 .value 中的项目相同数量的 Pair,其中每一项都是一对的 .key,而调用者的 .key 则是该对儿的 .value

:foo<Perl is great>.invert.raku.say;
# OUTPUT: «(:Perl("foo"), :is("foo"), :great("foo")).Seq»

:foo{ :42a, :72b }.invert.raku.say;
# OUTPUT: «((:a(42)) => "foo", (:b(72)) => "foo").Seq»

要进行精确的 .key.value 交换,使用 .antipair 方法。

86.1.12. 方法 keys

被定义为:

multi method keys(Pair:D: --> List:D)

返回一个包含调用者的List

say ('Perl' => 6).keys;                           # OUTPUT: «(Perl)␤»

86.1.13. 方法 values

被定义为:

multi method values(Pair:D: --> List:D)

返回一个包含调用者的List

say ('Perl' => 6).values;                         # OUTPUT: «(6)␤»

86.1.14. 方法 freeze

被定义为:

method freeze(Pair:D:)

Pair 的值从 Scalar 容器中移除,使其成为只读值,并将其返回。

my $str = "apple";
my $p = Pair.new('key', $str);
$p.value = "orange";              # this works as expected
$p.say;                           # OUTPUT: «key => orange␤»
$p.freeze.say;                    # OUTPUT: «orange␤»
$p.value = "a new apple";         # Fails
CATCH { default { put .^name, ': ', .Str } };
# OUTPUT: «X::Assignment::RO: Cannot modify an immutable Str (apple)␤»

注意: 从 6.d 语言版本开始,这个方法已经被废弃了。取而代之的是创建一个新的 Pair,并使用一个去容器化的 key/value。

$p.=Map.=head.say;                                    # OUTPUT: «orange␤»

86.1.15. 方法 Str

被定义为:

multi method Str(Pair:D: --> Str:D)

返回调用者的字符串表示,格式为 key ~ \t ~ value

my $b = eggs => 3;
say $b.Str;                                       # OUTPUT: «eggs  3␤»

86.1.16. 方法 Pair

被定义为:

method Pair()

返回调用的 Pair 对象。

my $pair = eggs => 3;
say $pair.Pair === $pair;                         # OUTPUT: «True␤»

86.2. 类型图

Pair 的类型关系

Pair

86.3. 由角色 Associative 提供的例程

Pair 做的角色是 Associative,它提供了以下例程:

86.3.1. (Associative) 方法 of

被定义为:

method of()

Associative 其实是一个参数化的角色,它可以使用不同的类作为键和值。从文档顶部可以看到,默认情况下,它的键强转为 Str,而值则使用非常通用的 Mu

my %any-hash;
say %any-hash.of;#  OUTPUT: «(Mu)␤»

该值是你用特定类实例化 Associative 时使用的第一个参数。

class DateHash is Hash does Associative[Cool,DateTime] {};
my %date-hash := DateHash.new;
say %date-hash.of; # OUTPUT: «(Cool)␤»

86.3.2. (Associative) 方法 keyof

被定义为:

method keyof()

返回用于 Associative 角色的参数化键,默认为 Any 强转为 Str。当你使用 Associative 的参数化版本时,这是被用作第二个参数的类。

my %any-hash;
%any-hash.keyof; #OUTPUT: «(Str(Any))␤»

86.3.3. (Associative) 方法 AT-KEY

method AT-KEY(\key)

应该返回给定键的值/容器。

86.3.4. (Associative) 方法 EXISTS-KEY

method EXISTS-KEY(\key)

应该返回一个 Bool,表示给定的键是否真的有值。

86.3.5. (Associative) 方法 STORE

method STORE(\values, :$initialize)

只有当你想支持如下语法的时候,才应该提供这个方法。

my %h is Foo = a => 42, b => 666;

该语法用于绑定你的 Associative 角色的实现。

应该接受要(重新)初始化对象的值,这些值可以是 Pair,也可以是单独的 key/value 对儿。当该方法第一次在对象上被调用时,可选的命名参数将包含一个 True 值。应该返回调用者。

87. 类 Parameter

Element of a Signature

class Parameter { }

代表一个参数,用于内省。

通常获取 Parameter 对象的方法是创建一个签名,然后调用 .params 来获取参数列表。

my $sig   = :(Str $x);
my $param = $sig.params[0];
say $param.type;              # OUTPUT: «Str()␤»

更多信息请参见 Signature,同时也是对大多数与参数相关的概念含义的解释。

87.1. 方法

87.1.1. 方法 name

返回变量名,其中包括所有的 sigils 和 twigils。这个名称在内部应用于代码时使用,或者在声明中用来确定声明的名称。这个名字不一定可以被调用者使用-如果可以,它也会作为一个别名出现。通常情况下,这个名称会被描述性地选择,作为一种自我记录的形式。

如果参数是匿名的,将返回 Nil

87.1.2. 方法 sigil

被定义为:

method sigil(Parameter:D: --> Str:D)

返回一个包含参数魔符的字符串,对于 "sigil" 的定义比 name|method name 的定义要宽松。即使参数是匿名的,仍然会返回一个sigil。

这个 "sigil" 实际上是一种内省,用于帮助确定一个参数的正常绑定风格,如果它没有通过特性被改变的话。

魔符

会被绑定为

默认行为

$

Scalar

生成新的 Scalar,用于代替参数中的 Scalar(如果有的话)

@

Positional

直接绑定到参数上

@

PositionalBindFailover

如果绑定失败,调用参数的 .cache 方法,绑定到结果

%

Associative

直接绑定到参数上

&

Callable

直接绑定到参数上

\

(anything)

直接绑定到参数上,保留现有的 Scalar(如果有的话)

同时,| 将绑定到所有剩余的参数,并在需要时进行新的捕获(Capture)。

87.1.3. 方法 type

返回参数的名义类型约束

87.1.4. 方法 coerce_type

返回参数的强转类型

87.1.5. 方法 constraints

返回参数的附加约束条件(通常为 all Junction)。

87.1.6. 方法 named

被定义为:

method named(Parameter:D: --> Bool:D)

如果是命名参数,则返回 True

my Signature $sig = :(Str $x, Bool :$is-named);
say $sig.params[0].named;                          # OUTPUT: «False␤»
say $sig.params[1].named;                          # OUTPUT: «True␤»

87.1.7. 方法 named_names

被定义为:

method named_names(Parameter:D: --> List:D)

返回命名参数的外部可用名称/别名列表。

87.1.8. 方法 positional

被定义为:

method positional(Parameter:D: --> Bool:D)

如果参数是位置性的,则返回 True

my Signature $sig = :(Str $x, Bool :$is-named);
say $sig.params[0].positional;                     # OUTPUT: «True␤»
say $sig.params[1].positional;                     # OUTPUT: «False␤»

87.1.9. 方法 slurpy

被定义为:

method slurpy(Parameter:D: --> Bool:D)

对于吞噬参数返回 True

87.1.10. 方法 twigil

被定义为:

method twigil(Parameter:D: --> Str:D)

返回包含参数名称中 twigil 部分的字符串。

87.1.11. 方法 optional

被定义为:

method optional(Parameter:D: --> Bool:D)

对于可选参数返回 True

87.1.12. 方法 raw

被定义为:

method raw(Parameter:D: --> Bool:D)

对于原始参数返回 True

sub f($a, $b is raw, \c) {
    my $sig = &?ROUTINE.signature;
    for ^$sig.params.elems {
        say $sig.params[$_].raw;
    }
}
f(17, "4711", 42); OUTPUT: «False␤True␤True␤»

原始参数可以绑定一个变量,也可以绑定一个传递给它的值,没有发生解容器化。也就是说,如果传递给它一个变量,就可以赋值给参数。这与 rw 参数不同,rw 参数只能绑定到变量,而不能绑定到值。

这是用 '\' 魔符声明的参数的正常行为,但这并不是真正的魔符,因为它只用在参数上。

sub f(\x) {
    x = 5;
}
f(my $x);   # works
f(42);      # dies
CATCH { default { put .^name, ': ', .Str } };
# OUTPUT: «X::Assignment::RO: Cannot modify an immutable Int␤»

其他参数可以通过使用 is raw 特性而变得生硬。这些参数在代码中仍然使用它们的标识。

sub f($x is raw) {
    $x = 5;
}

当与 slurpy list 参数一起使用时,is raw 特性将导致给出的参数列表被打包成 List 而不是 Array,这将防止它们被 Scalar 容器化。这是与无符号参数一起使用 + 时的默认行为。

my @types is List = Mu, Any;
say -> *@l { @l }(@types)[0] =:= @types[0];        # OUTPUT: «False␤»
say -> +@l { @l }(@types)[0] =:= @types[0];        # OUTPUT: «False␤»
say -> +l { l }(@types)[0] =:= @types[0];          # OUTPUT: «True␤»
say -> *@l is raw { @l }(@types)[0] =:= @types[0]; # OUTPUT: «True␤»

87.1.13. 方法 capture

被定义为:

method capture(Parameter:D: --> Bool:D)

对于将参数列表的其余部分捕获到一个单一的捕获对象中的参数,返回 True

sub how_many_extra_positionals($!, |capture) { capture.elems.say }
how_many_extra_positionals(0, 1, 2, 3);                        # OUTPUT: «3»
say &how_many_extra_positionals.signature.params[1].capture;   # OUTPUT: «True␤»

像原始参数一样,Capture 参数并不强迫任何上下文绑定到它们的值,这就是为什么它们的标志只在声明中使用。

87.1.14. 方法 rw

被定义为:

method rw(Parameter:D: --> Bool:D)

对于 is rw 参数返回 True

my Signature $sig = :(Str $x is rw, Bool :$is-named);
say $sig.params[0].rw;                             # OUTPUT: «True␤»
say $sig.params[1].rw;                             # OUTPUT: «False␤»

87.1.15. 方法 copy

被定义为:

method copy(Parameter:D: --> Bool:D)

对于 is copy 参数返回 True

my Signature $sig = :(Str $x, Bool :$is-named is copy);
say $sig.params[0].copy;                           # OUTPUT: «False␤»
say $sig.params[1].copy;                           # OUTPUT: «True␤»

87.1.16. 方法 readonly

被定义为:

method readonly(Parameter:D: --> Bool:D)

对于只读参数返回 True(默认)。

my Signature $sig = :(Str $x is rw, Bool :$is-named);
say $sig.params[0].readonly;                       # OUTPUT: «False␤»
say $sig.params[1].readonly;                       # OUTPUT: «True␤»

87.1.17. 方法 invocant

被定义为:

method invocant(Parameter:D: --> Bool:D)

如果该参数是调用参数,则返回 True

my Signature $sig = :($i : Str $x is rw, Bool :$is-named);
say $sig.params[0].invocant;                       # OUTPUT: «True␤»
say $sig.params[1].invocant;                       # OUTPUT: «False␤»

87.1.18. 方法 default

返回一个闭包,在调用时返回该参数的默认值,如果没有提供默认值则返回 Any

87.1.19. 方法 type_captures

被定义为:

method type_captures(Parameter:D: --> List:D)

返回与此参数相关联的类型捕获的变量名列表。类型捕获在附加的代码中定义了一个类型名,它是调用时从参数中收集的类型的别名。

sub a(::T ::U $x) { T.say }
a(8);                                       # OUTPUT: «(Int)␤»
say &a.signature.params[0].type_captures;   # OUTPUT: «(T U)␤»
sub b($x) { $x.^name.say }
a(8);                                       # OUTPUT: «Int␤»

使用的类型可能会因调用而改变。一旦它们被定义,类型捕获可以在任何你会使用类型的地方使用,甚至以后在同一个签名中使用。

sub c(::T $x, T $y, $z) { my T $zz = $z };
c(4, 5, 6);          # OK
c(4, 5, "six");      # Fails when assigning to $zz, wants Int not Str
c("four", 5, "six"); # Fails when binding $y, wants Str, not Int

类型捕获可与类型约束同时使用。

sub d(::T Numeric $x, T $y) {};
d(4, 5);            # OK
d(4e0, 5e0);        # OK
d(4e0, 5);          # Fails when binding $y
d("four", "five");  # Fails when binding $x

87.1.20. 方法 sub_signature

如果参数有子签名,则返回它的签名对象。否则返回 Any

87.1.21. 方法 prefix

被定义为:

method prefix(Parameter:D: --> Str:D)

如果参数是 slury 的,返回参数声明时的标记(如 *+)。否则,返回一个空字符串。

my Signature $flat-slurpy = :($a, *@b);
say $flat-slurpy.params[0].prefix; # OUTPUT:«""␤»
say $flat-slurpy.params[1].prefix; # OUTPUT:«*␤»

my Signature $unflat-slurpy = :($a, **@b);
say $unflat-slurpy.params[0].prefix; # OUTPUT:«""␤»
say $unflat-slurpy.params[1].prefix; # OUTPUT:«**␤»

my Signature $sar-slurpy = :($a, +@b);
say $sar-slurpy.params[0].prefix; # OUTPUT:«""␤»
say $sar-slurpy.params[1].prefix; # OUTPUT:«+␤»

87.1.22. 方法 suffix

被定义为:

method suffix(Parameter:D: --> Str:D)

如果有的话,返回参数声明的 ?! 标记,否则返回空字符串。否则,返回空字符串。

my Signature $pos-sig = :($a, $b?);
say $pos-sig.params[0].suffix; # OUTPUT: «""␤»
say $pos-sig.params[1].suffix; # OUTPUT: «?␤»

my Signature $named-sig = :(:$a!, :$b);
say $named-sig.params[0].suffix; # OUTPUT: «!␤»
say $named-sig.params[1].suffix; # OUTPUT: «""␤»

87.2. 运行时创建参数对象(6.d, 2019.03 及以后版本)

Parameter.new( ... )

在某些情况下,特别是在使用 MetaObject 协议时,以编程方式创建 Parameter 对象是有意义的。为此,您可以使用以下命名的参数来调用 new 方法。

  • name

可选。变量的名称,如果有的话。可以用与签名相同的方式指定。因此,它可能包含特定的附加信息,如一个符号($@%&),一个 : 前缀表示一个命名的参数,一个 twigil (.!)表示公共/私有属性绑定,一个后缀 !? 表示一个可选/必选参数,以及各种 +* 前缀的组合,表示吐槽类型和 | 表示一个捕获。

  • type

可选。参数的类型。如果没有指定,则假定为 Any

  • default

可选参数。如果参数是可选的,并且没有给该参数提供参数,则该参数的值。如果没有给定参数,假设不初始化,这将回到参数的(隐式)类型。

  • where

可选。附加约束条件,适用于任何与该参数匹配的参数。默认情况下不设置任何附加约束条件。

  • is-copy

可选。允许设置参数的 "is copy" 标志。默认情况下不设置该标志。

  • is-raw

可选。允许设置参数的 "is raw" 标志。默认情况下不设置该标志。

  • is-rw

可选。允许设置参数的 "is rw" 标志。默认情况下不设置该标志。

  • named

可选。表示该参数是否为命名参数。只有在名称中没有指定 : 前缀且需要命名参数时才应指定。

  • optional

可选。表示该参数是否为可选参数。只有在名称中没有指定 ? 后缀,并且需要一个可选参数时,才应指定。

  • mandatory

可选。表示该参数是否为强制性参数。只有在名称中没有指定 ! postfix,且需要强制参数时才应指定。

  • multi-invocant

可选。表示是否应该在多派发中考虑该参数,默认为 True,所以需要执行 :!multi-invocant 来使该参数在多派发中不被考虑。

  • sub-signature

可选。指定应该应用于参数的任何签名以解构它。默认情况下,不应用任何签名。

87.3. 类型图

Parameter 的类型关系

Parameter

88. 类 Pod::Block

Block in a Pod document

class Pod::Block { }

Pod 块的类,也是大多数其他 Pod 类的基础类。

Pod 块有内容(更多的pod块或字符串)和一个配置哈希。

有用的子类:

用于

Pod::Block::Code

代码块

Pod::Block::Comment

注释

Pod::Block::Declarator

声明符块

Pod::Block::Named

命名块

Pod::Block::Para

段落

Pod::Block::Table

=begin/end 表格数据

Pod::Defn

定义列表

Pod::FormattingCode

格式化代码

Pod::Heading

=head1 等标题

Pod::Item

列表项

88.1. 方法

88.1.1. 方法 contents

method contents(--> Positional:D)

返回此块的内容列表。

88.1.2. 方法 config

method config(--> Map:D)

返回配置的哈希。

88.2. 类型图

Pod::Block 的类型关系

Pod::Block

89. 角色 PositionalBindFailover

Failover for binding to a Positional

role PositionalBindFailover { ... }

这个角色提供了一个接口,通过这个接口,可以在绑定 Positional 参数时,将对象强转成 Positional

比如 Seq 类型不是 Positional,但你仍然可以写下面的内容,因为它 doesPositionalBindFailover 角色:

sub fifths(@a) {        # @a is constraint to Positional
    @a[4];
}
my $seq := gather {     # a Seq, which is not Positional
    take $_ for 1..*;
}
say fifths($seq);       # OUTPUT: «5␤»

在上面的例子中调用 fifths 通常会给出一个类型错误,因为 $seq 的类型是 Seq,而 Seq 并没有做 @ 魔符所暗示的 Positional 接口。

但签名绑定器识别出 Seq 做的是 PositionalBindFailover 角色,并调用其 cache 方法将其强转为 List,它做的是 Positional 角色。

同样的情况也发生在做这个角色的自定义类上,它们只需要提供一个产生 Iteratoriterator 方法:

class Foo does PositionalBindFailover {
    method iterator {
        class :: does Iterator {
            method pull-one {
                return 42 unless $++;
                IterationEnd
            }
        }.new
    }
}

sub first-five (@a) { @a[^5].say }
first-five Foo.new; # OUTPUT: # OUTPUT: «(42 Nil Nil Nil Nil)␤»

89.1. 方法

89.1.1. 方法 cache

method cache(PositionalBindFailover:D: --> List:D)

基于 iterator 方法返回一个 List,并将其缓存。随后对 cache 的调用总是返回相同的 List 对象。

89.1.2. 方法 list

method list(PositionalBindFailover:D: --> List:D)

返回一个基于 iterator 方法的 List,不进行缓存。

89.1.3. 方法 iterator

method iterator(PositionalBindFailover:D:) { ... }

这个方法存根确保实现角色 PositionalBindFailover 的类提供一个 iterator 方法。

89.2. 类型图

PositionalBindFailover 的类型关系

PositionalBindFailover

90. 类 Proc::Async

Running process (asynchronous interface)

class Proc::Async {}

Proc::Async 允许你异步地运行外部命令, 捕获标准输出和错误句柄, 并可选地写入其标准输入。

my $file = ‘foo’.IO;
spurt $file, “and\nCamelia\n♡\nme\n”;

my $proc = Proc::Async.new: :w, ‘tac’, ‘--’, $file, ‘-’;
# my $proc = Proc::Async.new: :w, ‘sleep’, 15; # uncomment to try timeouts

react {
    whenever $proc.stdout.lines { # split input on \r\n, \n, and \r
        say ‘line: ’, $_
    }
    whenever $proc.stderr { # chunks
        say ‘stderr: ’, $_
    }
    whenever $proc.ready {
        say ‘PID: ’, $_ # Only in Rakudo 2018.04 and newer, otherwise Nil
    }
    whenever $proc.start {
        say ‘Proc finished: exitcode=’, .exitcode, ‘ signal=’, .signal;
        done # gracefully jump from the react block
    }
    whenever $proc.print: “I\n♥\nCamelia\n” {
        $proc.close-stdin
    }
    whenever signal(SIGTERM).merge: signal(SIGINT) {
        once {
            say ‘Signal received, asking the process to stop’;
            $proc.kill; # sends SIGHUP, change appropriately
            whenever signal($_).zip: Promise.in(2).Supply {
                say ‘Kill it!’;
                $proc.kill: SIGKILL
            }
        }
    }
    whenever Promise.in(5) {
        say ‘Timeout. Asking the process to stop’;
        $proc.kill; # sends SIGHUP, change appropriately
        whenever Promise.in(2) {
            say ‘Timeout. Forcing the process to stop’;
            $proc.kill: SIGKILL
        }
    }
}

say ‘Program finished’;

上面的例子会产生以下输出:

line: me
line: ♡
line: Camelia
line: and
line: Camelia
line: ♥
line: I
Proc finished. Exit code: 0
Program finished

另外, 你也可以使用 Proc::Async 而不使用 react 块:

# 带参数的命令
my $proc = Proc::Async.new('echo', 'foo', 'bar');

# subscribe to new output from out and err handles:
$proc.stdout.tap(-> $v { print "Output: $v" }, quit => { say 'caught exception ' ~ .^name });
$proc.stderr.tap(-> $v { print "Error:  $v" });

say "Starting...";
my $promise = $proc.start;

# wait for the external program to terminate
await $promise;
say "Done.";

这将产生以下输出:

Starting...
Output: foo bar
Done.

一个例子, 打开一个外部程序进行写入:

my $prog = Proc::Async.new(:w, 'hexdump', '-C');
my $promise = $prog.start;
await $prog.write(Buf.new(12, 42));
$prog.close-stdin;
await $promise;

一个用管道连接多个命令的例子, 如 echo "Hello, world" | cat -n:

my $proc-echo = Proc::Async.new: 'echo', 'Hello, world';
my $proc-cat = Proc::Async.new: 'cat', '-n';
$proc-cat.bind-stdin: $proc-echo.stdout;
await $proc-echo.start, $proc-cat.start;

90.1. 方法

90.1.1. 方法 new

multi method new(*@ ($path, *@args), :$w, :$enc, :$translate-nl,
                 :$win-verbatim-args = False --> Proc::Async:D)
multi method new(   :$path, :@args, :$w, :$enc, :$translate-nl,
                 :$win-verbatim-args = False --> Proc::Async:D)

创建一个新的 Proc::Async 对象, 对象中包含外部程序名或路径 $path 和命令行参数 @args

如果将 :w 传递给 new, 则会打开一个通往外部程序标准输入流(stdin)的管道, 你可以用 writesay 写入。

:enc 指定流的编码(在各个方法中仍可重写), 默认为 utf8

如果 :translate-nl 被设置为 True (默认值), 操作系统特有的换行结束符(例如 Windows 上的 \r\n)将被自动翻译成 \n。

在 Windows 上, $win-verbatim-args 标志会禁用所有进程参数的自动引用。更多关于 windows 命令引用的信息, 请看这个博客。在其他平台上, 这个标志是被忽略的。这个标志是在 Rakudo 2020.06 版本中引入的, 在旧版本中不存在。

90.1.2. 方法 stdout

method stdout(Proc::Async:D: :$bin --> Supply:D)

返回外部程序的标准输出流的 Supply。如果传递了 :bin, 则标准输出流以二进制形式作为 Blob 传递, 否则将被解释为 UTF-8, 解码后以 Str 形式传递。

my $proc = Proc::Async.new(:r, 'echo', 'Raku');
$proc.stdout.tap( -> $str {
    say "Got output '$str' from the external program";
});
my $promise = $proc.start;
await $promise;

你必须在调用方法 start 之前调用 stdout, 否则会抛出一个异常 X::Proc::Async::TapBeforeSpawn

如果不调用 stdout, 外部程序的标准输出就根本无法捕获。

请注意, 你不能在同一个对象上同时调用带和不带 :binstdout;如果你尝试, 它将抛出一个类型为 X::Proc::Async::CharsOrBytes 的异常。

使用 .Supply 来合并 STDOUT 和 STDERR。

90.1.3. 方法 stderr

method stderr(Proc::Async:D: :$bin --> Supply:D)

返回外部程序的标准错误流的 Supply。如果传递了 :bin, 则标准错误将以二进制形式作为 Blob 传递, 否则将被解释为 UTF-8, 并进行解码, 然后以 Str 传递。

my $proc = Proc::Async.new(:r, 'echo', 'Raku');
$proc.stderr.tap( -> $str {
    say "Got error '$str' from the external program";
});
my $promise = $proc.start;
await $promise;

你必须在调用方法 start 之前调用 stderr, 否则会抛出一个类 X::Proc::Async::TapBeforeSpawn 的异常。

如果不调用 stderr, 外部程序的标准错误流根本不会被捕获。

请注意, 你不能在同一个对象上同时调用带和不带 :binstderr;如果你尝试, 它将抛出一个类型为 X::Proc::Async::CharsOrBytes 的异常。

使用 .Supply 来合并 STDOUT 和 STDERR。

90.1.4. 方法 bind-stdin

multi method bind-stdin(IO::Handle:D $handle)
multi method bind-stdin(Proc::Async::Pipe:D $pipe)

设置一个句柄(必须打开)或一个 Pipe 作为 STDIN的 来源。目标进程的 STDIN 必须是可写的, 否则 X::Proc::Async::BindOrUse 将被抛出。

my $p = Proc::Async.new("cat", :in);
my $h = "/etc/profile".IO.open;
$p.bind-stdin($h);
$p.start;

这相当于:

cat < /etc/profile

并将把 /etc/profile 的内容打印到标准输出。

90.1.5. 方法 bind-stdout

method bind-stdout(IO::Handle:D $handle)

将目标进程的 STDOUT 重定向到一个句柄(必须打开)。如果 STDOUT 被关闭, X::Proc::Async::BindOrUse 将被抛出。

my $p = Proc::Async.new("ls", :out);
my $h = "ls.out".IO.open(:w);
$p.bind-stdout($h);
$p.start;

这个程序会把 ls shell 命令的输出用管道输出到一个叫 ls.out 的文件中, 我们打开这个文件进行读取。

90.1.6. 方法 bind-stderr

method bind-stderr(IO::Handle:D $handle)

将目标进程的 STDERR 重定向到一个句柄(必须打开)。如果 STDERR 被关闭, X::Proc::Async::BindOrUse 将被抛出。

my $p = Proc::Async.new("ls", "--foo", :err);
my $h = "ls.err".IO.open(:w);
$p.bind-stderr($h);
$p.start;

90.1.7. 方法 w

method w(Proc::Async:D:)

如果 :w 被传递给构造函数, 也就是说, 如果外部程序启动时, 其输入流通过 .print.say.write 方法提供给程序输出, 则返回一个真值。

90.1.8. 方法 start

method start(Proc::Async:D: :$scheduler = $*SCHEDULER, :$ENV, :$cwd = $*CWD)

启动外部程序的生成。返回一个 Promise, 一旦外部程序退出, 这个 Promise 将与 Proc 对象一起被保留, 如果程序无法启动, 则被破坏。可以选择使用调度器代替默认的 $*SCHEDULER, 或者通过命名参数 :$ENV 改变进程运行的环境, 或者通过命名参数 :$cwd 改变目录。

如果 start 被调用到一个已经被调用过的 Proc::Async 对象上, 则会抛出一个类型为 X::Proc::Async::AlreadyStarted 的异常。

注意: 如果你想等待(await) Promise 并丢弃其结果, 使用:

try await $p.start;

会抛出异常, 如果程序以非零状态退出, 因为作为 Promise 结果返回的 Proc 会在沉没(sink)时抛出, 在这种情况下, 它会在 try 之外被沉没(sink)。为了避免这种情况, 自己在 try 里面沉没。

try sink await $p.start;

90.1.9. 方法 started

method started(Proc::Async:D: --> Bool:D)

.start 被调用前返回 False, 调用后返回 True

90.1.10. 方法 ready

method ready(Proc::Async:D: --> Promise:D)

返回进程成功启动后将被保留的 Promise。如果程序未能启动, 则 Promise 将被破坏。

具体实现说明: 从 Rakudo 2018.04 开始, 返回的 Promise 将持有进程id(PID)。

90.1.11. 方法 pid

method pid(Proc::Async:D: --> Promise:D)

相当于 ready

返回一个一旦程序成功启动就会被保留的 Promise。如果程序无法启动, Promise 将被破坏。返回的 Promise 将持有进程id(PID)。

具体实现说明: 从 Rakudo 2018.04 开始提供。

90.1.12. 方法 path

method path(Proc::Async:D:)

从 v6.d 开始已被废弃, 使用 command 代替。

返回作为第一个参数传递给 new 方法的外部程序的名称和/或路径。

90.1.13. 方法 args

method args(Proc::Async:D: --> Positional:D)

从v6.d开始已经废弃, 使用 command 代替。

返回传递给 new 方法的外部程序的命令行参数。

90.1.14. 方法 command

method command(Proc::Async:D: --> List:D)

从 v6.d 开始可用。

返回此 Proc::Async 对象使用的命令和参数。

my $p := Proc::Async.new: 'cat', 'some', 'files';
$p.command.say; # OUTPUT: «(cat some files)␤»

90.1.15. 方法 write

method write(Proc::Async:D: Blob:D $b, :$scheduler = $*SCHEDULER --> Promise:D)

$b 中的二进制数据写入外部程序的标准输入流中。

返回一个 Promise, 一旦数据完全落入外部程序的输入缓冲区, 这个 https://docs.raku.org/type/Promise 将被保留。

必须创建一个 Proc::Async 对象用于写入(使用 Proc::Async.new(:w, $path, @args))。否则会产生一个 X::Proc::Async::OpenForWriting 异常。

start 必须在调用方法 write 之前被调用, 否则将抛出 X::Proc::Async::MustBeStarted 异常。

90.1.16. 方法 print

method print(Proc::Async:D: Str(Any) $str, :$scheduler = $*SCHEDULER)

$str 中的文本数据写入外部程序的标准输入流, 并将其编码为 UTF-8。

返回一个 Promise, 一旦数据完全落入外部程序的输入缓冲区, 这个 https://docs.raku.org/type/Promise 将被保留。

写入时必须创建 Proc::Async 对象(使用 Proc::Async.new(:w, $path, @args))。否则会产生一个 X::Proc::Async::OpenForWriting 异常。

start 必须在调用方法 print 之前被调用, 否则将抛出 X::Proc::Async::MustBeStarted 异常。

90.1.17. 方法 say

method say(Proc::Async:D: $output, :$scheduler = $*SCHEDULER)

$output 上调用方法 gist, 添加一个换行, 编码为 UTF-8, 并将其发送到外部程序的标准输入流中, 编码为 UTF-8。

返回一个 Promise, 当数据完全降落到外部程序的输入缓冲区后, 这个 https://docs.raku.org/type/Promise 将被保留。

写入时必须创建 Proc::Async 对象(使用 Proc::Async.new(:w, $path, @args))。否则会产生一个 X::Proc::Async::OpenForWriting 异常。

start 必须在调用方法 say 之前被调用, 否则将抛出 X::Proc::Async::MustBeStarted 异常。

90.1.18. 方法 Supply

multi method Supply(Proc::Async:D: :$bin!)
multi method Supply(Proc::Async:D: :$enc, :$translate-nl)

返回合并后的 stdoutstderr 流的 Supply。如果提供了 :$bin 参数, 则 Supply 将是二进制的, 产生 Buf 对象, 否则将是字符模式, 产生 Str 对象, :$enc 参数可以指定要使用的编码:$translate-nl 选项指定新的行尾是否应该被翻译成与当前操作系统使用的行尾相匹配(例如 Windows 上的 \r\n)。

react {
    with Proc::Async.new: «"$*EXECUTABLE" -e 'say 42; note 100'» {
        whenever .Supply { .print }  # OUTPUT: «42␤100␤»
        whenever .start {}
    }
}

同时创建二进制和非二进制的 .Supply 是一个错误。同时使用 .Supplystderrstdout 供应也是一个错误。

90.1.19. 方法 close-stdin

method close-stdin(Proc::Async:D: --> True)

关闭外部程序的标准输入流。从 STDIN 读取的程序往往只有在其输入流被关闭时才会终止。因此, 如果等待方法 start 的 Promise 挂起(对于一个为写而打开的程序), 可能是忘记关闭 stdin(close-stdin)。

必须创建 Proc::Async 对象用于写入(使用 Proc::Async.new(:w, $path, @args))。否则会产生一个 X::Proc::Async::OpenForWriting 异常。

start 必须在调用方法 close-stdin 之前被调用, 否则将抛出 X::Proc::Async::MustBeStarted 异常。

90.1.20. 方法 kill

method kill(Proc::Async:D: $signal = "HUP")

向正在运行的程序发送一个信号。信号可以是一个信号名("KILL" 或 "SIGKILL"), 一个整数(9)或 Signal 枚举(Signal::SIGKILL)的一个元素。

90.2. 类型图

Proc::Async 的类型关系

Proc::Async

91. 类 Proxy

class Proxy {}

Proxy 是一个对象,它允许你设置一个钩子,当从容器中获取一个值(FETCH)或设置一个值(STORE)时,这个钩子就会执行。请注意,Proxy 可以在会破坏行为的地方引入可变性,例如在 Hash 的键中。

要创建一个容器,返回存储在它里面的两倍的值,你可以做这样的事情:

sub double() is rw {
    my $storage = 0;
    Proxy.new(
        FETCH => method ()     { $storage * 2    },
        STORE => method ($new) { $storage = $new },
    )
}
my $doubled := double();
$doubled = 4;
say $doubled;       # OUTPUT: «8␤»

91.1. 方法

91.1.1. 方法 new

method new(:&FETCH!, :&STORE! --> Proxy:D)

当访问值时,调用 &FETCH 时有一个参数(代理对象),必须返回取值产生的值。当容器中存储新的值时,调用 &STORE 时有两个参数(代理对象和新的值)。

91.2. 类型图

Proxy 类型图

Proxy

92. 角色 QuantHash

Object hashes with a limitation on the type of values

role QuantHash does Associative { }

QuantHash 角色提供了 SettyBaggyMixy 角色共享的基本功能。这些角色提供的对象哈希值在某种程度上是有限的。

QuantHash集合运算符内部使用的东西。

92.1. 方法

92.1.1. 方法 hash

method hash()

QuantHash 对象强转成一个 Hash(通过字符串化对象的键),哈希值的限制与 QuantHash 相同,并返回它。

92.1.2. 方法 Hash

method Hash()

QuantHash 对象强转为 Hash(通过字符串化对象的键),对值没有任何限制,并返回它。

92.1.3. 方法 of

method of()

返回这个 QuantHash 值的值的类型。对于 Setty 来说,通常是 Bool;对于 Baggy 来说,通常是 UInt;对于 Mixy 来说,通常是 Real

92.1.4. 方法 keyof

method keyof()

返回这个 QuantHash 子类的键的值的类型。通常是 Mu,这也是双关的 QuantHash 的默认值。

92.1.5. 方法 Setty

method Setty(--> Setty:D)

QuantHash 对象强转为使用 Setty 角色的等价对象。请注意,对于 Mixy 类型的强转,负值的项将被跳过。

my %b is Bag = one => 1, two => 2;
say %b.Setty; # OUTPUT: «set(one two)␤»
my %m is Mix = one => 1, minus => -1;
say %m.Setty; # OUTPUT: «set(one)␤»

92.1.6. 方法 Baggy

method Baggy(--> Baggy:D)

QuantHash 对象强转为使用 Baggy 角色的等价对象。注意,对于 Mixy 类型的强转,负值的项将被跳过。

my %s is Set = <one two>;
say %s.Baggy; # OUTPUT: «Bag(one, two)␤»
my %m is Mix = one => 1, minus => -1;
say %m.Baggy; # OUTPUT: «Bag(one)␤»

92.1.7. 方法 Mixy

method Mixy(--> Mixy:D)

QuantHash 对象强转为使用 Mixy 角色的等价对象。

my %s is Set = <one two>;
say %s.Mixy; # OUTPUT: «Mix(one, two)␤»
my %b is Bag = one => 1, two => 2;
say %b.Mixy; # OUTPUT: «Mix(one, two)␤»

92.2. 类型图

QuantHash 的类型关系

QuantHash

93. 类 Range

Interval of ordered values

93.1. 简介

class Range is Cool does Iterable does Positional {}

范围有两个主要目的:生成连续的数字或字符串列表,以及作为匹配器检查一个数字或字符串是否在某个范围内。

范围是使用四个可能的范围运算符之一来构建的,这些运算符由两个点组成,还可以选择一个 ^ 符号,表示标有 ^ 符号的端点不在范围内。

1 .. 5;  # 1 <= $x <= 5
1^.. 5;  # 1 <  $x <= 5
1 ..^5;  # 1 <= $x <  5
1^..^5;  # 1 <  $x <  5

^ 符号也是一个前缀运算符,用于构造从零开始的数字范围。

my $x = 10;
say ^$x;     # same as 0 ..^ $x.Numeric

迭代一个范围(或调用 list 方法)使用与 ++ 前缀和后缀操作符相同的语义,即在起点上调用 succ 方法,然后调用生成的元素。

范围总是从小到大的元素;如果起始点大于结束点,则认为范围是空的。

for 1..5 { .say };       # OUTPUT: «1␤2␤3␤4␤5␤»
('a' ^..^ 'f').list;     # OUTPUT: «'b', 'c', 'd', 'e'»
5 ~~ ^5;                 # OUTPUT: «False»
4.5 ~~ 0..^5;            # OUTPUT: «True»
(1.1..5).list;           # OUTPUT: «(1.1, 2.1, 3.1, 4.1)»

使用 …​ 序列操作符来生成从大值到小值的元素列表,或者使用除按1递增以外的偏移量和其他复杂情况。

使用 *(Whatever)表示终点是开放式的。

for 1..* { .say };       # start from 1, continue until stopped
for 1..∞ { .say };       # the same

当心 WhateverCode 端点,而不是一个普通的 Whatever,会经过范围运算符,创建另一个 WhateverCode,返回一个 Range

# A Whatever produces the 1..Inf range
say (1..*).^name;        # OUTPUT: «Range␤»
say (1..*);              # OUTPUT: «1..Inf␤»
# Upper end point is now a WhateverCode
say (1..*+20).^name;     # OUTPUT: «{ ... }␤»
say (1..*+20).WHAT;      # OUTPUT: «(WhateverCode)␤»
say (1..*+20).(22);      # OUTPUT: «1..42␤»

Range 实现了 Positional 接口,所以它的元素可以使用索引来访问。当给定的索引大于 Range 对象的大小时,将返回 Nil 对象。该访问也适用于惰性的 Range 对象。

say (1..5)[1];  # OUTPUT: «2␤»
say (1..5)[10]; # OUTPUT: «Nil␤»
say (1..*)[10]; # OUTPUT: «11␤»

93.1.1. 下标中的 Range

Range 可以在下标中使用,以获得一系列的值。请注意,将 Range 赋值给标量容器,会将 Range 变成一个项。使用绑定、带 @ 魔符的标量容器或滑入(slip)来获得你的意思。

my @numbers =  <4 8 15 16 23 42>;
my $range := 0..2;
.say for @numbers[$range]; # OUTPUT: «4␤8␤15␤»
my @range = 0..2;
.say for @numbers[@range]; # OUTPUT: «4␤8␤15␤»

93.1.2. 平移和缩放间隔

它可以平移或缩放范围的间隔:

say (1..10) + 1;       # OUTPUT: «2..11␤»
say (1..10) - 1;       # OUTPUT: «0..9␤»
say (1..10) * 2;       # OUTPUT: «2..20␤»
say (1..10) / 2;       # OUTPUT: «0.5..5.0␤»

93.1.3. 与区间匹配

你可以使用智能匹配来匹配范围。

say 3 ~~ 1..12;          # OUTPUT: «True␤»
say 2..3 ~~ 1..12;       # OUTPUT: «True␤»

仅在 Rakudo 中,你可以使用 in-range 方法对一个范围进行匹配,其实这相当于智能匹配,只是当超出范围时,它会抛出一个异常,而不是返回 False

say ('א'..'ת').in-range('ע');  # OUTPUT: «True␤»

但是,如果它不在范围内:

say ('א'..'ת').in-range('p', "Letter 'p'");
# OUTPUT: «(exit code 1) Letter 'p' out of range. Is: "p", should be in "א".."ת"␤

in-range 的第二个参数是将打印异常的可选信息。默认情况下,它将打印 Value

93.2. 方法

93.2.1. 方法 ACCEPTS

被定义为:

multi method ACCEPTS(Range:D: Mu \topic)
multi method ACCEPTS(Range:D: Range \topic)
multi method ACCEPTS(Range:D: Cool:D \got)
multi method ACCEPTS(Range:D: Complex:D \got)

表示该范围是否包含(与)另一个范围重叠。举个例子:

my $p = Range.new( 3, 5  );
my $r = Range.new( 1, 10 );

say $p.ACCEPTS( $r );    # OUTPUT: «False␤»
say $r.ACCEPTS( $p );    # OUTPUT: «True␤»
say $r ~~ $p;            # OUTPUT: «False␤»  (same as $p.ACCEPTS( $r )
say $p ~~ $r;            # OUTPUT: «True␤»   (same as $r.ACCEPTS( $p )

当然,一个无限的 Range 总是包含另一个 Range,因此:

say 1..10 ~~ -∞..∞;    # OUTPUT: «True␤»
say 1..10 ~~ -∞^..^∞;  # OUTPUT: «True␤»

同样,一个具有开放边界的 Range 往往包括其他 Range

say 1..2 ~~ *..10;  # OUTPUT: «True␤»
say 2..5 ~~ 1..*;   # OUTPUT: «True␤»

也可以使用非数字范围,例如基于字符串的范围。

say 'a'..'j' ~~ 'b'..'c';  # OUTPUT: «False␤»
say 'b'..'c' ~~ 'a'..'j';  # OUTPUT: «True␤»
say 'perl' ~~ -∞^..^∞;     # OUTPUT: «True␤»
say 'perl' ~~ -∞..∞;       # OUTPUT: «True␤»
say 'perl' ~~ 1..*;        # OUTPUT: «True␤»

当智能匹配一个整数 Range 与一个 Cool(字符串)时,ACCEPTS 方法利用 beforeafter 运算符来检查 Cool 值是否与范围重叠。

say 1.10 ~~ '5';   # OUTPUT: «False␤»
say '5' before 1;  # OUTPUT: «False␤»
say '5' after 10;  # OUTPUT: «True␤»
say '5' ~~ *..10;  # OUTPUT: «False␤»

在上面的例子中,由于 '5' 字符串是在 10 的整数值之后,所以 Range 不与指定的值重叠。

当与 Mu 实例(即通用实例)匹配时,使用 cmp 操作符。

93.2.2. 方法 min

method min(Range:D:)

返回范围的起始点。

say (1..5).min;                                   # OUTPUT: «1␤»
say (1^..^5).min;                                 # OUTPUT: «1␤»

93.2.3. 方法 excludes-min

method excludes-min(Range:D: --> Bool:D)

如果起始点被排除在范围之外,则返回 True,否则返回 False

say (1..5).excludes-min;                          # OUTPUT: «False␤»
say (1^..^5).excludes-min;                        # OUTPUT: «True␤»

93.2.4. 方法 max

method max(Range:D:)

返回范围的结束点。

say (1..5).max;                                   # OUTPUT: «5␤»
say (1^..^5).max;                                 # OUTPUT: «5␤»

93.2.5. 方法 excludes-max

method excludes-max(Range:D: --> Bool:D)

如果结束点被排除在范围之外,则返回 True,否则返回 False

say (1..5).excludes-max;                          # OUTPUT: «False␤»
say (1^..^5).excludes-max;                        # OUTPUT: «True␤»

93.2.6. 方法 bounds

method bounds()

返回一个由起点和终点组成的列表。

say (1..5).bounds;                                # OUTPUT: «(1 5)␤»
say (1^..^5).bounds;                              # OUTPUT: «(1 5)␤»

93.2.7. 方法 infinite

method infinite(Range:D: --> Bool:D)

如果任一端点用 * 声明,则返回 True

say (1..5).infinite;                              # OUTPUT: «False␤»
say (1..*).infinite;                              # OUTPUT: «True␤»

93.2.8. 方法 is-int

method is-int(Range:D: --> Bool:D)

如果两个端点都是 Int 值,则返回 True

say ('a'..'d').is-int;                            # OUTPUT: «False␤»
say (1..^5).is-int;                               # OUTPUT: «True␤»
say (1.1..5.5).is-int;                            # OUTPUT: «False␤»

93.2.9. 方法 int-bounds

proto method int-bounds(|)
multi method int-bounds()
multi method int-bounds($from is rw, $to is rw --> Bool:D)

如果 Range 是一个整数范围(如 is-int 所示),那么这个方法返回一个列表,其中包含了它要遍历的第一个和最后一个值(考虑 excludes-minexcludes-max)。如果不是整数范围,则返回失败。

say (2..5).int-bounds;                            # OUTPUT: «(2 5)␤»
say (2..^5).int-bounds;                           # OUTPUT: «(2 4)␤»

如果调用(可写)参数,这些参数将取较高和较低边界的值,并返回是否可以从 Range 中确定整数边界。

if (3..5).int-bounds( my $min, my $max) {
    say "$min, $max" ; # OUTPUT: «3, 5␤»
}
else {
    say "Could not determine integer bounds";
}

93.2.10. 方法 minmax

被定义为:

multi method minmax(Range:D: --> List:D)

如果 Range 是一个整数范围(如 is-int 所示),那么这个方法将返回一个包含第一个和最后一个值的列表,它将遍历这个列表(考虑到 excludes-minexcludes-max)。如果 Range 不是一个整数范围,那么这个方法将返回一个包含范围的起始点和终点的两元素列表,除非 excludes-minexcludes-max 中的任何一个为 True,在这种情况下,将返回一个 Failure

my $r1 = (1..5); my $r2 = (1^..5);
say $r1.is-int, ', ', $r2.is-int;                 # OUTPUT: «True, True␤»
say $r1.excludes-min, ', ', $r2.excludes-min;     # OUTPUT: «False, True␤»
say $r1.minmax, ', ', $r2.minmax;                 # OUTPUT: «(1 5), (2 5)␤»

my $r3 = (1.1..5.2); my $r4 = (1.1..^5.2);
say $r3.is-int, ', ', $r4.is-int;                 # OUTPUT: «False, False␤»
say $r3.excludes-max, ', ', $r4.excludes-max;     # OUTPUT: «False, True␤»
say $r3.minmax;                                   # OUTPUT: «(1.1 5.2)␤»
say $r4.minmax;
CATCH { default { put .^name, ': ', .Str } };
# OUTPUT: «X::AdHoc: Cannot return minmax on Range with excluded ends␤»

93.2.11. 方法 elems

method elems(Range:D: --> Numeric:D)

返回范围内的元素数,例如,当被迭代时,或作为 List 使用时。如果起始点大于结束点,返回 0,包括当起始点被指定为 时。当 Range 是惰性的,包括当终点被指定为 或任何一个终点被指定为 * 时,返回 0

say (1..5).elems;                                 # OUTPUT: «5␤»
say (1^..^5).elems;                               # OUTPUT: «3␤»

93.2.12. 方法 list

method list(Range:D: --> List:D)

生成该范围所代表的元素列表。

say (1..5).list;                                  # OUTPUT: «(1 2 3 4 5)␤»
say (1^..^5).list;                                # OUTPUT: «(2 3 4)␤»

93.2.13. 方法 flat

method flat(Range:D: --> List:D)

生成该范围所代表的元素列表。

93.2.14. 方法 pick

multi method pick(Range:D:         --> Any:D)
multi method pick(Range:D: $number --> Seq:D)

执行与 Range.list.pick 相同的功能,但如果没有必要,则不实际生成列表,从而尝试优化。

93.2.15. 方法 roll

multi method roll(Range:D:         --> Any:D)
multi method roll(Range:D: $number --> Seq:D)

执行与 Range.list.roll 相同的功能,但如果没有必要,则不实际生成列表,从而尝试优化。

93.2.16. 方法 sum

multi method sum(--> Numeric:D)

返回 Range 中所有元素的总和。如果一个元素不能被强制转换成 Numeric,则抛出 X::Str::Numeric

(1..10).sum                                       # 55

93.2.17. 方法 reverse

method reverse(Range:D: --> Seq:D)

返回一个 Seq,其中 Range 代表的所有元素都被反转。注意,反转一个无限的 Range 不会产生任何有意义的结果。

say (1^..5).reverse;                            # OUTPUT: «(5 4 3 2)␤»
say ('a'..'d').reverse;                         # OUTPUT: «(d c b a)␤»
say (1..∞).reverse;                             # OUTPUT: «(Inf Inf Inf ...)␤»

93.2.18. 方法 Capture

被定义为:

method Capture(Range --> Capture:D)

返回一个捕获值为 .min.max.excludes-min、.excludes-max.infinite.is-int 的命名参数。

93.2.19. 方法 rand

被定义为:

method rand(Range:D --> Num:D)

返回一个属于范围的伪随机值。

say (1^..5).rand;                              # OUTPUT: «1.02405550417031␤»
say (0.1..0.3).rand;                           # OUTPUT: «0.2130353370062␤»

93.2.20. 方法 EXISTS-POS

被定义为:

multi method EXISTS-POS(Range:D: int \pos)
multi method EXISTS-POS(Range:D: Int \pos)

如果 pos 大于或等于零且小于 self.elems,返回 True。否则返回 False

say (6..10).EXISTS-POS(2); # OUTPUT: «True␤»
say (6..10).EXISTS-POS(7); # OUTPUT: «False␤»

93.2.21. 方法 AT-POS

被定义为:

multi method AT-POS(Range:D: int \pos)
multi method AT-POS(Range:D: int:D \pos)

检查 Int 位置是否存在,如果存在,则返回该位置的元素。

say (1..4).AT-POS(2) # OUTPUT: «3␤»

93.2.22. 方法 raku

被定义为:

multi method raku(Range:D:)

返回一个特定于实现的字符串,当给 EVAL 时产生一个等价对象。

say (1..2).raku # OUTPUT: «1..2␤»

93.2.23. 方法 fmt

被定义为:

method fmt(|c)

返回一个字符串,其中 Range 中的最小值和最大值已经根据 |c 格式化。

关于字符串格式化的更多信息,请参见 sprintf

say (1..2).fmt("Element: %d", ",") # OUTPUT: «Element: 1,Element: 2␤»

93.2.24. 方法 WHICH

被定义为:

multi method WHICH (Range:D:)

这将返回一个标识对象的字符串。该字符串由实例的类型(Range)以及 minmax 属性组成。

say (1..2).WHICH # OUTPUT: «Range|1..2␤»

93.2.25. 子例程 infix:<+>

multi sub infix:<+>(Range:D \r, Real:D \v)
multi sub infix:<+>(Real:D \v, Range:D \r)

接收一个 Real 数,并将其加到 Range 对象的两个边界上。小心使用括号。

say (1..2) + 2; # OUTPUT: «3..4␤»
say 1..2 + 2;   # OUTPUT: «1..4␤»

93.2.26. 子例程 infix:<→

multi sub infix:<->(Range:D \r, Real:D \v)

接收一个 Real,并将该数字减去 Range 对象的两个边界。小心使用括号。

say (1..2) - 1; # OUTPUT: «0..1␤»
say 1..2 - 1;   # OUTPUT: «1..1␤»

93.2.27. 子例程 infix:<*>

multi sub infix:<*>(Range:D \r, Real:D \v)
multi sub infix:<*>(Real:D \v, Range:D \r)

接收一个 Real,并将 Range 对象的两个边界乘以该数字。

say (1..2) * 2; # OUTPUT: «2..4␤»

93.2.28. 子例程 infix:</>

multi sub infix:</>(Range:D \r, Real:D \v)

接收一个 Real,然后用这个数字除以 Range 对象的两个边界。

say (2..4) / 2; # OUTPUT: «1..2␤»

93.2.29. 子例程 infix:<cmp>

multi sub infix:<cmp>(Range:D \a, Range:D \b --> Order:D)
multi sub infix:<cmp>(Num(Real) \a, Range:D \b --> Order:D)
multi sub infix:<cmp>(Range:D \a, Num(Real) \b --> Order:D)
multi sub infix:<cmp>(Positional \a, Range:D \b --> Order:D)
multi sub infix:<cmp>(Range:D \a, Positional \b --> Order:D)

比较两个 Range 对象。如果你使用 Real,它将被比较到 Range b…​b。如果你使用 Positional

say (1..2) cmp (1..2); # OUTPUT: «Same␤»
say (1..2) cmp (1..3); # OUTPUT: «Less␤»
say (1..4) cmp (1..3); # OUTPUT: «More␤»
say (1..2) cmp 3;      # OUTPUT: «Less␤»
say (1..2) cmp [1,2];  # OUTPUT: «Same␤»

Range

94. 签名

Parameter list pattern

class Signature {}

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

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

94.1. 签名字面量

签名出现在子例程方法名后面的圆括号中, 对于 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' } );

你可以使用任何类型的字面量, 包括数字字面量, 作为签名的一部分;这通常与 multis 结合使用。

proto sub stuff(|) {*}
multi sub stuff(33) { 58 }
multi sub stuff(⅓) { 43 }
multi sub stuff(Int)  { 3 }
multi sub stuff(Complex)  { 66 }
say stuff($_) for (33, ⅓, i, 48); # OUTPUT: «58␤43␤66␤3␤»

然而, 你不能在签名中使用 TrueFalse 作为字面值, 因为它们总是会成功(或失败)。如果你这样做, 将被发出警告:

sub foo(True) {};
my $sig =  :( True );

它们都会警告 "Literal values in signatures are smartmatched against and smartmatch with True will always succeed. Use the where clause instead."。使用 False 会产生类似的警告。

支持根据列表(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 的一个更通用的版本。

当对散列(Hash)进行智能匹配时, 签名被假定为由散列的键组成。

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

签名(Signature)字面量可以包含字符串/数字字面量。

my $sig = :('Þor', Str, Int);
say <Þor Hammer 1> ~~ $sig; # OUTPUT: «True␤»

而且它们也可以包含调用者标记。

class Foo {
    method bar( $self: ){ "baz" }
};
say Foo.^methods.first(*.name eq 'bar').signature ~~ :($: *%) ;
# OUTPUT: «True␤»

94.1.1. 参数分隔符

签名由逗号分割的 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; # OUTPUT: «Well I'm class Foo, of course!␤»

94.1.2. 类型约束

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

my $sig = :(Int $a, Str $b)

类型约束可以有任何编译时定义的值。

subset Positive-integer of Int where * > 0;
sub divisors(Positive-integer $n) { $_ if $n %% $_ for 1..$n };
divisors 2.5;
# ERROR «Type check failed in binding to parameter '$n';
# expected Positive-integer but got Rat (2.5) $n)»
divisors -3;
# ERROR: «Constraint type check failed in binding to parameter '$n';
# expected Positive-integer but got Int (-3)»

请注意, 在上面的代码中, 类型约束是在两个不同的层次上执行的:第一个层次检查它是否属于子集(subset)所基于的类型, 在本例中是 Int。如果它失败了, 就会产生一个类型检查错误。一旦该过滤器被清除, 就会检查定义子集的约束, 如果失败就会产生一个约束类型检查(`Constraint type check `)错误。

匿名参数也可以, 如果你实际上不需要用名字来引用一个参数, 例如为了区分 multi 中的不同签名, 或者检查 Callable 的签名。

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, 则使用它的值进行智能匹配…​"。这是 (.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'…»
约束可选参数

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

sub f(Int $a, UInt $i? where { !$i.defined or $i > 5 } ) { ... }
约束吞噬参数

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

sub f(*@a where { $_.all ~~ Int }) { say @a };
f(42);
f(<a>);
CATCH { default { say .^name, ' ==> ', .Str }  }
# OUTPUT: «[42]␤Constraint type check failed in binding to parameter '@a' ...»
约束命名参数

命名参数的约束适用于冒号对的值部分。

sub f(Int :$i){};
f :i<forty-two>;
CATCH { default { say .^name, ' ==> ', .Str }  }
# OUTPUT: «X::TypeCheck::Binding::Parameter ==> Type check failed in
# binding to parameter '$i'; expected Int but got Str ("forty-two")␤»
约束参数的定义值

通常, 类型约束只检查参数的值是否为正确的类型。至关重要的是, 对象实例和类型对象都将满足这样的约束条件, 如下所示:

say  42.^name;    # OUTPUT: «Int␤»
say  42 ~~ Int;   # OUTPUT: «True␤»
say Int ~~ Int;   # OUTPUT: «True␤»

请注意 42Int 是如何满足匹配的。

有时我们需要区分这些对象实例(42)和类型对象(Int)。考虑下面的代码:

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 类型约束。这个约束检查传递的值是否是一个对象实例, 类似于调用它的 DEFINITE(元)方法。

为了热身, 让我们将 :D 应用到我们简陋的 Int 示例的右侧:

say  42 ~~ Int:D;  # OUTPUT: «True␤»
say Int ~~ Int:D;  # OUTPUT: «False␤»

请注意, 在上面的内容中, 只有 42 符合 Int:D

回到 limit-lines, 我们现在可以修改它的签名以尽早发现错误:

sub limit-lines (Str:D $s, Int $limit) { };
say limit-lines Str, 3;
CATCH { default { put .^name ~ '--' ~ .Str } };
# OUTPUT: «Parameter '$s' of routine 'limit-lines' must be an object instance of type 'Str',
#          not a type object of type 'Str'.  Did you forget a '.new'?»

这比之前程序失败的方式要好得多, 因为这里失败的原因比较明确。

也有可能类型对象是一个例程唯一有意义的接受对象。这可以通过 :U 类型约束来实现, 它检查传递的值是否是一个类型对象而不是一个对象实例。下面是我们的 Int 例子, 这次应用了 :U

say  42 ~~ Int:U;  # OUTPUT: «False␤»
say Int ~~ Int:U;  # OUTPUT: «True␤»

现在 42 未能匹配 Int:U, 而 Int 成功了。

下面是一个更实际的例子:

sub can-turn-into(Str $string, Any:U $type) {
   return so $string.$type;
}
say can-turn-into("3", Int);        # OUTPUT: «True␤»
say can-turn-into("6.5", Int);      # OUTPUT: «True␤»
say can-turn-into("6.5", Num);      # OUTPUT: «True␤»
say can-turn-into("a string", Num); # OUTPUT: «False␤»

用对象实例作为第二个参数调用 can-turn-into 会产生预期的约束违反。

say can-turn-into("a string", 123);
# OUTPUT: «Parameter '$type' of routine 'can-turn-into' must be a type object
# of type 'Any', not an object instance of type 'Int'...»

对于明确表示正常行为, 即不约束参数将是实例还是类型对象, 可以使用 :_, 但这是不必要的, 因为这是对参数的默认约束(这种)。因此, :(Num:_ $):(Num $) 是一样的。

总结一下, 这里是这些类型约束的快速说明, 也统称为类型笑脸。

# Checking a type object
say Int ~~ Any:D;    # OUTPUT: «False␤»
say Int ~~ Any:U;    # OUTPUT: «True␤»
say Int ~~ Any:_;    # OUTPUT: «True␤»

# Checking a subset
subset Even of Int where * // 2;
say 3 ~~ Even:D;     # OUTPUT: «True␤»
say 3 ~~ Even:U;     # OUTPUT: «False␤»
say Int ~~ Even:U;   # OUTPUT: «True␤»

# Checking an object instance
say 42 ~~ Any:D;     # OUTPUT: «True␤»
say 42 ~~ Any:U;     # OUTPUT: «False␤»
say 42 ~~ Any:_;     # OUTPUT: «True␤»

# Checking a user-supplied class
class Foo {};
say Foo ~~ Any:D;    # OUTPUT: «False␤»
say Foo ~~ Any:U;    # OUTPUT: «True␤»
say Foo ~~ Any:_;    # OUTPUT: «True␤»

# Checking an instance of a class
my $f = Foo.new;
say $f  ~~ Any:D;    # OUTPUT: «True␤»
say $f  ~~ Any:U;    # OUTPUT: «False␤»
say $f  ~~ Any:_;    # OUTPUT: «True␤»

类和对象文档进一步阐述了实例和类型对象的概念, 以及用 .DEFINITE 方法发现它们。

请记住所有的参数都有值, 即使是可选的参数也有默认值, 对于显式类型约束来说, 默认值是约束类型的类型对象。如果不存在显式类型约束, 那么对于方法、子方法和子程序来说, 默认值是 Any 类型的对象, 对于块来说, 默认值是 Mu 类型的对象。这意味着, 如果你使用 :D 类型的笑脸, 你需要提供一个默认值或使参数成为必需的。否则, 默认值将是一个类型对象, 这将使定义性约束失败。

sub divide (Int:D :$a = 2, Int:D :$b!) { say $a/$b }
divide :1a, :2b; # OUTPUT: «0.5␤»

当那个特定的参数(无论是位置参数还是命名参数)完全没有值时, 默认值就会启动。

sub f($a = 42){
  my $b is default('answer');
  say $a;
  $b = $a;
  say $b
};
f;     # OUTPUT: «42␤42␤»
f Nil; # OUTPUT: «Nil␤answer␤»

$a 的默认值是 42。在没有值的情况下, $a 将被分配到签名中声明的默认值。然而, 在第二种情况下, 它确实收到了一个值, 恰好是 Nil。将 Nil 分配给任何变量都会将其重置为默认值, 而默认值已经通过使用默认特性声明为 answer。例行参数和变量对缺省值的处理方式不同, 这在一定程度上是通过在每种情况下声明缺省值的不同方式来说明的(对参数使用 =, 对变量使用 default 特性)。

注意:在 6.c 语言中, :U/:D 约束变量的默认值是一个带有这种约束的类型对象, 它是不可初始化的, 因此不能使用 .= 运算符, 例如:

use v6.c;
my Int:D $x .= new: 42;
# OUTPUT: You cannot create an instance of this type (Int:D)
# in block <unit> at -e line 1

在 6.d 语言中, 默认的是没有笑脸约束的类型对象。

use v6.d;
my Int:D $x .= new: 42; # OUTPUT: «42␤»

关于术语的结束语:本节是关于使用类型笑脸 :D:U 来限制参数的定义性。偶尔 definedness 被用作 definiteness 的同义词;这可能会引起混淆, 因为这两个术语有着微妙的不同含义。

如上所述, 定义性涉及类型对象和对象实例之间的区别。一个类型对象总是不确定的, 而一个对象实例总是确定的。一个对象是类型对象/不确定还是对象实例/确定, 可以使用 DEFINITE(元)方法来验证。

Definiteness 应该与 definedness 区分开来, 后者关注的是定义对象和未定义对象之间的区别。一个对象是定义的还是未定义的, 可以使用定义的方法来验证, 这个方法在类 Mu 中实现。默认情况下, 一个类型对象被认为是未定义的, 而一个对象实例被认为是被定义的;也就是说:.defined 对一个类型对象返回 False, 否则返回 True。但是这个默认行为可以被子类覆盖。覆盖默认的 .defined 行为的子类的一个例子是 Failure, 因此即使是实例化的 Failure 也会作为一个未定义的值:

my $a = Failure;                # Initialize with type object
my $b = Failure.new("foo");     # Initialize with object instance
say $a.DEFINITE;                # Output: «False␤» : indefinite type object
say $b.DEFINITE;                # Output: «True␤»  : definite object instance
say $a.defined;                 # Output: «False␤» : default response
say $b.defined;                 # Output: «False␤» : .defined override
约束 Callables 的签名

可调用(Callable)参数的签名可以通过在参数后面指定一个签名字面量来限制(不允许有空格)。

sub apply(&l:(Int:D --> Int:D), Int:D \n) {
    l(n)
}

sub identity(Int:D \i --> Int:D) { i }
sub double(Int:D \x --> Int:D) { 2 * x }

say apply &identity, 10; # OUTPUT: «10␤»
say apply &double, 10;   # OUTPUT: «20␤»

类型化的 lambdas 对带约束的可调用参数也有效。

say apply -> Int:D \x --> Int:D { 2 * x }, 3;  # OUTPUT: «6␤»
say apply -> Int:D \x --> Int:D { x ** 3 }, 3; # OUTPUT: «27␤»

请注意, 这种速记语法只适用于带有 & 符号的参数。对于其他参数, 你需要使用长版本:

sub play-with-tens($c where .signature ~~ :(Int, Str)) { say $c(10, 'ten') }
sub by-joining-them(Int $i, Str $s) { $s ~ $i }
play-with-tens &by-joining-them;                              # OUTPUT: «ten10␤»
play-with-tens -> Int \i, Str \s { s x (1..10).roll mod i };  # OUTPUT: «tenten␤»

sub g(Num $i, Str $s) { $s ~ $i }
# play-with-tens(&g); # Constraint type check failed
约束返回类型

有多种方法可以限制例程的返回类型。以下所有版本目前都是有效的, 并且会在成功执行例程时强制进行类型检查。

NilFailure 总是被允许作为返回类型, 不管任何类型约束。这允许 Failure 被返回并在调用链上传递。

sub foo(--> Int) { Nil };
say foo.raku; # OUTPUT: «Nil␤»

不支持类型捕获。

返回类型箭头: -->

这种在签名中表示返回类型(或常量)的形式是首选, 因为它可以处理常量值, 而其他形式不能。为了保持一致性, 这是本站唯一接受的形式。

返回类型箭头必须放在参数列表的末尾, 在它之前有一个逗号(,), 已可以没有。

sub greeting1(Str $name  --> Str) { say "Hello, $name" } # Valid
sub greeting2(Str $name, --> Str) { say "Hello, $name" } # Valid

sub favorite-number1(--> 42) {        } # OUTPUT: 42
sub favorite-number2(--> 42) { return } # OUTPUT: 42

如果类型约束是一个常量表达式, 则将其作为该例程的返回值。该例程中的任何 return 语句必须是无参数的。

sub foo(Str $word --> 123) { say $word; return; }
my $value = foo("hello"); # OUTPUT: hello
say $value;               # OUTPUT: 123
# The code below will not compile
sub foo(Str $word --> 123) { say $word; return $word; }
my $value = foo("hello");
say $value;
returns

在签名声明后面的关键字 returns 与 --> 有相同的功能, 但需要注意的是, 这种形式对常量值不起作用。你也不能在块中使用它。这就是为什么总是首选尖箭头形式的原因。

sub greeting(Str $name) returns Str { say "Hello, $name" } # Valid
sub favorite-number returns 42 {        } # This will fail.
of

of 只是 returns 关键字的真实名称。

sub foo() of Int { 42 }; # Valid
sub foo() of 42 {  };    # This will fail.
prefix(C-like) 形式

这类似于将类型约束放置在变量上, 比如 my Type $var = 20;, 只不过 $var 是例程的定义。

my Int sub bar { 1 };     # Valid
my 42 sub bad-answer {};  # This will fail.
强制转换类型

要接受一个类型, 但又自动将其强制转换到另一个类型, 请将接受的类型作为目标类型的参数。如果接受的类型是 Any, 则可以省略。

sub f(Int(Str) $want-int, Str() $want-str) {
    say $want-int.^name ~ ' ' ~ $want-str.^name
}
f '10', 10;
# OUTPUT: «Int Str␤»

use MONKEY;
augment class Str { method Date() { Date.new(self) } };
sub foo(Date(Str) $d) { say $d.^name; say $d };
foo "2016-12-01";
# OUTPUT: «Date␤2016-12-01␤»

强制转换是通过调用带有要强制转换到的类型名称的方法来执行的, 如果它存在的话(例如, Foo(Bar) 强转者, 将调用方法 Foo)。该方法被假定为返回正确的类型-目前没有对结果进行额外的检查。

也可以对返回类型进行强制转换。

sub square-str (Int $x --> Str(Int)) {
    $x²
}

for 2,4, *²  … 256 -> $a {
    say $a, "² is ", square-str( $a ).chars, " figures long";
}

# OUTPUT: «2² is 1 figures long␤
#          4² is 2 figures long␤
#          16² is 3 figures long␤
#          256² is 5 figures long␤»

在这个例子中, 强制转换返回类型为 Str, 我们可以直接应用字符串方法, 如字符数。

94.1.3. 吞噬参数(或长度可变参数)

如果一个函数可以接受不同数量的参数, 那么它就是可变的;也就是说, 它的参数数量(arity)不是固定的。因此, 可选参数、命名参数和吞噬参数都是可变的。一个数组或哈希参数可以通过前导的单个星(*)或双星号(**)或前导的加号(+)标记为吞噬参数。一个吞噬参数可以绑定到任意数量的参数(零或更多), 它将导致一个与魔符兼容的类型。

这些参数被称为 "slurpy", 因为它们会吞噬函数的任何剩余参数, 就像某人吞噬面条一样。

$ = :($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>; # OUTPUT: «foo bar␤»

位置参数和命名参数可以结合起来;命名参数(即 Pair)收集在指定的散列中, 位置参数收集在数组中。

sub combined-slurpy (*@a, *%h) { { array => @a, hash => %h } }
# or: sub combined-slurpy (*%h, *@a) { ... }

say combined-slurpy(one => 1, two => 2);
# OUTPUT: «{array => [], hash => {one => 1, two => 2}}␤»
say combined-slurpy(one => 1, two => 2, 3, 4);
# OUTPUT: «{array => [3 4], hash => {one => 1, two => 2}}␤»
say combined-slurpy(one => 1, two => 2, 3, 4, five => 5);
# OUTPUT: «{array => [3 4], hash => {five => 5, one => 1, two => 2}}␤»
say combined-slurpy(one => 1, two => 2, 3, 4, five => 5, 6);
# OUTPUT: «{array => [3 4 6], hash => {five => 5, one => 1, two => 2}}␤»

需要注意的是, 位置参数不允许放在吞噬参数之后:

:(*@args, $last);
# ===SORRY!=== Error while compiling:
# Cannot put required parameter $last after variadic parameters

通常一个吞噬参数会创建一个 Array(或兼容类型), 为每个参数创建一个新的 Scalar 容器, 并将每个参数的值分配给这些 Scalar。如果原始参数也有一个中间的 Scalar, 那么在这个过程中会被绕过, 并且在被调用的函数中不可用。

带魔符的参数总是会对收集的参数施加一个上下文。不带魔符的参数也可以在前面加一个 + 号的情况下吞噬性地使用, 以便与它们开始的任何初始类型一起工作。

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

sub zipi( +zape ) {
    zape.^name => zape
};
say zipi( "Hey "); # OUTPUT: «List => (Hey )␤»
say zipi( 1...* ); # OUTPUT: «Seq => (...)␤»

吞噬参数在与一些特质和修饰符结合时有特殊的行为, 这在吞噬数组参数一节中有所描述。

94.1.4. 吞噬数组参数的类型

吞噬数组参数有三种变化。

单星号形式将传递的参数扁平化。

双星号形式不对参数进行扁平化处理。

加号形式根据单参数规则进行扁平化处理。

每种形式将在接下来的几节中详细介绍。由于每一种形式之间的差异都有细微的差别, 我们为每一种形式提供了一些例子, 以说明每一种扁平化约定与其他约定的不同之处。

扁平化的吞噬

用一个星号声明的吞噬参数将通过溶解一个或多个裸露的 Iterable 层来扁平化参数。

my @array = <a b c>;
my $list := <d e f>;
sub a(*@a)  { @a.raku.say };
a(@array);                 # OUTPUT: «["a", "b", "c"]»
a(1, $list, [2, 3]);       # OUTPUT: «[1, "d", "e", "f", 2, 3]»
a([1, 2]);                 # OUTPUT: «[1, 2]»
a(1, [1, 2], ([3, 4], 5)); # OUTPUT: «[1, 1, 2, 3, 4, 5]»
a(($_ for 1, 2, 3));       # OUTPUT: «[1, 2, 3]»

一个星号吞噬参数就可以将所有给定的迭代对象扁平化, 有效地将任何用逗号创建的对象提升到最高级别。

未扁平化的吞噬参数

用两颗星声明的吞噬参数不会将列表中的任何 Iterable 参数扁平化, 而是大致保持参数的原样。

my @array = <a b c>;
my $list := <d e f>;
sub b(**@b) { @b.raku.say };
b(@array);                 # OUTPUT: «[["a", "b", "c"],]␤»
b(1, $list, [2, 3]);       # OUTPUT: «[1, ("d", "e", "f"), [2, 3]]␤»
b([1, 2]);                 # OUTPUT: «[[1, 2],]␤»
b(1, [1, 2], ([3, 4], 5)); # OUTPUT: «[1, [1, 2], ([3, 4], 5)]␤»
b(($_ for 1, 2, 3));       # OUTPUT: «[(1, 2, 3),]␤»

双星号吞噬参数隐藏了嵌套的逗号对象, 并在吞噬数组中保持原样。

单参数规则 slurpy

一个使用加号创建的吞噬参数会涉及到"单参数规则", 它根据上下文决定如何处理吞噬参数。基本上, 如果只传递了一个参数, 而且这个参数是 Iterable, 那么这个参数就会被用来填充吞噬参数数组。在任何其他情况下, +@ 的工作原理和 **@ 一样。

my @array = <a b c>;
my $list := <d e f>;
sub c(+@b) { @b.raku.say };
c(@array);                 # OUTPUT: «["a", "b", "c"]␤»
c(1, $list, [2, 3]);       # OUTPUT: «[1, ("d", "e", "f"), [2, 3]]␤»
c([1, 2]);                 # OUTPUT: «[1, 2]␤»
c(1, [1, 2], ([3, 4], 5)); # OUTPUT: «[1, [1, 2], ([3, 4], 5)]␤»
c(($_ for 1, 2, 3));       # OUTPUT: «[1, 2, 3]␤»

关于额外的讨论和示例, 请参见函数的吞噬约定

94.1.5. 类型捕获

类型捕获允许将类型约束的规范推迟到函数被调用时。它们允许在签名和函数主体中引用类型。

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

# 第一个参数是 Int 类型, 所以第二个参数也是
# 我们通过调用在 &f 中使用的运算符来推导出第三种类型
my &s = f(10,2, Int.new / Int.new);
say s(2);  # 10 / 2 * 2  == 10

94.1.6. 位置参数 vs. 命名参数

参数可以是位置的, 也可以是命名的。默认情况下, 参数都是位置的, 除了吞噬的哈希和用前导冒号 : 标记的参数。后者被称为冒号对。检查下面的签名以及它们所表示的内容。

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

在调用方, 位置参数的传递顺序与参数的声明顺序相同。

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

在命名参数和形式参数的情况下, 只有名称用于将实参映射到形参。如果使用胖箭头来构造一个 Pair, 那么只有那些以有效标识符为键的参数才会被识别为命名参数。

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

你可以使用一个与命名参数同名的变量来调用这个例程;在这种情况下 : 将被用于调用, 这样变量的名称就被理解为参数的键。

sub named-shortcut( :$shortcut ) {
    say "Looks like $shortcut"
}
named-shortcut( shortcut => "to here"); # OUTPUT: «Looks like to here␤»
my $shortcut = "Þor is mighty";
named-shortcut( :$shortcut );           # OUTPUT: «Looks like Þor is mighty␤»

可以为命名参数取一个与变量名不同的名字。

sub named(:official($private)) { "Official business!" if $private }
named :official;

94.1.7. 参数别名

冒号对语法可以用来提供参数的别名。

sub alias-named(:color(:$colour), :type(:class($kind))) {
    say $colour ~ " " ~ $kind
}
alias-named(color => "red", type => "A");    # both names can be used
alias-named(colour => "green", type => "B"); # more than two names are ok
alias-named(color => "white", class => "C"); # every alias is independent

冒号 : 的存在将决定我们是否创建一个新的命名参数。:$colour 不仅是别名变量的名称, 也是一个新的命名参数(在第二次调用中使用)。然而, $kind 将只是别名变量的名称, 不会创建一个新的命名参数。更多别名的用法可以在 sub MAIN 中找到。

一个带有命名参数的函数可以被动态调用, 用 | 去引用一个 Pair 来将它变成一个命名参数。

multi f(:$named) { note &?ROUTINE.signature };
multi f(:$also-named) { note &?ROUTINE.signature };
for 'named', 'also-named' -> $n {
    f(|($n => rand))                # OUTPUT: «(:$named)␤(:$also-named)␤»
}

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

同样可以用来将 Hash 转换为命名参数。

sub f(:$also-named) { note &?ROUTINE.signature };
my %pairs = also-named => 4;
f |%pairs;                              # OUTPUT: «(:$also-named)␤»

一个包含列表的 Hash 在滑入命名参数时可能会有问题。为了避免额外的容器层, 在滑入之前先强制为 Map

class C { has $.x; has $.y; has @.z };
my %h = <x y z> Z=> (5, 20, [1,2]);
say C.new(|%h.Map);
# OUTPUT: «C.new(x => 5, y => 20, z => [1, 2])␤»

你可以为一个命名参数创建任意数量的别名。

sub alias-named(:color(:$colour),
                :variety(:style(:sort(:type(:class($kind)))))) {
    return $colour ~ " " ~ $kind
}
say alias-named(color => "red", style => "A");
say alias-named(colour => "green", variety => "B");
say alias-named(color => "white", class => "C");

你可以通过将参数作为匿名参数的别名来创建不创建任何变量的命名参数。这在使用命名参数仅仅作为选择 multi 候选者的手段时是很有用的, 例如, 在使用特质时经常会出现这种情况。

# Timestamps calls to a routine.
multi sub trait_mod:<is>(Routine:D $r is raw, :timestamped($)!) {
    $r does my role timestamped { has Instant:D @.timestamps };
    $r.wrap: -> | { ENTER $r.timestamps.push: now; callsame };
}

sub foo is timestamped { }
foo;
say +&foo.?timestamps; # OUTPUT: «1␤»

94.1.8. 可选参数和强制参数

位置参数在默认情况下是强制性的, 也可以用默认值或尾部的问号将其变成可选项。

$ = :(Str $id)         # 强制性参数
$ = :($base = 10)      # 可选参数, 默认值为 10
$ = :(Int $x?)         # 可选参数, 默认为 Int 类型的对象

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

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

默认值可能取决于之前的参数, 并且每次调用都会被重新计算(至少在概念上)。

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

94.1.9. 动态变量

动态变量在签名中是允许的, 尽管它们并没有提供特殊的行为, 因为无论如何, 参数绑定都会连接两个作用域。

94.1.10. 解构参数

非标量参数的后面可以用括号中的子签名来代替, 它将对给出的参数进行解构。列表的解构就是其元素。

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

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

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

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

尖号循环也可以解构散列, 允许赋值给变量。

my %hhgttu = (:40life, :41universe, :42everything);
for %hhgttu -> (:$key, :$value) {
  say "$key → $value";
}
# OUTPUT: «universe → 41␤life → 40␤everything → 42␤»

一般来说, 对象是根据它的属性来解构的。一个常见的用法是在 for 循环中解包一个 Pairkeyvalue

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

然而, 这种将对象解包为其属性的行为只是默认行为。要使一个对象得到不同的解构, 可以改变它的捕获方法。

94.1.11. 子签名

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

sub foo(|c(Int, Str)){
   put "called with {c.raku}"
};
foo(42, "answer");
# OUTPUT: «called with \(42, "answer")»

94.1.12. 长名字

如果要将某些参数排除在多重分派中考虑之外, 可以用双分号将它们分开。

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

94.1.13. 捕获参数

在参数前加上一个竖杠 |, 使得该参数成为一个Capture, 用完所有剩余的位置参数和命名参数。

这通常用于 proto 定义中(如 proto foo (|) {*}), 以表明例程的 multi 定义可以有任何类型约束。参见 proto 的例子。

如果绑定到一个变量上, 可以使用滑入操作符 | 将参数作为一个整体转发。

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)␤»

94.1.14. 参数特性和修饰符

默认情况下, 形参数会被绑定到其实参上, 并标记为只读。我们可以通过形参的特质来改变这一点。

is copy 特性会导致参数被复制, 并允许在例程中修改参数。

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

is rw 特质, 代表的是可读可写, 使得参数绑定到一个变量(或其他可写容器)上。对参数的赋值会改变变量在调用方的值。

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

对于吞噬型参数, is rw 被保留给语言设计者将来使用。

is raw trait 会自动应用于用反斜线声明为"魔符"的参数, 也可以用来使通常有符号的参数像这些参数一样。在吞噬的特殊情况下, 通常会产生一个充满 Scalar 的数组, 如上所述, is raw 会使参数产生一个 List。该 List 中的每个元素将直接作为 raw 参数进行绑定。

要显式地要求一个只读参数, 请使用 is readonly 特质。请注意, 这只适用于容器。里面的对象很可能有突变器方法, 而且 Raku 不会在对象的属性上强制执行不可变性。

特质后面可以跟上 where 子句:

sub ip-expand-ipv6($ip is copy where m:i/^<[a..f\d\:]>**3..39$/) { }

94.2. 方法

94.2.1. params 方法

method params(Signature:D: --> Positional)

返回构成签名的 Parameter 对象的列表。

94.2.2. arity 方法

method arity(Signature:D: --> Int:D)

返回满足签名所需的位置参数的最小数量。

94.2.3. count 方法

method count(Signature:D: --> Real:D)

返回可以绑定到签名的位置参数的最大数量。如果有一个吞噬型的位置参数, 则返回 Inf

94.2.4. returns 方法

不管签名的返回约束是什么:

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

94.2.5. 方法 ACCEPTS

multi method ACCEPTS(Signature:D: Signature $topic)
multi method ACCEPTS(Signature:D: Capture $topic)
multi method ACCEPTS(Signature:D: Mu \topic)

如果 $topic 是一个 Signature, 如果 $topic 接受的东西也会被调用者接受, 则返回 True, 否则返回 False

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

$topic 是一个 Capture, 如果可以绑定到调用者, 返回 True, 也就是说, 如果有调用者的 Signature 的函数可以用 $topic 调用:

\(1, 2, :foo) ~~ :($a, $b, :foo($bar)); # OUTPUT: «True»
\(1, :bar)    ~~ :($a);                 # OUTPUT: «False»

最后, 带有 Mu \topic 的候选者将 topic 转换为 Capture, 并遵循与 Capture $topic 相同的语义:

<a b c d>  ~~ :(Int $a);      # OUTPUT: «False»
42         ~~ :(Int);         # OUTPUT: «False» (Int.Capture throws)
set(<a b>) ~~ :(:$a, :$b);    # OUTPUT: «True»

由于 where 子句是不可反省的, 所以该方法不能确定两个签名是否接受同一种 where 约束参数。这种比较将返回 False。这包括带字面量的签名, 它们只是 where 约束的语法糖。

say :(42) ~~ :($ where 42)    # OUTPUT: «False␤»

94.2.6. 方法 Capture

被定义为:

method Capture()

抛出 X::Cannot::Capture

94.3. 运行时创建 Signature 对象(6.d, 2019.03 及以后版本)

Signature.new(params => (...), returns => Type, arity => 1, count => 1)

在某些情况下, 特别是在使用 MetaObject 协议时, 以编程方式创建 Signature 对象是有意义的。为此, 你可以使用以下命名参数调用 new 方法。

  • params

本签名的 Parameter 对象列表。

  • returns

任何返回值应该匹配的约束条件, 默认为 Mu, 这意味着没有返回值约束检查。

  • arity

满足签名所需的最小位置参数数。默认为用 params 参数给定的 Parameter 对象的数量。

  • count

可以绑定到签名的最大数量的位置参数。如果没有指定, 默认为 arity。如果有一个吞噬型的位置参数, 请指定 Inf

94.4. 类型图

Signature 的类型关系

Signature

95. 类 Supplier::Preserving

class Supplier::Preserving is Supplier { }

这是一个现场 Supply 类型对象的工厂,它提供了向 supplies 发射新值的机制,当没有消费者进入 Supply 时,这些值会被保留下来。任何分接(tapping)都会消耗已经存储的值和未来的值。

启动一个保存的 Supply,并在它完成(done)后消耗它的值:

my $p = Supplier::Preserving.new;
start for ^3 {
    $p.emit($_);
    LAST {
        say „done after { now - BEGIN now}s“;
        $p.done;
    }
}
sleep 2;
react {
    whenever $p.Supply { $_.say; }
    whenever Promise.in(2) { done }
}
say „also done after { now - BEGIN now }s“

会输出:

done after 0.0638467s
0
1
2
also done after 4.0534119s

95.1. 方法

95.1.1. 方法 new

method new()

Supplier 构造函数。

95.2. 类型图

Supplier::Preserving 的类型关系:

Supplier::Preserving

95.3. 由类 Supplier 提供的例程

Supplier::Preserving 继承自类 Supplier,它提供了以下例程:

95.3.1. (Supplier) 方法 new

method new()

Supplier 构造函数。

95.3.2. (Supplier) 方法 Supply

method Supply(Supplier:D: --> Supply)

这将创建一个新的 Supply 对象,在这个 Supply 上发出的任何值都将传递给这个对象。这是所有现场(live)供应的工厂。

95.3.3. (Supplier) 方法 emit

method emit(Supplier:D: Mu \value)

将给定值发送到 Supply 在此 Supplier 上创建的所有供应上的所有 tap

95.3.4. (Supplier) 方法 done

method done(Supplier:D:)

调用所有有 donetap 的回调。

my $supplier = Supplier.new;
my $supply   = $supplier.Supply;
$supply.tap(-> $v { say $v }, done => { say "no more answers" });
$supplier.emit(42);
$supplier.done;

会输出:

42
no more answers

95.3.5. (Supplier) 方法 quit

multi method quit(Supplier:D: Exception $ex)
multi method quit(Supplier:D: Str() $message)

在所有有 tap 的供应上面调用 quit 回调,并将异常传递给他们。如果用 Str 调用,异常将是一个 X::AdHoc,包含提供的消息。

这是为关闭出错的供应(supply)而设计的。

96. 类 Supplier

class Supplier { }

这是一个现场供应(Supply)对象的工厂,它提供了向供应对象发射新值的机制:

my $supplier = Supplier.new;
my $supply_1 = $supplier.Supply;
$supply_1.tap(-> $v { say "One $v" });
my $supply_2 = $supplier.Supply;
$supply_2.tap(-> $v { say "Two $v" });
$supplier.emit(42);

会输出:

One 42
Two 42

按需供应是由 Supply 类的工厂方法或由 supply 关键字创建的。可以用 Supplier::Preserving 创建一个实时和按需供应(Supply)的混合体。

96.1. 方法

96.1.1. 方法 new()

method new

Supplier 构造函数。

96.1.2. 方法 Supply

method Supply(Supplier:D: --> Supply)

这将创建一个新的 Supply 对象,在这个 Supply 上发出的任何值都将传递给这个对象。这是所有实时(live)供应的工厂。

96.1.3. 方法 emit

method emit(Supplier:D: Mu \value)

将给定值发送到 Supply 在此 Supplier 上创建的所有供应上的所有 tap

96.1.4. 方法 done

method done(Supplier:D:)

调用所有有 donetap 的回调。

my $supplier = Supplier.new;
my $supply   = $supplier.Supply;
$supply.tap(-> $v { say $v }, done => { say "no more answers" });
$supplier.emit(42);
$supplier.done;

会输出:

42
no more answers

96.1.5. 方法 quit

multi method quit(Supplier:D: Exception $ex)
multi method quit(Supplier:D: Str() $message)

在所有有 tap 的供应上面调用 quit 回调,并将异常传递给他们。如果用 Str 调用,异常将是一个 X::AdHoc,包含提供的消息。

这是为了在出现错误时关闭供应。

96.2. 类型图

Supplier 的类型关系

Supplier

97. 类 Supply

class Supply {}

供应是一个线程安全的异步数据流,就像 Channel 一样,但它可以有多个订阅者(taps),这些订阅者都可以通过供应获得相同的值。

它是观察者模式的线程安全实现,也是支持 Raku 中反应式编程的核心。

供应有两种类型:实时供应和按需供应。当分接入一个实时供应时,分接只会看到在创建分接(tap)后流经供应的值。这类供应通常是无限的,比如鼠标移动。关闭这样的分接并不会阻止鼠标事件的发生,只是意味着这些值将在看不见的情况下流逝。所有分接者看到的数值流转都是一样的。

在一个按需供应上的分接将启动值的产生,而再次分接供应可能会产生一组新的值。例如,Supply.interval 每次分接都会产生一个具有适当间隔的新计时器。如果分接被关闭,定时器就会停止向该分接发射值。

一个实时的 Supply 是从 Supplier 工厂方法 Supply 中获得的。通过在 Supplier 对象上调用 emit 来发射新的值。

my $supplier = Supplier.new;
my $supply = $supplier.Supply;
$supply.tap(-> $v { say "$v" });
$supplier.emit(42); # Will cause the tap to output "42"

live 方法在实时供应时返回 True。工厂方法如intervalfrom-list 会返回按需供应。

可以使用 Supplier::Preserving 创建一个在第一次分接之前都会保留值的实时供应。

更多的例子可以在并发页面中找到。

97.1. 返回分接的方法

97.1.1. 方法 tap

method tap(Supply:D: &emit = -> $ { },
        :&done,
        :&quit,
        :&tap
    --> Tap:D)

在所有现有的tap之外,创建一个新的tap(可以说是一种订阅)。第一个位置参数是一段代码,它将在通过 emit 调用获得新的值时被调用。

在一些情况下可以调用 &done 回调:如果一个供应块正在被tap,当到达一个 done 例程时;如果一个供应块正在被tap,当供应块到达终点时,它会自动触发;如果在父 Supplier 上调用了 done 方法(在供应块的情况下,如果有多个由 whenever 引用的 Suppliers,它们必须都有它们的 done 方法被调用,这样才能触发tap的 &done 回调,因为此时供应块将到达终点)。

如果tap在一个供应块上,并且该供应块以错误方式退出,则会调用 &quit 回调。如果在父 Supplier 上调用了 quit 方法,它也会被调用(在供应块的情况下,任何一个以未捕获的异常退出的 Supplier 都会调用 &quit 回调,因为该块会以错误退出)。错误将作为参数传递给回调。

Tap 对象创建后,就会调用 &tap 回调,该对象作为参数传递给回调。该回调在 emit/done/quit 之前被调用,提供了一种可靠的方式来获取 Tap 对象。这很有用的一种情况是,当 Supply 开始同步发射值时,因为对 .tap 的调用不会返回 Tap 对象,直到它完成发射,防止它在需要时被停止。

方法 tap 会返回一个 Tap 类型的对象,你可以在这个对象上调用 close 方法来取消订阅。

my $s = Supply.from-list(0 .. 5);
my $t = $s.tap(-> $v { say $v }, done => { say "no more ticks" });

生成:

0
1
2
3
4
5
no more ticks

97.1.2. act

method act(Supply:D: &act --> Tap:D)

用给定的代码在给定的供应上创建一个 tap。与 tap 不同的是,给定的代码保证一次只由一个线程执行。

97.2. 实用方法

97.2.1. 方法 Capture

被定义为:

method Capture(Supply:D: --> Capture:D)

相当于在调用者上调用 .List.Capture

97.2.2. 方法 Channel

method Channel(Supply:D: --> Channel:D)

返回一个 Channel 对象,该对象将接收来自供应的所有未来值,当供应完成时将关闭(close),当供应退出时将退出(错误关闭)。

97.2.3. 方法 Promise

method Promise(Supply:D: --> Promise:D)

返回 Supply 完成后将被保留的 Promise。如果 Supply 也发出任何值,那么 Promise 将保留最终值。否则,将保留 Nil 值。如果 Supplyquit 而不是 done 结束,那么 Promise 将以该异常情况被打破。

my $supplier = Supplier.new;
my $s = $supplier.Supply;
my $p = $s.Promise;
$p.then(-> $v { say "got $v.result()" });
$supplier.emit('cha');         # not output yet
$supplier.done();              # got cha

当处理倾向于只产生一个值的供应时,当只有最终值值得关注时,或者当只有完成(成功或失败)才是相关的时,Promise 方法是最有用的。

97.2.4. 方法 live

method live(Supply:D: --> Bool:D)

如果供应是"实时"的,也就是说,值一到手就会被发送至tap,则返回 True。在默认的 Supply 中总是返回 True(但是在 Supply.from-list 返回的 Supply 中则是 False)。

say Supplier.new.Supply.live;    # OUTPUT: «True␤»

97.2.5. schedule-on

method schedule-on(Supply:D: Scheduler $scheduler)

在指定的调度器上运行 emitdonequit 回调。

这对于需要从 GUI 线程中运行某些操作的 GUI 工具包来说很有用。

97.3. 等待直到供应完成的方法

97.3.1. 方法 wait

method wait(Supply:D:)

分接被调用的 Supply,并阻塞执行,直到 Supply 完成(在这种情况下,它计算到 Supply 上发出的最终值,如果没有发出值,则为 Nil)或 quit(在这种情况下,它将抛出传递给 quit 的异常)。

my $s = Supplier.new;
start {
  sleep 1;
  say "One second: running.";
  sleep 1;
  $s.emit(42);
  $s.done;
}
$s.Supply.wait;
say "Two seconds: done";

97.3.2. 方法 list

method list(Supply:D: --> List:D)

分接被调用的 Supply,并返回一个懒惰列表,该列表将在 Supply 发出值时被重新整理。一旦 Supply 完成(done),该列表将被终止。如果 Supply 退出(quit),那么一旦到达懒惰列表中的那个点,就会抛出一个异常。

97.3.3. 方法 grab

method grab(Supply:D: &when-done --> Supply:D)

分接被调用的 Supply,当它完成后,调用 &when-done,然后发出在结果 Supply 上返回的值列表。如果原来的 Supply 退出(quit),那么在返回的 Supply 上立即传达异常。

my $s = Supply.from-list(4, 10, 3, 2);
my $t = $s.grab(&sum);
$t.tap(&say);           # OUTPUT: «19␤»

97.3.4. 方法 reverse

method reverse(Supply:D: --> Supply:D)

分接被调用的 Supply。一旦该 Supply 发出 done,它所发出的所有值将以相反的顺序在返回的 Supply 上发出。如果原来的 Supply 退出了(quit),那么异常会立即在返回的 Supply 上传达。

my $s = Supply.from-list(1, 2, 3);
my $t = $s.reverse;
$t.tap(&say);           # OUTPUT: «3␤2␤1␤»

97.3.5. 方法 sort

method sort(Supply:D: &custom-routine-to-use? --> Supply:D)

分接它所调用的 Supply。一旦该 Supply 发出 done,它发出的所有值将被排序,并按排序顺序在返回的 Supply 上发出结果。可选择接受一个比较器 Block。如果原来的 Supply 退出(quit),那么在返回的 Supply 上会立即传达异常。

my $s = Supply.from-list(4, 10, 3, 2);
my $t = $s.sort();
$t.tap(&say);           # OUTPUT: «2␤3␤4␤10␤»

97.3.6. 方法 collate

method collate(Supply:D:)

分接它被调用的 Supply。一旦该 Supply 发射完成(done),它发射的所有值将根据 Unicode 字形特征进行排序。一个新的 Supply 将带着排序后的值返回。关于整理排序的更多细节,请参见 Any.collate

my $s = Supply.from-list(<ä a o ö>);
my $t = $s.collate();
$t.tap(&say);           # OUTPUT: «a␤ä␤o␤ö␤»

97.4. 返回另一个 Supply 的方法

97.4.1. 方法 from-list

method from-list(Supply:U: +@values --> Supply:D)

从传递给本方法的值中创建一个按需供应。

my $s = Supply.from-list(1, 2, 3);
$s.tap(&say);           # OUTPUT: «1␤2␤3␤»

97.4.2. 方法 share

method share(Supply:D: --> Supply:D)

从一个按需供应中创建一个实时供应,从而可以在多个tap上共享按需供应的值,而不是每个tap都看到自己的按需供应的所有值的副本。

# this says in turn: "first 1" "first 2" "second 2" "first 3" "second 3"
my $s = Supply.interval(1).share;
$s.tap: { "first $_".say };
sleep 1.1;
$s.tap: { "second $_".say };
sleep 2

97.4.3. 方法 flat

method flat(Supply:D: --> Supply:D)

创建一个供应,在给定供应中看到的所有值在被再次发射之前都被扁平化。

97.4.4. 方法 do

method do(Supply:D: &do --> Supply:D)

创建一个供应,所有在给定供应中看到的值,都会再次发出。给定的代码,只为其副作用执行,保证一次只由一个线程执行。

97.4.5. 方法 on-close

method on-close(Supply:D: &on-close --> Supply:D)

返回一个新的供应,每当该供应的一个分接(Tap)关闭时,该供应就会运行 &on-close。这包括是否有更多的操作链接到供应上。例如,$supply.on-close(&on-close).map(*.uc))。当使用 reactsupply 块时,使用 CLOSE 相位器通常是更好的选择。

my $s = Supplier.new;
my $tap = $s.Supply.on-close({ say "Tap closed" }).tap(
    -> $v { say "the value is $v" },
    done    => { say "Supply is done" },
    quit    => -> $ex { say "Supply finished with error $ex" },
);

$s.emit('Raku');
$tap.close;        # OUTPUT: «Tap closed␤»

97.4.6. 方法 interval

method interval(Supply:U: $interval, $delay = 0, :$scheduler = $*SCHEDULER --> Supply:D)

创建一个供应,每隔 $interval 秒发射一个值,从调用后的 $delay 秒开始。发射的值是一个整数,从0开始,每发射一个值就递增一个。

实现可能会把太小的值和负值当作他们支持的最低分辨率,在这种情况下可能会发出警告;例如把 0.0001 当作 0.001。对于 6.d 语言版本,指定的最小值是 0.001

97.4.7. 方法 grep

method grep(Supply:D: Mu $test --> Supply:D)

创建一个新的供应,该供应仅从原来的供应中发出那些与 $test 进行智能匹配的值。

my $supplier = Supplier.new;
my $all      = $supplier.Supply;
my $ints     = $all.grep(Int);
$ints.tap(&say);
$supplier.emit($_) for 1, 'a string', 3.14159;   # prints only 1

97.4.8. 方法 map

method map(Supply:D: &mapper --> Supply:D)

返回一个新的供应,通过 &mapper 映射给定供应的每个值,并将其发射到新的供应。

my $supplier = Supplier.new;
my $all      = $supplier.Supply;
my $double   = $all.map(-> $value { $value * 2 });
$double.tap(&say);
$supplier.emit(4);           # OUTPUT: «8»

97.4.9. 方法 batch

method batch(Supply:D: :$elems, :$seconds --> Supply:D)

创建一个新的供应,将给定供应的值按批次中的元素数(使用 :elems)或最大秒数(使用 :seconds)或两者之一进行分批。当供应完成后,任何剩余的值都会在最后一批中发出。

97.4.10. 方法 elems

method elems(Supply:D: $seconds? --> Supply:D)

创建一个新的供应,在这个供应中,所见数值的变化会被发出。如果你只想每隔几秒钟更新一次,它还可以选择使用一个时间间隔(秒)。

97.4.11. 方法 head

method head(Supply:D: Int(Cool) $number = 1 --> Supply:D)

创建一个与 List.head 相同语义的 "head" 供应。

my $s = Supply.from-list(4, 10, 3, 2);
my $hs = $s.head(2);
$hs.tap(&say);           # OUTPUT: «4␤10␤»

97.4.12. 方法 tail

method tail(Supply:D: Int(Cool) $number = 1 --> Supply:D)

创建一个与 List.tail 相同语义的 "tail" 供应。

my $s = Supply.from-list(4, 10, 3, 2);
my $ts = $s.tail(2);
$ts.tap(&say);           # OUTPUT: «3␤2␤»

97.4.13. 方法 first

method first(Supply:D: :$end, |c)

本方法从调用调用者的 grep 方法创建的供应中创建第一个元素的供应,如果可选的命名参数 :end 是 truthy,则创建最后一个元素的供应,剩余的参数作为参数。如果没有剩余的参数,本方法相当于在调用者上,在没有参数的情况下,根据命名参数 :end 调用 headtail 方法。

my $rand = supply { emit (rand × 100).floor for ^∞ };
my $first-prime = $rand.first: &is-prime;
# output the first prime from the endless random number supply $rand,
# then the $first-prime supply reaches its end
$first-prime.tap: &say;

97.4.14. 方法 rotor

method rotor(Supply:D: @cycle --> Supply:D)

创建一个与 List.rotor 具有相同语义的 "rotoring" 供应。

97.4.15. 方法 delayed

method delayed(Supply:D: $seconds, :$scheduler = $*SCHEDULER --> Supply:D)

创建一个新的供应,其中所有流经给定供应的值都会被发射,但给定的延迟以秒为单位。

97.4.16. 方法 throttle

method throttle(Supply:D:
  $limit,                 # values / time or simultaneous processing
  $seconds or $callable,  # time-unit / code to process simultaneously
  $delay = 0,             # initial delay before starting, in seconds
  :$control,              # supply to emit control messages on (optional)
  :$status,               # supply to tap status messages from (optional)
  :$bleed,                # supply to bleed messages to (optional)
  :$vent-at,              # bleed when so many buffered (optional)
  :$scheduler,            # scheduler to use, default $*SCHEDULER
  --> Supply:D)

从一个给定的供应中产生一个供应,但要确保通过的信息数量是有限的。

它有两种操作模式:按时间单位或按一个代码块的最大执行次数:这由第二个位置参数决定。

第一个位置参数指定了应该应用的限制。

如果第二个位置参数是一个 Callable,那么限制表示执行 Callable 的最大并行进程数,该值由接收到的值给出。在这种情况下,发出的值将是启动 Callable 获得的 Promise

如果第二个位置参数是一个数值,则解释为时间单位(以秒为单位)。如果你指定 .1 作为值,那么它确保每十分之一秒都不会超过限制。

如果超过了限制,那么传入的消息会被缓冲,直到有空间再次传递/执行 Callable

第三个位置参数是可选的:它表示节流阀在传递任何值之前将等待的秒数。

:control 命名参数可以选择指定一个 Supply,当节流阀运行时,你可以用它来控制它。可以发送的消息,是 "key:value" 形式的字符串。请看下面的消息类型,你可以发送以控制节流阀。

:status 命名参数可以选择指定一个将接收任何状态消息的 Supply。如果指定了,它将在原始 Supply 用完后至少发送一条状态消息。参见下面的状态消息

:bleed 命名参数选择性地指定了一个供应,该供应将接收任何显式放水(通过放水控制消息)或自动放水(如果有一个 vent-at 激活)的值。

:vent-at 命名参数表示在任何附加值被路由到 :bleed Supply 之前可能被缓冲的值的数量。如果没有指定,默认为0(不会导致自动出血)。只有当指定了 :bleed Supply 时才有意义。

:scheduler 命名参数表示要使用的调度器。默认值为 $*SCHEDULER

控制消息

这些消息可以发送到 :control 供应。控制消息由 "key: value" 形式的字符串组成,例如 "limit: 4"。

  • limit

将信息的数量(如第一位置条件中最初给定的)改为给定的值。

  • bleed

将给定数量的缓冲消息路由到 :bleed Supply。

  • vent-at

改变自动出血前缓冲值的最大数量。如果该值比以前低,将导致缓冲值立即重新路由以符合新的最大值。

  • status

用给定的 id 向 :status Supply 发送状态消息。

状态消息

状态返回信息是一个包含以下键的哈希值。

  • allowed

当前仍允许传递/执行的信息/可调用信息的数量。

  • bled

路由到 :bleed 供应的信息数量。

  • buffered

当前因溢出而被缓冲的消息数量。

  • emitted

发出(通过)的信息数量。

  • id

这个状态信息的 id(单调递增的数字)。如果你想记录状态信息,很方便。

  • limit

正在应用的当前限制。

  • vent-at

在自动重新路由到 :blooded Supply 之前可能被缓冲的消息的最大数量。

例子

有一段简单的代码,当它开始异步运行时宣布,随机等待一段时间,然后当它完成时宣布。这样做6次,但不要让超过3次的代码同时运行。

my $s = Supply.from-list(^6);  # set up supply
my $t = $s.throttle: 3,        # only allow 3 at a time
{                              # code block to run
    say "running $_";          # announce we've started
    sleep rand;                # wait some random time
    say "done $_"              # announce we're done
}                              # don't need ; because } at end of line
$t.wait;                       # wait for the supply to be done

而一次运行的结果将是:

running 0
running 1
running 2
done 2
running 3
done 1
running 4
done 4
running 5
done 0
done 3
done 5

97.4.17. 方法 stable

method stable(Supply:D: $time, :$scheduler = $*SCHEDULER --> Supply:D)

创建一个新的供应,只有在给定的 $time(以秒为单位)内没有被另一个值取代的情况下,才会传递流经给定供应的值。使用 :scheduler 参数,可以选择使用默认调度器以外的另一个调度器。

为了澄清上面的问题,如果在超时的 $time 内,除了最后一个值之外,所有发送到 Supplier 的附加值都会被扔掉。在超时期间,每次向 Supplier 发送额外的值时,$time 都会被重置。

这个方法在处理 UI 输入时非常有用,因为它不希望在用户停止输入一段时间后才执行操作,而不是在每次击键时才执行。

my $supplier = Supplier.new;
my $supply1 = $supplier.Supply;
$supply1.tap(-> $v { say "Supply1 got: $v" });
$supplier.emit(42);

my Supply $supply2 = $supply1.stable(5);
$supply2.tap(-> $v { say "Supply2 got: $v" });
sleep(3);
$supplier.emit(43);  # will not be seen by $supply2 but will reset $time
$supplier.emit(44);
sleep(10);
# OUTPUT: «Supply1 got: 42␤Supply1 got: 43␤Supply1 got: 44␤Supply2 got: 44␤»

从上面可以看出,$supply1 收到了所有向 Supplier 发出的值,而 $supply2 只收到一个值。43 之所以被扔掉,是因为后面还有一个"最后的"值 44,这个值被保留了下来,并在大约8秒后发送给 $supply2,这是因为超时 $time 在3秒后被重置。

97.4.18. 方法 reduce

method reduce(Supply:D: &with --> Supply:D)

创建一个与 List.reduce 相同语义的 "reducing" 供应。

my $supply = Supply.from-list(1..5).reduce({$^a + $^b});
$supply.tap(-> $v { say "$v" }); # OUTPUT: «15␤»

97.4.19. 方法 produce

method produce(Supply:D: &with --> Supply:D)

创建一个与 List.produce 相同语义的"生产"供应。

my $supply = Supply.from-list(1..5).produce({$^a + $^b});
$supply.tap(-> $v { say "$v" }); # OUTPUT: «1␤3␤6␤10␤15␤»

97.4.20. 方法 lines

method lines(Supply:D: :$chomp = True --> Supply:D)

创建一个供应,它将从通常由某些异步 I/O 操作创建的供应中逐行发出字符。可选的 :chomp 参数表示是否要删除行分隔符:默认为 True

97.4.21. 方法 words

method words(Supply:D: --> Supply:D)

创建一个供给,它将从通常由一些异步 I/O 操作创建的供应中一个字一个字地发射进来的字符。

my $s = Supply.from-list("Hello Word!".comb);
my $ws = $s.words;
$ws.tap(&say);           # OUTPUT: «Hello␤Word!␤»

97.4.22. 方法 unique

method unique(Supply:D: :$as, :$with, :$expires --> Supply:D)

根据可选的 :as:with 参数的定义,创建一个只提供唯一值的供应(与 unique 相同)。可选的 :expires 参数在"重置"之前需要等待多长时间(以秒为单位),即使是与旧值相同的值,也不认为它已经被看到。

97.4.23. 方法 repeated

method repeated(Supply:D: :&as, :&with)

创建一个只提供重复值的供应,这些重复值由可选的 :as:with 参数定义(与 unique 相同)。

my $supply = Supply.from-list(<a A B b c b C>).repeated(:as(&lc));
$supply.tap(&say);           # OUTPUT: «A␤b␤b␤C␤»

更多使用其 sub 形式的例子请参见 repeated

注:自 6.e 版本起可用(Rakudo 2020.01 及以后版本)。

97.4.24. 方法 squish

method squish(Supply:D: :$as, :$with --> Supply:D)

创建一个只提供唯一值的供应,由可选的 :as:with 参数定义(与 squish 相同)。

97.4.25. 方法 max

method max(Supply:D: &custom-routine-to-use = &infix:<cmp> --> Supply:D)

创建一个供应,只有当给定供应的值大于之前看到的任何值时,才会从给定供应中发射。换句话说,从一个连续上升的供应中,它将发射所有的值,从一个连续下降的供应中,它将只发射第一个值。从一个连续递减的供应中,它将只发射第一个值。可选参数指定比较器,就像 Any.max 一样。

97.4.26. 方法 min

method min(Supply:D: &custom-routine-to-use = &infix:<cmp> --> Supply:D)

创建一个供应,只有当给定供应的值小于之前看到的任何值时,才会发出这些值。换句话说,从一个连续递减的供应中,它将发射所有的值。从一个连续上升的供应中,它将只发射第一个值。可选参数指定比较器,就像 Any.min 一样。

97.4.27. 方法 minmax

method minmax(Supply:D: &custom-routine-to-use = &infix:<cmp> --> Supply:D)

创建一个供应,每当从给定供应中看到一个新的最小值或最大值时,该供应就会发出一个 Range。可选参数指定比较器,就像 Any.minmax 一样。

97.4.28. 方法 skip

method skip(Supply:D: Int(Cool) $number = 1 --> Supply:D)

返回一个新的 Supply,该 Supply 将从给定的 Supply 中发出所有的值,但前几个 $number 值除外,前几个值将被丢弃。

my $supplier = Supplier.new;
my $supply = $supplier.Supply;
$supply = $supply.skip(3);
$supply.tap({ say $_ });
$supplier.emit($_) for 1..10; # OUTPUT: «4␤5␤6␤7␤8␤9␤10␤»

97.4.29. 方法 start

method start(Supply:D: &startee --> Supply:D)

创建一个供应的供应。对于原始供应中的每个值,代码对象被调度到另一个线程上,并返回一个供应,或者是一个单一的值(如果代码成功),或者是一个没有值的退出(如果代码失败)。

这对于异步启动你不阻塞的工作很有用。

使用 migrate 将这些值再次加入到一个单一的供应中。

97.4.30. 方法 migrate

method migrate(Supply:D: --> Supply:D)

将一个本身具有 Supply 类型的值的 Supply 作为输入。每当外部 Supply 发出一个新的 Supply 时,这个 Supply 就会被分接并发出它的值。任何之前被分接的 Supply 将被关闭。这对于在不同的数据源之间迁移,并且只关注最新的数据源是很有用的。

例如,想象一个应用程序,用户可以在不同的股票之间切换。当他们切换到一个新的股票时,会建立一个连接到 web 套接字以获取最新的值,而之前的任何连接都应该被关闭。每一个从 web socket 过来的数值流都会被表示为一个 Supply,而这些数值流本身也会被排放到一个 Supply 的最新数据源中去观看。migrate 方法可以用来将这个 Supply 平铺到用户关心的当前值的单一 Supply 中。

下面是这样一个程序的简单模拟:

my Supplier $stock-sources .= new;

sub watch-stock($symbol) {
    $stock-sources.emit: supply {
        say "Starting to watch $symbol";
        whenever Supply.interval(1) {
            emit "$symbol: 111." ~ 99.rand.Int;
        }
        CLOSE say "Lost interest in $symbol";
    }
}

$stock-sources.Supply.migrate.tap: *.say;

watch-stock('GOOG');
sleep 3;
watch-stock('AAPL');
sleep 3;

它产生的输出,如下所示:

Starting to watch GOOG
GOOG: 111.67
GOOG: 111.20
GOOG: 111.37
Lost interest in GOOG
Starting to watch AAPL
AAPL: 111.55
AAPL: 111.6
AAPL: 111.6

97.5. 合并供应的方法

97.5.1. 方法 merge

method merge(Supply @*supplies --> Supply:D)

创建一个供应,从给定的供应中看到的任何值,都会被发射出去。产生的供应只有在所有给定的供应都完成时才会被执行。也可以作为类方法调用。

97.5.2. 方法 zip

method zip(Supply @*supplies, :&with = &[,] --> Supply:D)

创建一个供应,当所有供应上出现新的值时,该供应就会发出组合值。默认情况下,会创建 List,但可以通过指定你自己的 :with 参数来改变。一旦任何一个给定的供应被完成,就会立即完成结果的供应。也可以作为类方法调用。

97.5.3. 方法 zip-latest

method zip-latest(Supply @*supplies, :&with = &[,], :$initial --> Supply:D)

创建一个供应,当任何一个供应上出现新的值时,该供应就会发出组合值。默认情况下,创建的是 List,但可以通过使用 :with 参数指定你自己的组合器来改变。可选的 :initial 参数可以用来指示组合值的初始状态。默认情况下,所有的供应都必须在其上至少有一个值发出,然后才会在结果供应上发出第一个组合值。只要任何一个给定的供应完成,结果供应就会完成。也可以作为类方法调用。

97.6. 作为供应暴露的 I/O 功能

97.6.1. sub signal

sub signal(*@signals, :$scheduler = $*SCHEDULER)

为指定的信号枚举(如 SIGINT)和可选的 scheduler 参数创建一个供给。任何接收到的信号,都会在供应端发出。例如

signal(SIGINT).tap( { say "Thank you for your attention"; exit 0 } );

会抓住 Control-C,谢谢,然后退出。

要从信号号转到信号,可以这样做:

signal(Signal(2)).tap( -> $sig { say "Received signal: $sig" } );

支持的信号列表可以通过检查 Signal::.keys 找到(就像检查任何枚举一样)。关于枚举工作方式的更多细节,请参见枚举

注意:Rakudo 在 2018.05 之前的版本有一个 bug,由于这个 bug,信号的数值在某些系统上是不正确的。例如,Signal(10) 正在返回 SIGBUS,即使它在特定系统上实际上是 SIGUSR1。也就是说,除了 2018.04、2018.04.1 和 2018.05 之外,使用 signal(SIGUSR1) 在所有 Rakudo 版本上都能按预期工作,在这些版本上可以通过使用 signal(SIGBUS) 来代替实现预期行为。这些问题在 2018.05 之后的 Rakudo 版本中得到了解决。

97.6.2. 方法 IO::Notification.watch-path

method watch-path($path --> Supply:D)

创建一个供应,操作系统将向其发射值,以指示给定路径的文件系统变化。也有一个在 IO 对象上使用 watch 方法的快捷方式,就像这样:

IO::Notification.watch-path(".").act( { say "$^file changed" } );
".".IO.watch.act(                     { say "$^file changed" } );   # same

97.7. 类型图

供应的类型图

Supply

98. 类 Tap

Subscription to a Supply

class Tap {}

Tap 是对供应的订阅。

my $s = Supplier.new;
my $tap = $s.Supply.on-close({ say "Tap closed" }).tap(
    -> $v { say "the value is $v" },
    done    => { say "Supply is done" },
    quit    => -> $ex { say "Supply finished with error $ex" },
);

# later
$tap.close;

98.1. 方法

98.1.1. 方法 close

method close(Tap:D:)

关闭 tap。

98.2. 类型图

Tap 的类型关系

Tap

99. 类 Whatever

Placeholder for the value of an unspecified argument

class Whatever { }

Whatever 是一个其对象没有任何明确意义的类;它的语义来自于接受 Whatever 对象作为做某些特殊事情的标记的其他例程。使用 * 字面值作为操作数,就会创建一个 Whatever 对象。

* 的大部分魅力来自于 Whatever-currying。当 * 被用在项的位置,也就是作为操作数,与大多数操作符结合使用时,编译器会将表达式转化为类型为 WhateverCode 的闭包,它实际上是一个 Block,可以在接受 Callables 的地方使用。

my $c = * + 2;          # same as   -> $x { $x + 2 };
say $c(4);              # OUTPUT: «6␤»

一个表达式中的多个 * 会生成具有同样多参数的闭包。

my $c = * + *;          # same as   -> $x, $y { $x + $y }

在复杂表达式中使用 * 也会产生闭包。

my $c = 4 * * + 5;      # same as   -> $x { 4 * $x + 5 }

* 上调用方法也会产生一个闭包。

<a b c>.map: *.uc;      # same as    <a b c>.map: -> $char { $char.uc }

如前所述,并不是所有的操作符和语法结构都会将 (或 Whatever-stars)柯里化成 WhateverCode。在以下情况下, 仍将是一个 Whatever 对象。

例外

例子

它的作用

逗号

1, *, 2

生成一个带有 * 元素的列表

范围操作符

1 .. *

Range.new(:from(1), :to(*));

序列操作符

1 …​ *

无限列表

赋值

$x = *

把 * 赋值给 $x

绑定

$x := *

把 * 绑定到 $x

列表重复

1 xx *

生成一个无限列表

范围运算符被特殊处理了。 它们与 Whatever-stars 不发生柯里化, 但是它们与 WhateverCode 发生柯里化。

say (1..*).^name;       # OUTPUT: «Range␤»
say ((1..*-1)).^name;   # OUTPUT: «WhateverCode␤»

这使得所有这些构造都能正常工作:

.say for 1..*;          # infinite loop

my @a = 1..4;
say @a[0..*];           # OUTPUT: «(1 2 3 4)␤»
say @a[0..*-2];         # OUTPUT: «(1 2 3)␤»

因为 Whatever-currying 是一种纯粹的语法编译器转换,所以你不会得到运行时将存储的 Whatever-star 编译成 WhateverCode

my $x = *;
$x + 2;   # Not a closure, dies because it can't coerce $x to Numeric
CATCH { default { put .^name, ': ', .Str } };
# OUTPUT: «X::Multi::NoMatch: Cannot resolve caller Numeric(Whatever: );
# none of these signatures match:␤
# (Mu:U \v: *%_)»

存储 Whatever-star 的用例涉及到上面提到的那些柯里化例外情况。例如,如果你想要一个默认的无限序列。

my $max    = potential-upper-limit() // *;
my $series = known-lower-limit() ... $max;

在智能匹配的特定情况下,存储的 也会导致生成一个 WhateverCode。需要注意的是,这其实并不是存储的 被柯里化了,而是左手边的 *

my $constraint           = find-constraint() // *;
my $maybe-always-matcher = * ~~ $constraint;

如果这个假设的 find-constraint 没有找到任何约束,$maybe-always-matcher 对任何事情都会评估为 True

$maybe-always-matcher(555);      # True
$maybe-always-matcher(Any);      # True

HyperWhatever 的功能与 Whatever 类似,只是它指的是多个值,而不是一个值。

99.1. 方法

99.1.1. 方法 ACCEPTS

multi method ACCEPTS(Whatever:D: Mu $other)
multi method ACCEPTS(Whatever:U: Mu $other)

如果 invocant 是一个实例,总是返回 True。如果 invocant 是一个类型对象,则执行类型检查。

say 42 ~~ (*);       # OUTPUT: «True␤»
say 42 ~~ Whatever;  # OUTPUT: «False␤»

99.1.2. 方法 Capture

被定义为:

method Capture()

抛出 X::Cannot::Capture

99.2. 类型图

Whatever 的类型关系

Whatever

100. 类 WhateverCode

Code object constructed by Whatever-currying

class WhateverCode is Code { }

WhateverCode 对象是 Whatever 柯里化的结果。详情请看 Whatever 文档。

当你想控制一个方法或函数如何解释任何 Whatever star 时,你可以使用带有 WhateverWhateverCode 参数的多重分派来实现,就像下面的例子一样。

class Cycle {
      has $.pos;
      has @.vals;
}

multi sub get-val(Cycle $c, Int $idx) {
      $c.vals[$idx % $c.vals.elems]
}

# Define what to do with a stand-alone * as the second argument
multi sub get-val(Cycle $c, Whatever $idx) {
    get-val($c, $c.pos);
}

# Define what to do with a * WhateverCode in an expression
multi sub get-val(Cycle $c, WhateverCode $idx) {
    get-val($c, $idx($c.pos));
}

my Cycle $c .= new(:pos(2), :vals(0..^10));

say get-val($c, 3);   # OUTPUT: «3␤»
say get-val($c, *);   # OUTPUT: «2␤»
say get-val($c, *-1); # OUTPUT: «1␤»

WhateverCode does Callable 角色,所以应该可以内省出它所包含的 Callable 的类型;例如,继续前面的例子,我们可以通过检查签名来添加一个 multi,处理一个有两个参数的 WhateverCode

# Define what to do with two * in an expression
multi sub get-val(Cycle $c, WhateverCode $idx where { .arity == 2 }) {
    get-val($c, $idx($c.pos, $c.vals.elems));
}

say get-val($c, * + * div 2); # 2 + 10/2 = 7

不过请注意,子表达式可以利用自己的 Whatever star 规则。

my @a = (0, 1, 2);
say get-val($c, @a[*-1]) # 2, because the star belongs to the Array class

这可能会使 Whatever star 的所有权变得相当快的混乱,所以要注意不要过度。

你可以使用 Callable 类型进行类型约束,以便接受任何 Callable,包括 WhateverCode

sub run-with-rand (Callable $code) { $code(rand) };
run-with-rand *.say;           # OUTPUT: «0.773672071688484␤»
run-with-rand {.say};          # OUTPUT: «0.38673179353983␤»
run-with-rand sub { $^v.say }; # OUTPUT: «0.0589543603685792␤»

&-sigiled 参数进行类型约束,效果同样不错,而且更容易输入。

sub run-with-rand (&code) { code time };

100.1. 类型图

WhateverCode 的类型关系

WhateverCode

100.2. 由类 Code 提供的例程

WhateverCode 继承自类 Code,它提供了以下例程:

100.2.1. (Code) 方法 ACCEPTS

multi method ACCEPTS(Code:D: Mu $topic)

通常调用代码对象并传递 $topic 作为参数。但是,当对一个不接受参数的代码对象进行调用时,代码对象被调用时没有参数,$topic 被丢弃。调用的结果会被返回。

100.2.2. (Code) 方法 arity

被定义为:

method arity(Code:D: --> Int:D)

返回为了调用代码对象而必须传递的最小位置参数数。代码对象 Signature 中的任何可选参数或吞噬参数都不作贡献,命名参数也不作贡献。

sub argless() { }
sub args($a, $b?) { }
sub slurpy($a, $b, *@c) { }
say &argless.arity;             # OUTPUT: «0␤»
say &args.arity;                # OUTPUT: «1␤»
say &slurpy.arity;              # OUTPUT: «2␤»

100.2.3. (Code) 方法 assuming

method assuming(Callable:D $self: |primers)

返回一个 Callable,该 Callable 实现了与原始 Callable 相同的行为,但传递给 .assuming 的值已经绑定到相应的参数。

my sub slow($n){ my $i = 0; $i++ while $i < $n; $i };

# takes only one parameter and as such wont forward $n
sub bench(&c){ c, now - ENTER now };

say &slow.assuming(10000000).&bench; # OUTPUT: «(10000000 7.5508834)␤»

对于参数数量(arity)大于1的 sub,你可以用 Whatever * 来表示所有没有"假定"的位置参数。

sub first-and-last ( $first, $last ) {
    say "Name is $first $last";
}

my &surname-smith = &first-and-last.assuming( *, 'Smith' );

&surname-smith.( 'Joe' ); # OUTPUT: «Name is Joe Smith␤»

您可以处理假定和非假定位置参数的任意组合。

sub longer-names ( $first, $middle, $last, $suffix ) {
    say "Name is $first $middle $last $suffix";
}

my &surname-public = &longer-names.assuming( *, *, 'Public', * );

&surname-public.( 'Joe', 'Q.', 'Jr.'); # OUTPUT: «Name is Joe Q. Public Jr.␤»

命名参数也可以是假定的。

sub foo { say "$^a $^b $:foo $:bar" }
&foo.assuming(13, :42foo)(24, :72bar); # OUTPUT: «13 24 42 72␤»

而且你可以在 Callables 的所有类型上使用 .assuming,包括方法

# We use a Whatever star for the invocant:
my &comber = Str.^lookup('comb').assuming: *, /P \w+/;
say comber 'Perl is awesome! Python is great! And PHP is OK too';
# OUTPUT: «(Perl Python PHP)␤»

my &learner = {
    "It took me $:months months to learn $^lang"
}.assuming: 'Raku';
say learner :6months;  # OUTPUT: «It took me 6 months to learn Raku␤»

100.2.4. (Code) 方法 count

被定义为:

method count(Code:D: --> Real:D)

返回调用代码对象时可以传递的位置参数的最大数量。对于可以接受任意数量的位置参数的代码对象(也就是说,它们有一个吞噬(slurpy)参数),count 将返回 Inf。命名参数不作贡献。

sub argless() { }
sub args($a, $b?) { }
sub slurpy($a, $b, *@c) { }
say &argless.count;             # OUTPUT: «0␤»
say &args.count;                # OUTPUT: «2␤»
say &slurpy.count;              # OUTPUT: «Inf␤»

100.2.5. (Code) 方法 of

被定义为:

method of(Code:D: --> Mu)

返回 Code返回类型约束

say -> () --> Int {}.of; # OUTPUT: «(Int)␤»

100.2.6. (Code) 方法 signature

被定义为:

multi method signature(Code:D: --> Signature:D)

返回该代码对象的 Signature 对象,该对象描述其参数。

sub a(Int $one, Str $two) {};
say &a.signature; # OUTPUT: «(Int $one, Str $two)␤»

100.2.7. (Code) 方法 cando

method cando(Capture $c)

返回一个可与给定 Capture 一起调用的候选者列表。因为 Code 对象没有任何多重分派,所以要么返回一个包含对象的列表,要么返回一个空列表。

my $single = \'a';         # a single argument Capture
my $plural = \('a', 42);   # a two argument Capture
my &block = { say $^a };   # a Block object, that is a subclass of Code, taking one argument
say &block.cando($single); # OUTPUT: «(-> $a { #`(Block|94212856419136) ... })␤»
say &block.cando($plural); # OUTPUT: «()␤»

100.2.8. (Code) 方法 Str

被定义为:

multi method Str(Code:D: --> Str:D)

将输出方法名,但也会产生一个警告。使用 .raku.gist 代替。

sub marine() { }
say ~&marine;
# OUTPUT: «Sub object coerced to string (please use .gist or .raku to do that)␤marine␤»
say &marine.Str;
# OUTPUT: «Sub object coerced to string (please use .gist or .raku to do that)␤marine␤»
say &marine.raku; # OUTPUT: «sub marine { #`(Sub|94280758332168) ... }␤»

100.2.9. (Code) 方法 file

被定义为:

method file(Code:D: --> Str:D)

返回声明代码对象的文件名。

say &infix:<+>.file;

100.2.10. (Code) 方法 line

被定义为:

method line(Code:D: --> Int:D)

返回声明该代码对象的行号。

say &infix:<+>.line;

100.2.11. (Code) 方法 is-implementation-detail

method is-implementation-detail(--> False)

注意:这个特性从 2020.05 版本开始在 Rakudo 编译器中使用。

如果代码对象被标记为 is implementation-detail 特质,则返回 True,否则返回 False

100.3. 由角色 Callable 提供的例程

WhateverCode 继承自类 Code,它遵守的角色是 Callable,它提供了以下例程。

100.3.1. (Callable) 方法 CALL-ME

method CALL-ME(Callable:D $self: |arguments)

这个方法是 postfix:«( )»postfix:«.( )» 所需要的。它是使一个对象真正可调用的方法,需要重载,让一个给定的对象像一个例程一样行动。如果对象需要存储在一个 &-sigiled 容器中,则必须实现 Callable

class A does Callable {
    submethod CALL-ME(|c){ 'called' }
}
my &a = A;
say a(); # OUTPUT: «called␤»

应用 Callable 角色并不是使对象可调用的必要条件,如果一个类只是想在常规标量容器中添加类似子程序的语义,可以使用子方法 CALL-ME 来实现。

class A {
    has @.values;
    submethod CALL-ME(Int $x where 0 <= * < @!values.elems) {
        @!values[$x]
    }
}
my $a = A.new: values => [4,5,6,7];
say $a(2); # OUTPUT: «6␤»

100.3.2. (Callable) 方法 Capture

被定义为:

method Capture()

抛出 X::Cannot::Capture