假设你举办了一场乒乓球比赛。裁判以 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 = open 'scores.txt'; # get filehandle and...my = .get.words; # ... get players.my ;my ;for .lines ->my = .sort().sort().reverse;for ->
这将产生以下输出
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 = open 'scores.txt';
my 声明一个词法变量,它仅在声明点到块结束的当前块中可见。如果没有封闭块,它将在文件的其余部分(实际上将是封闭块)中可见。块是包含在大括号 { } 中的任何代码部分。
符印§
变量名以符印开头,符印是非字母数字符号,例如 $、@、% 或 & — 或偶尔是双冒号 ::。符印指示变量的结构化接口,例如它应被视为单个值、复合值、子例程等。符印后是标识符,它可能由字母、数字和下划线组成。在字母之间,你还可以使用破折号 - 或撇号 ',因此 isn't 和 double-click 是有效的标识符。
标量§
Sigils 表示变量的默认访问方法。带有 @ sigil 的变量按位置访问;带有 % sigil 的变量按字符串键访问。但是,$ sigil 表示一个常规标量容器,它可以保存任何单个值,并可以以任何方式访问。标量甚至可以包含复合对象,如 Array 或 Hash;$ sigil 表示应将其视为单个值,即使在期望多个值(如 Array 或 Hash)的上下文中也是如此。
文件句柄§
内置函数 open 打开一个文件,此处名为 scores.txt,并返回一个文件句柄——表示该文件的一个对象。赋值运算符 = 将该文件句柄赋值给左侧的变量,这意味着 $file 现在存储文件句柄。
字符串文字§
'scores.txt' 是一个字符串文字。字符串是一段文本,字符串文字是直接出现在程序中的字符串。在此行中,它是提供给 open 的参数。
数组§
my = .get.words;
右侧调用一个方法——一个名为 get 的命名行为组——在存储在 $file 中的文件句柄上。get 方法从文件中读取并返回一行,删除行尾。如果在调用 get 后打印 $file 的内容,你将看到第一行已不再存在。words 也是一个方法,在从 get 返回的字符串上调用。words 将其调用者——它操作的字符串——分解为一个单词列表,此处表示用空格分隔的字符串。它将单个字符串 'Beth Ana Charlie Dave' 转换为字符串列表 'Beth', 'Ana', 'Charlie', 'Dave'。
最后,此列表存储在 Array @names 中。@ sigil 将声明的变量标记为 Array。数组存储有序列表。
哈希§
my ;my ;
这两行代码声明了两个哈希。% sigil 将每个变量标记为 Hash。 Hash 是一个无序的键值对集合。其他编程语言将其称为哈希表、字典或映射。你可以使用 %hash{$key} 查询哈希表以获取对应于特定 $key 的值。
在比分统计程序中,%matches 存储了每个选手赢得的比赛场次。%sets 存储了每个选手赢得的局数。这两个哈希都以选手姓名为索引。
for§
for .lines ->
for 产生一个循环,该循环对列表中的每一项运行大括号分隔的块一次,将变量 $line 设置为每次迭代的当前值。$file.lines 产生从文件 scores.txt 中读取的行列表,从文件的第二行开始,因为我们已经调用过 $file.get 一次,一直到文件的末尾。
在第一次迭代期间,$line 将包含字符串 Ana Dave | 3:0;在第二次迭代期间,包含 Charlie Beth | 3:1,依此类推。
my (, ) = .split(' | ');
my 可以同时声明多个变量。赋值的右侧是调用名为 split 的方法,传递字符串 ' | ' 作为参数。
split 将其调用者分解为字符串列表,以便使用分隔符 ' | ' 连接列表项会产生原始字符串。
$pairing 获取返回列表的第一个项,$result 获取第二个项。
在处理第一行后,$pairing 将保存字符串 Ana Dave,$result 将保存 3:0。
接下来的两行遵循相同的模式
my (, ) = .words;my (, ) = .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' |
然后程序计算每个选手赢得的局数
+= ;+= ;
以上两个语句涉及 += 复合赋值运算符。它们是使用简单赋值运算符 = 的以下两个语句的变体,乍一看可能更容易理解
= + ;= + ;
对于你的程序,首选使用 += 复合赋值运算符的简写版本,而不是使用简单赋值运算符 = 的长写版本。这不仅是因为较短的版本需要更少的键入,还因为 += 运算符会将哈希键值对的未定义值静默初始化为零。换句话说:通过使用 +=,无需在向其中有意义地添加 $r1 之前包含单独的语句,如 %sets{$p1} = 0。我们将在下面更详细地了解此行为。
Any" data-indexedheader="Tutorial;Any (Basics)">Any§
+= ;+= ;
%sets{$p1} += $r1 表示将左侧变量中的值增加 $r1。在第一次迭代中,%sets{$p1} 尚未定义,因此它默认为一个称为 Any 的特殊值。+= 运算符方便地将未定义值 Any 视为值为 0 的数字,允许它明智地向其中添加一些其他值。为了执行加法,字符串 $r1、$r2 等会自动转换为数字,因为加法是一个数字运算。
胖箭头§
在执行这两行之前,%sets 为空。向哈希中尚未存在的条目添加内容将导致该条目立即出现,其值从零开始。此行为称为自动生成。在首次运行这两行之后,%sets 包含 'Ana' => 3, 'Dave' => 0。(胖箭头 => 在 Pair 中分隔键和值。)
后增量§
if >else
如果 $r1 数值上大于 $r2,则 %matches{$p1} 增加 1。如果 $r1 不大于 $r2,则 %matches{$p2} 增加。与 += 的情况一样,如果之前不存在哈希值,则增量操作会自动生成该值。
$thing++ 是 $thing += 1 的变体;它与后者的不同之处在于,表达式的返回值是在增加之前的 $thing,而不是增加后的值。与许多其他编程语言一样,你可以将 ++ 用作前缀。然后它返回增加后的值:my $x = 1; say ++$x 打印 2。
主题变量§
my = .sort().sort().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 设置为每个玩家的名称。将此代码理解为“对于 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 = '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 代码都可以出现在大括号中,因此可以通过将 Array 和 Hash 放在大括号中来插值它们。
大括号中的数组在每个项目之间插值一个空格字符。大括号中的哈希作为一系列行进行插值。每行将包含一个键,后跟一个制表符,然后是与该键关联的值,最后是一个换行符。
我们现在来看一个例子。
在此示例中,您将看到一些特殊语法,它可以更轻松地创建字符串列表。这是 <...> quote-words 构造。当您将单词放在 < 和 > 之间时,它们都被假定为字符串,因此您不必用双引号 "..." 将它们分别包裹起来。
say "Math: ";# OUTPUT: «Math: 3»my = <Luke Matthew Mark>;say "The synoptics are: ";# OUTPUT: «The synoptics are: Luke Matthew Mark»say "";# OUTPUT (From the table tennis tournament):# Charlie 4# Dave 6# Ana 8# Beth 4
当数组和哈希变量直接出现在双引号字符串中(而不是在大括号内)时,只有当其名称后跟后缀运算符(一个后跟语句的括号对)时,才会进行插值。在变量名和后缀之间进行方法调用也是可以的。
Zen 切片§
my = <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 postcircumfixsay "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 = .sort().sort().reverse;
... 为
my = .keys.sort().sort().reverse;
2. 您可以使用冗余的 @names 变量来警告出现未在第一行中提到的玩家,例如由于错字。您将如何修改程序以实现此目的?
提示:尝试使用 成员运算符。
答案:将 @names 更改为 @valid-players。在循环遍历文件行时,检查 $p1 和 $p2 是否在 @valid-players 中。请注意,对于成员运算符,您还可以使用 (elem) 和 !(elem)。
...;my = .get.words;...;for .lines ->