Wait the light to fall

Raku 101 例

焉知非鱼

假设您举办乒乓球锦标赛。裁判员以格式告诉你每场比赛的结果 Player1 Player2 | 3:2,这意味着 Player1 赢 Player2 了3到2局。你需要一个脚本来总结每个玩家赢得的比赛和数量,以确定总冠军。

输入数据(存储在一个名为的文件中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;
 
my $file  = open 'scores.txt';
my @names = $file.get.words;
 
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 {
    say "$n has won %matches{$n} matches and %sets{$n} sets";
}

这会产生输出:

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

v6 #

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

statement #

Raku程序由零个或多个语句组成。甲语句用分号或在线的端部的大括号结束:

my $file = open 'scores.txt';

lexical和block #

my声明一个词法变量,它只在从声明点到块结尾的当前块中可见。如果没有封闭块,则在整个文件的剩余部分(它实际上是封闭块)中可见。块是大括号之间的代码的任何部分{ }。

sigil 和 identifier #

变量名开始于印记,这是因为这样的非字母数字符号$,@,%,或&–or偶尔双冒号::。Sigils指示变量的结构接口,例如它是否应被视为单个值,复合值,子例程等。在sigil之后出现一个标识符,可以由字母,数字和下划线组成。在字母之间你也可以使用破折号-或撇号’,因此isn’t并且double-click是有效的标识符。

scalar #

Sigils指示变量的默认访问方法。带有@印记的变量可以在位置上访问; 带有%sigil的变量由字符串键访问。的$印记,然而,指示可以包含任何单个值和以任何方式进行访问的通用标量容器中。标量甚至可以包含像a Array或a 这样的复合对象Hash; 的$印记表示这应该被视为一个单一的值,即使在期望多个值(如用一个上下文Array或Hash)。

filehandle 和 assignment #

内置函数open打开一个文件,此处命名scores,并返回一个文件句柄 - 表示该文件的对象。等号将该文件句柄= 分配给左侧的变量,这意味着$file现在存储文件句柄。

string literal #

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

my @names = $file.get.words;

array,method 和 invocant #

右侧调用一个方法 - 一个命名的行为组 - 以get存储在其中的文件句柄命名$file。该get方法从文件中读取并返回一行,删除行结尾。如果您$file在打电话后打印内容get,您将看到第一行不再在那里。words也是一个方法,在返回的字符串上调用get。words将其调用者 - 它所操作的字符串 - 分解为一个单词列表,这里的意思是由空格分隔的字符串。它将单个字符串’Beth Ana Charlie Dave’转换为字符串列表’Beth’, ‘Ana’, ‘Charlie’, ‘Dave’。

最后,此列表存储在Array中 @names。该@印记标志着声明的变量作为Array。数组存储有序列表。

my %matches;
my %sets;

hash #

这两行代码声明了两个哈希值。的%印记标记每个变量作为Hash。A Hash是键值对的无序集合。其他编程语言称为哈希表,字典或映射。您可以查询一个哈希表对应于一定的值$key用%hash{$key}。

在得分计数程序中,%matches存储每个玩家赢得的比赛数量。%sets存储每个玩家赢得的套数。这两个哈希都是由玩家的名字索引的。

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

for 和 block #

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。

处理完文件的第一行后,变量包含值:cell ‘0’

Variable Contents
$line ‘Ana Dave `
$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;

Any 和 += #

+= $r1表示将左侧变量中的值增加$ r1。在第一次迭代%sets{$p1}中尚未设置,因此它默认为一个名为的特殊值Any。加法和递增运算符视为Any零的数字; 字符串会自动转换为数字,因为加法是一个数字运算。

fat arrow,pair和autovivification 在这两行执行之前,%sets为空。添加到不在散列中的条目将导致该条目即时生成,其值从零开始。(这是autovivification)。在这两行第一次运行后,%sets包含’Ana’ => 3, ‘Dave’ => 0 。(胖箭头=> 分隔键中的键和值Pair。)

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

如果$r1在数值上大于$r2,则%matches{$p1}增加1。如果$r1不大于$r2,则%matches{$p2}递增。正如在这种情况下+=,如果之前不存在任何一个哈希值,它将通过增量操作自动生成。

postincrement 和 preincrement #

$thing++是$thing += 1或的缩写$thing = $thing + 1,除了表达式的返回值在增量$thing 之前的小异常,而不是递增的值。与许多其他编程语言一样,您可以将其++用作前缀。然后它返回递增的值; my $x = 1; say ++$x打印2。

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

variables, $_ #

该行包含三个单独的简单步骤。数组的sort方法返回数组内容的排序版本。但是,数组上的默认排序按其内容排序。要以获胜者优先顺序打印玩家名称,代码必须按照玩家的分数而不是他们的名字对数组进行排序。该sort方法的参数是一个块,用于将数组元素(播放器的名称)转换为要排序的数据。数组项通过主题变量 传入$_。

block #

您之前已经看过块:for循环-> $line { … } 和if语句都在块上运行。块是一个独立的Raku代码片段,带有可选的签名(-> $line 部分)。

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

stable sort #

当两个数组项具有相同的值时,sort将它们保留为找到它们的顺序。计算机科学家称之为稳定的。该程序利用Raku的这个属性sort通过两次排序来实现目标:首先是赢得的集合数量(次要标准),然后是赢得的匹配数量。

在第一个排序步骤之后,名称在顺序中Beth Charlie Dave Ana。在第二个排序步骤之后,它仍然是相同的,因为没有人比其他人赢得更少的比赛但是更多的比赛。这样的情况是完全可能的,特别是在较大的比赛中。

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

for @sorted -> $n {
    say "$n has won %matches{$n} matches and %sets{$n} sets";
}

say,print 和 put #

为了打印出玩家及其分数,代码循环@sorted,$n依次设置每个玩家的名字。将此代码读作“对于已排序的每个元素,设置$n为元素,然后执行以下块的内容”。say将其参数打印到标准输出(通常是屏幕),然后是换行符。(print如果您不想在最后使用换行符,请使用。)

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

interpolation #

当您运行该程序时,您将看到它say不会逐字打印该字符串的内容。代替$n它打印变量的内容$n- 存储在其中的玩家的名字$n。代码及其内容的自动替换是插值。此插值仅在由双引号分隔的字符串中发生"…"。单引号字符串’…‘不进行插值:

double-quoted strings 和 single-quoted strings #

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以及花括号中的代码块来插入变量。由于任何Perl代码都可以出现在花括号中,因此可以通过将它们放在花括号中来插入Arrays和Hashes。

花括号内的数组使用每个项目之间的单个空格字符进行插值。花括号内的哈希值被插值为一系列线条。每行包含一个键,后跟一个制表符,然后是与该键相关的值,最后是换行符。

我们现在看一个例子吧。

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

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}";                         # From the table tennis tournament 
 
# Charlie 4 
# Dave    6 
# Ana     8 
# Beth    4 

当数组和散列变量直接出现在双引号字符串中(而不是在大括号内)时,如果它们的名称后跟postcircumfix - 一个跟在语句后面的包围对,它们只会被插值。在变量名和postcircumfix之间进行方法调用也没问题。

Zen slice #

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。

答:删除该行my @names = $file.get.words;,然后更改:

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

为:

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

除了删除冗余@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!";
    }
    ...
}