假设你举办了一场乒乓球比赛。裁判以 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

v6§

use v6;

每个 Raku 程序都应从类似于 use v6; 的行开始。此行告诉编译器程序期望的 Raku 版本。例如,6.c 是 Raku 版本的一个示例。如果你不小心用 Perl 运行文件,你将收到一条有用的错误消息。

语句§

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

Raku 程序由零个或多个语句组成。语句以分号或行尾的大括号结尾。

在 Raku 中,单行注释以单个哈希字符 # 开始,并一直延伸到行尾。Raku 还支持 多行/嵌入式注释。编译器不会将注释评估为程序文本,它们仅供人类读者使用。

词法作用域§

my $file = open 'scores.txt';

my 声明一个词法变量,它仅在声明点到块结束的当前块中可见。如果没有封闭块,它将在文件的其余部分(实际上将是封闭块)中可见。块是包含在大括号 { } 中的任何代码部分。

符印§

变量名以符印开头,符印是非字母数字符号,例如 $@%& — 或偶尔是双冒号 ::。符印指示变量的结构化接口,例如它应被视为单个值、复合值、子例程等。符印后是标识符,它可能由字母、数字和下划线组成。在字母之间,你还可以使用破折号 - 或撇号 ',因此 isn'tdouble-click 是有效的标识符。

标量§

Sigils 表示变量的默认访问方法。带有 @ sigil 的变量按位置访问;带有 % sigil 的变量按字符串键访问。但是,$ sigil 表示一个常规标量容器,它可以保存任何单个值,并可以以任何方式访问。标量甚至可以包含复合对象,如 ArrayHash$ sigil 表示应将其视为单个值,即使在期望多个值(如 ArrayHash)的上下文中也是如此。

文件句柄§

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

字符串文字§

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

数组§

my @names = $file.get.words;

右侧调用一个方法——一个名为 get 的命名行为组——在存储在 $file 中的文件句柄上。get 方法从文件中读取并返回一行,删除行尾。如果在调用 get 后打印 $file 的内容,你将看到第一行已不再存在。words 也是一个方法,在从 get 返回的字符串上调用。words 将其调用者——它操作的字符串——分解为一个单词列表,此处表示用空格分隔的字符串。它将单个字符串 'Beth Ana Charlie Dave' 转换为字符串列表 'Beth', 'Ana', 'Charlie', 'Dave'

最后,此列表存储在 Array @names 中。@ sigil 将声明的变量标记为 Array。数组存储有序列表。

哈希§

my %matches;
my %sets;

这两行代码声明了两个哈希。% sigil 将每个变量标记为 HashHash 是一个无序的键值对集合。其他编程语言将其称为哈希表字典映射。你可以使用 %hash{$key} 查询哈希表以获取对应于特定 $key 的值。

在比分统计程序中,%matches 存储了每个选手赢得的比赛场次。%sets 存储了每个选手赢得的局数。这两个哈希都以选手姓名为索引。

for§

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 中。

在处理文件的第 1 行后,变量包含以下值

变量内容
$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。我们将在下面更详细地了解此行为。

Any" data-indexedheader="Tutorial;Any (Basics)">Any§

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

%sets{$p1} += $r1 表示将左侧变量中的值增加 $r1。在第一次迭代中,%sets{$p1} 尚未定义,因此它默认为一个称为 Any 的特殊值。+= 运算符方便地将未定义值 Any 视为值为 0 的数字,允许它明智地向其中添加一些其他值。为了执行加法,字符串 $r1$r2 等会自动转换为数字,因为加法是一个数字运算。

胖箭头§

在执行这两行之前,%sets 为空。向哈希中尚未存在的条目添加内容将导致该条目立即出现,其值从零开始。此行为称为自动生成。在首次运行这两行之后,%sets 包含 'Ana' => 3, 'Dave' => 0。(胖箭头 =>Pair 中分隔键和值。)

后增量§

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

如果 $r1 数值上大于 $r2,则 %matches{$p1} 增加 1。如果 $r1 不大于 $r2,则 %matches{$p2} 增加。与 += 的情况一样,如果之前不存在哈希值,则增量操作会自动生成该值。

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

主题变量§

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

此行由三个单独的简单步骤组成。数组的 sort 方法返回数组内容的已排序版本。但是,数组上的默认排序按其内容排序。要按获胜者优先的顺序打印玩家姓名,代码必须按玩家的得分对数组进行排序,而不是按姓名排序。sort 方法的参数是一个,用于将数组元素(玩家姓名)转换为按其排序的数据。数组项通过主题变量 $_ 传递。

§

你之前见过块:for 循环 -> $line { ... }if 语句都在块上工作。块是 Raku 代码的一个自包含部分,具有可选签名(-> $line 部分)。

按得分对玩家进行排序的最简单方法是 @names.sort({ %matches{$_} }),按获胜场次排序。然而,Ana 和 Dave 都赢得了两场比赛。这种简单的排序没有考虑获胜的盘数,这是决定谁赢得比赛的次要标准。

稳定排序§

当两个数组项具有相同的值时,sort 会将它们保留在找到它们的顺序中。计算机科学家称之为稳定排序。该程序利用 Raku 的 sort 的这一特性,通过两次排序来实现目标:首先按获胜的盘数(次要标准)排序,然后按获胜的场次(主要标准)排序。

在第一个排序步骤之后,名称按 Beth Charlie Dave Ana 的顺序排列。在第二个排序步骤之后,它仍然相同,因为没有人比其他人赢得的场次更少,但赢得的盘数更多。这种情况完全有可能发生,尤其是在大型比赛中。

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

标准输出§

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 设置为每个玩家的名称。将此代码理解为“对于 sorted 的每个元素,将 $n 设置为元素,然后执行以下块的内容。”变量 $match-noun 将存储字符串 match(如果玩家赢得一场比赛)或 matches(如果玩家赢得零场或多场比赛)。为此,使用了三元运算符 (?? !!)。如果 %matches{$n} == 1 计算结果为 True,则返回 match。否则,返回 matches。无论哪种方式,返回的值都存储在 $match-noun 中。相同的方法适用于 $set-noun

语句 say 将其参数打印到标准输出(通常是屏幕),后跟换行符。(如果您不想要末尾的换行符,请使用 print。)

请注意,say 将通过调用 .gist 方法截断某些数据结构,因此如果您想要确切的输出,put 会更安全。

变量插值§

当您运行程序时,您会看到 say 不会逐字打印该字符串的内容。它会打印变量 $n 的内容,而不是 $n — 存储在 $n 中的玩家名称。这种用代码内容自动替换代码的行为称为插值。此插值仅发生在用双引号 "..." 分隔的字符串中。单引号字符串 '...' 不进行插值

双引号字符串§

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 中的双引号字符串可以使用 $ sigil 插值变量,也可以插值大括号中的代码块。由于任何任意的 Raku 代码都可以出现在大括号中,因此可以通过将 ArrayHash 放在大括号中来插值它们。

大括号中的数组在每个项目之间插值一个空格字符。大括号中的哈希作为一系列行进行插值。每行将包含一个键,后跟一个制表符,然后是与该键关联的值,最后是一个换行符。

我们现在来看一个例子。

在此示例中,您将看到一些特殊语法,它可以更轻松地创建字符串列表。这是 <...> quote-words 构造。当您将单词放在 <> 之间时,它们都被假定为字符串,因此您不必用双引号 "..." 将它们分别包裹起来。

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 

当数组和哈希变量直接出现在双引号字符串中(而不是在大括号内)时,只有当其名称后跟后缀运算符(一个后跟语句的括号对)时,才会进行插值。在变量名和后缀之间进行方法调用也是可以的。

Zen 切片§

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. 示例程序的输入格式是冗余的:包含所有玩家名称的第一行不是必需的,因为您可以通过查看后续行中的名称来找出哪些玩家参加了锦标赛,因此原则上我们可以安全地将其删除。

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

答案:删除 scores.txt 中的第一行后,删除行 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!";
    }
    ...
}