Raku 单行程序

on

1. 命令行选项

1.1. 使用命令行选项

我们来谈谈 Rakudo[1] 编译器为我们提供的命令行选项。

1.1.1. -e

在使用 Raku 时要知道的第一个选项是 -e。它接收一个带有 Raku 单行程序的字符串,并立即执行。

例如,打印当前用户的名字:

$ raku -e'say $*USER'
ash

1.1.2. -n

这个选项会重复每一行输入数据的代码。当你想处理一个文件时,这是很方便的。例如,这里有一个单行程序,它将每行中的值相加并打印出总和:

$ raku -ne'say [+] .split(" ")' data.txt

如果 data.txt 文件包含以下内容:

10 20 30 40
1 2 3 4
5 6 7 8

那么这个单行程序的结果就是:

100
10
26

是否使用 shell 的输入重定向没有区别,下面这行也是可以的:

$ raku -ne'say [+] .split(" ")' < data.txt

确保你把 e 选项放在列表的最后(所以,不是 raku -en'…​'),或者把选项分开:raku -n -e'…​'

1.1.3. -p

这个选项类似于 -n,但在每次迭代后都会打印主题变量。

下面的单行程序将文件中的文本行反转并打印到控制台:

$ raku -npe'.=flip' data.txt

对于同样的输入文件,结果会是这样的:

04 03 02 01
4 3 2 1
8 7 6 5

注意,你必须更新 $_ 变量,所以你输入 .=flip。如果你只有 .flip,你将反转字符串,但是结果没有被使用,而是打印出原来的行。

一个带有 .flip 和没有 -p 的等效程序看起来像这样:

$ raku -ne'.flip.say' data.txt

1.2. 简短的单行程序的例子

让我们通过几个单行程序,把文件作为开胃菜来工作。下一整章的内容都是关于文件的工作。

1.2.1. 双倍空格的文件

$ raku -npe's/$/\n/' text.txt

1.2.2. 删除所有空行

$ raku -ne'.say if .chars' text.txt

根据你对"空白"的定义,你可能需要另一个单行程序,跳过包含空格的行:

$ raku -ne'.say if /\S/' text.txt

1.2.3. 对文件中的所有行进行编号

$ raku -ne'say ++$ ~ ". " ~ $_' text.txt

这段代码,可能需要注释。$ 变量是一个状态变量,可以不用声明。

1.2.4. 将所有文本转换为大写字母

$ raku -npe'.=uc' text.txt

1.2.5. 从每行的开头和结尾去掉空白

$ raku -npe'.=trim' text.txt

1.2.6. 打印文件的第一行

$ raku -ne'.say ; exit' text.txt

1.2.7. 打印文件的前10行

$ raku -npe'exit if $++ == 10' text.txt

这次,后缀 ++ 运算符被应用到 $ 变量上。

1.3. 用 $*ARGFILES 读取文件

$*ARGFILES 是一个内置的动态变量,在处理多个输入文件时可能会很方便。

如何读取命令行中传递的两个或多个文件?

$ raku work.pl a.txt b.txt

如果你需要把所有的文件一起处理,就好像它们是一个数据源一样,你可以用一个单行程序要求变量来完成这项工作:

.say for $*ARGFILES.lines

在程序内部,你不必考虑循环处理文件,$*ARGFILES 会自动帮你完成。

如果命令行中没有文件,变量将被附加到 STDIN 上:

$ cat a.txt b.txt | raku work.pl

确实很方便,不是吗?

1.3.1. $*ARGFILES 和 MAIN

如果你想在更大的程序中使用 $*ARGFILES 变量,我还得提醒你。考虑一下下面的例子:

sub MAIN(*@files) {
    .say for $*ARGFILES.lines;
}

在最近的 Raku 版本中,$*ARGFILES 在 MAIN 子程序内部和外部的工作方式不同。

本程序将完美地与早期版本(在 Rakudo 2018.10 版本之前, 包括 Rakudo 2018.10 版本)一起工作。从 Rakudo Star 2018.12 开始,如果在 MAIN 内部使用 $*ARGFILES,则总是连接到 $*IN

2. 使用文件

2.1. 重命名文件

让我们来解决一个任务,重命名所有在命令行参数中传递的文件,并以首选的格式给文件顺序号。下面是一个命令行的例子:

$ raku rename.raku *.jpg img_0000.jpg

在本例中,当前目录下的所有图像文件将被重命名为 img_0001.jpg、img_0002.jpg 等。

这里是 Raku 中可能的解决方案(保存在 rename.raku 文件中):

@*ARGS[0..*-2].sort.map: *.Str.IO.rename(++@*ARGS[*-1])

预先定义的动态变量 @ARGS 包含命令行的参数。在上面的例子中,shell 将 .jpg 掩码展开为一个文件列表,所以数组包含了所有文件。最后一个元素是重命名样本 img_0000.jpg。

如果你熟悉 C 或 Perl,请注意这个变量叫做 ARGS,而不是 ARGV

为了循环处理所有的文件(并跳过最后一个带有文件掩码的文件项),我们取 @ARGS 的片断。0..-2 构造创建一个索引范围,以取除最后一个元素以外的所有元素。

然后对列表进行排序(原 @*ARGS 数组保持不变),我们使用 map 方法对文件名进行迭代。

map 的主体包含一个 WhateveCode 块(见第6章);它接收当前值的字符串表示,将其制作成一个 IO::Path 对象,然后调用 rename 方法。注意,IO 方法创建了一个 IO::Path 类的对象;而裸露的 IO 则是 Raku 对象系统层次结构中的一个角色。

最后,增量运算符 ++ 会改变重命名样本(该样本保存在 @ARGS 的最后一个,-1 号元素中)。当运算符应用于一个字符串时,它会递增其中的数字部分,所以我们得到 img_0001.jpg、img_0002.jpg 等。

2.2. 横向合并文件

让我们把几个文件合并成一个文件。我们的任务是将两个(或三个,或更多)文件逐行复制其内容。例如,我们要合并两个日志文件,知道它们的所有行都是对应的。

文件 a.txt:

2019/12/20 11:16:13
2019/12/20 11:17:58
2019/12/20 11:19:18
2019/12/20 11:24:30

文件 b.txt:

"/favicon.ico" failed (No such file)
"/favicon.ico" failed (No such file)
"/robots.txt" failed (No such file)
"/robots.txt" failed (No such file)

第一个单行程序就说明了这个问题:

.say for [Z~] @*ARGS.map: *.IO.lines;

假设程序的运行方式如下:

$ raku merge.raku a.txt b.txt

对于命令行中的每一个文件名(@*.ARGS.map),都会创建一个 IO::Path 对象(.IO),然后读取文件中的行(.lines)。

在有两个文件的情况下,我们有两个序列,然后使用 zip 元操作符 Z 应用于连接后缀 ~ 逐行连接。

在这一步之后,我们得到另一个序列,我们可以逐行打印(.say for)。

2019/12/20 11:16:13"/favicon.ico" failed (No such file)
2019/12/20 11:17:58"/favicon.ico" failed (No such file)
2019/12/20 11:19:18"/robots.txt" failed (No such file)
2019/12/20 11:24:30"/robots.txt" failed (No such file)

结果形式上是正确的,但我们要在原来的行间加一个空格。下面是单行程序的更新版本:

.trim.say for [Z~] @*ARGS.map: *.IO.lines.map: *~ ' '

在这里,每一行的末尾都会附加一个空格字符(.map: *~ ' '),由于合并后的行末尾会有一个额外的空格,所以会被 trim 方法去除。它的同级,trim-trailing,可以用来代替(如果你关心原来的尾部空格恰好在第二个文件中,也可以用一个 regex)。

经过以上的修改,现在两个文件已经完美的合并了:

2019/12/20 11:16:13 "/favicon.ico" failed (No such file)
2019/12/20 11:17:58 "/favicon.ico" failed (No such file)
2019/12/20 11:19:18 "/robots.txt" failed (No such file)
2019/12/20 11:24:30 "/robots.txt" failed (No such file)

例如,将同一个文件合并到自己身上,或者提供两个以上的文件,都没有问题:

$ raku merge.raku a.txt a.txt a.txt

2.3. 翻转文件

在本节中,我们将创建一个单行程序,以相反的顺序打印文本文件的行(就像 tail -r 那样)。

第一个单行程序可以完成 STDIN 流的工作。

.say for $*IN.lines.reverse

运行该程序为:

$ raku reverse.raku < text.txt

在这种情况下,$*IN 可以省略,这使得单行本更短:

如果你想直接从 Raku 读取文件,修改一下程序,用命令行参数创建一个文件句柄:

.say for @*ARGS[0].IO.open.lines.reverse

现在你运行它如下:

$ raku reverse.raku text.txt

重要的是要记住,line 方法的默认行为是将换行符从最终的行序列中排除(该方法返回一个 Seq 对象,而不是数组或列表)。

在 Raku 中,lines 方法根据 IO::Handle 对象的 .nl-in 属性中存储的值来分割行。

你可以用下面的小脚本查看当前的行分隔符的值。

dd $_ for @*ARGS[0].IO.open.nl-in

这就是你在那里发现的默认情况。

$["\n", "\r\n"]

有趣的是,你可以控制行的行为,并告诉 Raku 不要排除换行字符。

@*ARGS[0].IO.open(chomp => False).lines.reverse.put

chomp 属性默认设置为 True。你也可以更改默认的分隔符。

@*ARGS[0].IO.open(
    nl-in => "\r",
    chomp => False
).lines.reverse.put

请注意,如果不使用 chomping,就不需要在行上显式的 for 循环:在最后两个单行中,.put 方法是直接在序列对象上调用的。在早期的版本中,字符串不包含新行字符,因此它们会被打印成一个长行。

给你一个小作业。告诉大家 put 和 `say 的区别。

3. 使用数字

3.1. 对可分数进行移位

任务是找出 3 和 5 在 1000 以下的所有倍数之和。我们感兴趣的行的第一个元素是 3,5,9,15,20,21 等。大家已经可以看到,其中有些元素,如15,既是 3 的倍数,又是 5 的倍数,所以不能把 3 和 5 的倍数分别相加。

小故事就在这里。

say sum((1..999).grep: * %% (3 | 5))

现在,我们来解读一下。

我们需要的是过滤那些 3 或 5 的倍数的数字。如果你重新阅读上一句话,你应该会有一个钟声:在 Raku 中,这可以通过 junctions 来实现,非正式地称为量子叠加。要测试一个数字是否可以被 3 或 5 整除,请写出以下内容。

$x %% 3 | 5

对了,%% 这个整除操作符,是个很贴心的东西,有助于避免布尔测试中的否定式,你如果只有一个 %,会不会写成这样。

!($x % (3 | 5))

好了,主要条件已经准备好了,我们来扫描1到(含)999之间的数字。

(1..999).grep: * %% (3 | 5)

这里出现了一些比较有趣的语言元素。例如,WhateverCode 块,它是由一个 字符引入的(见第6章)。与方法调用的冒号形式一起,它允许摆脱一对嵌套的括号和小括号。如果没有 ,代码会更加复杂。

(1..999).grep({$_ %% (3 | 5)})

数字已经被过滤(如果你喜欢的话,可以用 grepped),现在是时候把它们加起来并打印出结果了。我们来到本节开头的最后一个单行程序。

当然,与其写 say sum(…​),还不如写一两个方法调用。

((1..999).grep: * %% (3 | 5)).sum.say

作为奖励曲目,这是我的第一个解决方案,它比单行程序要长得多。

sub f($n) {
    ($n <<*>> (1...1000 / $n)).grep: * < 1000
}

say (f(3) ∪ f(5)).keys.sum;

3.2. 生成随机整数

你可能会问,这有什么大不了的,调用一种 rand 函数不是一个常规任务吗?嗯,从某种意义上说,是的,但你可能更喜欢调用一个方法。

让我们看看下面的代码:

2020.rand.Int.say;

这就是整个程序,它生成的是 2020 以下的随机数。它只使用方法调用,一个接一个地链起来。如果你从来没有见过 Raku,你首先注意到的是这里有一个对数字进行调用的方法。这在这个语言中并不是什么特别的东西。

在一个数字上调用 rand 方法,有可能是对 Cool 类中定义的方法的调用,而 Cool 类会立即委派给数字的数字代表。

method rand() { self.Num.rand }

Num 类的后面,发生了对底层 NQP 引擎的调用。

method rand(Num:D: ) {
    nqp::p6box_n(nqp::rand_n(nqp::unbox_n(self)));
}

在我们的例子中,对象 2020 是一个 Int,所以 rand 被直接派发到 Num 类的方法上。

rand 方法返回的是一个浮点数,所以调用另一个方法 Int 来得到一个整数。

运行代码几次,确认它能生成随机数。

$ raku -e'2020.rand.Int.say'
543
$ raku -e'2020.rand.Int.say'
1366
$ raku -e'2020.rand.Int.say'
1870

如果你想了解随机数生成器的质量,可以深入到 NQP 和 MoarVM,之后再深入到虚拟机的后端引擎。为了使结果可重复(例如,用于测试),通过调用 srand 函数设置种子。

$ raku -e'srand(10); 2020.rand.Int.say'
296
$ raku -e'srand(10); 2020.rand.Int.say'
296

请注意,srand 例程是一个子程序,而不是一个方法。它也被定义在同一个 Num 类中。

proto sub srand($, *%) {*}
multi sub srand(Int:D $seed --> Int:D) {
    nqp::p6box_i(nqp::srand($seed))
}

如果你有机会看到源文件,你很可能会注意到,还有一个独立的 rand 子程序。

proto sub rand(*%) {*} multi sub rand(--> Num:D) {
    nqp::p6box_n(nqp::rand_n(1e0))
}

你只能在没有参数的情况下调用它,在这种情况下,Raku 会生成一个介于 0 和 1 之间的随机数字。如果你传递参数,你会得到一个错误的解释,方法调用是比较好的。

$ raku -e'rand(20)'
===SORRY!=== Error while compiling -e
Unsupported use of rand(N); in Raku please use N.rand for Num
or (^N).pick for Int result
at -e:1
------> rand⏏(20)

3.3. 与大数打交道

在这一节中,我们将看欧拉工程的第十三个问题。让我展示其中的一部分。

37107287533902102798797998220837590246510135740250
46376937677490009712648124896970078050417018260538
74324986199524741059474233309513058123726617309629
91942213363574161572522430563301811072406154908250
23067588207539346171171980310421047513778063246676
89261670696623633820136378418383684178734361726757
28112879812849979408065481931592621691275889832738
44274228917432520321923589422876796487670272189318
47451445736001306439091167216856844588711603153276
70386486105843025439939619828917593665686757934951
62176457141856560629502157223196586755079324193331
64906352462741904929101432445813822663347944758178
92575867718337217661963751590579239728245598838407
58203565325359399008402633568948830189458628227828
80181199384826282014278194139940567587151170094390
35398664372827112653829987240784473053190104293586
86515506006295864861532075273371959191420517255829
71693888707715466499115593487603532921714970056938
54370070576826684624621495650076471787294438377604
53282654108756828443191190634694037855217779295145
36123272525000296071075082563815656710885258350721
45876576172410976447339110607218265236877223636045
17423706905851860660448207621209813287860733969412
81142660418086830619328460811191061556940512689692

...

72107838435069186155435662884062257473692284509516
20849603980134001723930671666823555245252804609722
53503534226472524250874054075591789781264330331690

的确,这个数字看起来很大,任务是找到一百个整数之和的前十位,每个整数由50位组成。

听起来像一个任务,可能需要一些优化和简化,以摆脱一切不有助于前十位数的结果。但在 Raku 中不是这样。

在这里,你可以简单地将数字相加,并取其中的前十位数。

<
    37107287433902102798797998220837590246510135740250
    # Other 98 numbers here
    53503534526472524250874054075591789781264330331690
>.sum.substr(0, 10).say

Raku 默认使用任意长的整数;你不需要包含任何模块或以其他方式激活这种行为。你甚至可以计算幂数,并很快得到结果。

$ raku -e'say 37107287433902102798797998220837590 ** 1000'

另一个需要注意的是,我们可以透明地将字符串转换为数字,反之亦然。在当前的程序中,数字列表以一对角括号内的引号字符串列表的形式呈现。

在这个列表中,你调用了和方法,它可以处理数字。得到总和后,你又把它当作一个字符串,并提取其中的前十个字符。整个代码看起来非常自然,而且很容易阅读。

3.4. 测试回文数字

我们的任务是找到最大的回文数(从两端读出的数字,如1551),它是两个三位数的乘积。

换句话说,我们要扫描 999×999 以下的数字,可以优化解法,但实际上,我们只需要允许数字,也就是积,因此,我们不要跳过乘法部分。

这是我们的单行程序。

(((999...100) X* (999...100)).grep: {$^a eq $^a.flip}).max.say

大家已经准备好了,链式方法调用在 Raku 单行程序中使用是非常方便的。

前面我们也看到了方法调用的冒号形式,但这次我们使用的是一个带有占位变量的代码块。这里不太清楚是否可以使用星号,因为我们在代码块中需要变量两次。

这一行的第一部分使用了交叉运算符 X*,(参见第 6 章)。它生成了所有三位数的乘积。由于我们需要最大的数字,所以从右到左开始是合理的,这就是为什么序列是 999…​100,而不是 100…​999。

我们来看看 grepped 乘积序列中的前几个数字。

580085 514415 906609 119911 282282 141141 853358 650056

单行程序并不总是很理想。在我们的例子中,我们需要生成整个产品序列,以找到其中的最大值。答案位于第三位,所以用 first 取代 max 将是一个错误。但好在如果你使用第一,Raku 将不会生成所有的数字。还有一个有用的方法,head,也可以防止生成超过必要的数量。

下面的代码运行得更快,并且给出了正确的结果。

(((999...100) X* (999...100)).grep: {$^a eq $^a.flip}).head(10).max.say

3.5. 斐波那契偶数的加法

这里的任务是找出所有低于400万的斐波那契偶数之和。

这就是完整的解决方案。

(1, 1, * + * ... * >= 4_000_000).grep(* %% 2).sum.say

从左端或右端解析代码同样有趣。让我们从左端开始。

在第一个括号内,我们正在生成一个斐波那契数列,这个数列从两个1开始,接下来生成的每个数都是前两个数的和。你可以用一个WhateverCode块来表达它。* + * 等同于 {$^a + $^b}

序列的一个不太为人所知的特征是最后的条件。在许多例子中,你会看到一个裸星或一个 Inf。在我们的例子中,我们用一个明确的上边界来限制序列。

请注意,你不能简单地写出:

1, 1, * + * ... 4_000_000

为了更好地可视化,可以尝试一个更小的限制,比如100。

> (1, 1, * + * ... 100)
(1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181

当序列越过我们想要的边界时,生成不会停止。只有当序列的下一个计算元素正好等于给定的数字时,它才会停止生成,例如。

> (1, 1, * + * ... 144)
(1 1 2 3 5 8 13 21 34 55 89 144)

如果你不知道 400 万前面的斐波那契数,请使用另一个带有布尔条件的 WhateverCode 块。* >= 4_000_000。请注意,这个条件和你在常规循环中写的条件是相反的,因为这里我们要求的是超过四百万,而不是小于。当你不得不停止序列时,这个条件就变成了 True。如果没有星号,你可以使用默认变量: {$_ >= 4_000_000}

代码的其余部分则是对偶数进行抓取,并将它们相加。

3.6. 玩转斐波那契数字

是的,很有可能,你从来没有在实际的代码中使用过这样的数字,同样,很有可能,你用它们解决了很多教育问题。尽管如此,今天,让我们尝试着去接近 Code-Golf.io 网站上的斐波那契问题的最短解法(另见第7章关于高尔夫编码的内容)。

我们如何形成一个斐波那契序列?用序列运算符 …​:

0, 1, * + * ... *

如果你想在代码中加入一些异国情调,你可以用 Inf 或 ∞ 代替最后一颗星。在任何情况下,结果都是一个 Seq 类型的懒惰序列。Raku 不会立即计算它(也不能,因为右边缘是无限的)。

最后,让我们找到第一个斐波那契数的索引,它有 1000 位。当然,你可以在上面创建的序列上循环,自己追踪指数。但是在 Raku 中,有一个选项可以修改 grep 例程族,要求它返回匹配项的索引而不是项本身。

另外,我们不使用 grep,而是使用一个更合适的方法,首先。如果我们调用k键的方法,它将返回第一个匹配的项目或其索引。只要提到键就够了,其实不需要值。

say (0, 1, * + * ... *).first(*.chars >= 1000, :k)

这个程序打印一个整数,这就是给定问题的正确答案。

3.7. 两点之间的距离

这里有一个插图来帮助制定任务。我们的目标是找到A和B两点之间的距离。

为了使答案更透明,更容易检查,我选择了线段AB,使它是边长为3和4的直角三角形的斜边。在这种情况下,第三条边的长度是5。

下面是解题思路。

say abs(5.5+2i - (1.5+5i))

这段代码使用的是复数,只要把问题移到复数平面上,你就会从表面上两点之间的距离等于这两个数相减的绝对值中获得收益。

在这种情况下,其中一个点是复平面上的5.5+2i点,第二个点是1.5+5i。在乐乐中,你写下复数就像写数学一样。

如果没有复数的内置支持,你就必须明确使用毕达哥拉斯定理。

say sqrt((5.5 - 1.5)² + (2 - 5)²)

作业。修改 Raku 的 grammar,允许使用以下代码。

say √((5.5 - 1.5)² + (2 - 5)²)

3.8. 玩质数

让我们来解决这个问题,你需要打印第10001个质数(第一个是2)。

Raku 编程语言在质数方面很擅长,因为它有一个 Int 类的内置方法 is-prime。

有几种生成质数的方法。对于单行道来说,最好的是最简单,但效率最低的方法,它可以测试每个数字。

say ((1..*).grep: *.is-prime)[10000]

计算结果大约需要半分钟,但代码相当短。用所谓的埃拉托斯特尼斯的筛子来解决这个任务,效率要高得多,但可能需要更多的代码行,因此不是单行程序,不在本书的讨论范围之内。

3.9. 使用 map 和 Seq 计算 π 的值

在本节中,我们使用两种不同的方法计算 π 的值。目标是尝试不同的方法来生成数字序列。

准备工作

当然,你不需要自己计算π的值,因为 Raku 语言给我们提供了一些预定义的常量,形状有 π 和 pi 两种,还有双倍值 τ 和 tau。

但是为了演示 map 和 Seq 的用法,我们来实现一个最简单的算法来计算 π。

这里有一个草拟的代码来检查答案。

my $pi = 1;
my $sign = 1;

for 1..10000 -> $k {
    $sign *= -1;
    $pi += $sign / ($k * 2 + 1);
}

say 4 * $pi;

第一部分

现在,让我们使用 map 来使解更紧凑。最好是让公式也更通用。

这是我们的第一个单行程序。

say 4 * [+] (^1000).map({(-1) ** $_ / (2 * $_ + 1)})

希望你能理解这里的一切。我们在本书前面的章节中介绍了这个解决方案的不同部分。

但我还是要强调,你需要在-1周围加上括号,如果你键入 -1 $,那么你总是得到-1,因为减号前缀被应用于取幂的结果。所以,正确的代码是 (-1) $

第二部分

还可以尝试使用序列运算符…​…​根据上面提到的公式生成行,这也很有意思。另外,我们还要用有理数来生成分数 ⅓、⅕ 等。

say 4 * [+] <1/1>,
    {-Rat.new($^n.numerator, $^n.denominator + 2)} ...
    *.abs < 1E-5;

这个序列以一个有理数 <1/1> 开始,这很方便,因为我们可以立即取其分子和分母。序列的生成器块使用这个数来创建一个新的有理数,它的分母在每次迭代时增加2。

你可能会问我为什么要提到 $n.numerator,它总是1。这是需要的,因为要改变符号,我们需要知道当前值的符号,而符号被保留在有理值的分子部分。

占位符 $^n 自动取生成器代码块的第一个(也是唯一的)数组。

这个序列被生成,直到当前值的绝对值足够小。可能很想用 * ≅ 0 来代替最后的条件,但那个程序会运行太长时间而无法产生结果,因为近似相等运算符的默认容差是 10-15,而行不会那么快收敛。

另外,你也不能使用 < …​ / …​> 语法在生成器中创建一个 Rat

{ <$^n.numerator / $^n.denominator + 2> }

在这种情况下,Raku 将其视为一种引号结构,如 <red greenblue>,而不是代码块,你会得到一个字符串列表。

第三部分

Damian Conway 提出了以下使用序列的有趣而紧凑的解决方案。

say 4 * [+] (1,-1,1...* )
        «/« (1, 3, 5 ... 9999);

3.10. 求和

在本节中,我们将看到一个单行程序,它可以计算一个表的列的总和。

下面是一个文件中的一些示例数据。

100.20 303.50 150.25
130.44 1505.12 36.41
200.12 305.60 78.12

下面是打印每列三个数字—​总和的代码。

put [Z+] lines.map: *.words

程序会打印出我们需要的数字。

430.76 2114.22 264.78

我们知道裸行和 $*IN.lines 是一样的,所以 lines.map 会遍历输入流中的所有行。然后,每一行都会被分割成单词—​由空格分隔的子串。

工作中解析输入数据的部分就完成了。我们已经得到了一些对应于输入数据行的序列。对于我们的示例文件,这些序列如下。

(("100.20", "303.50", "150.25").Seq, ("130.44", "1505.12", "36.41").Seq, ("200.12", "305.60", "78.12").Seq).Seq

现在,把每个第一个元素、第二个元素等加起来。缩减运算符和拉链元运算符的组合只用四个字符就完成了所有的工作:[Z+]

这时,我们就有了一个还原序列。

(430.76, 2114.22, 264.78).Seq

最后一个琐碎的步骤是使用put例程打印数值。如果你做了前面的功课,你就会知道 say 使用 gist 方法(在序列周围加上括号)来可视化结果,而 put 只是简单地使用 Str 方法打印数值。

让我们在脚本中再添加几个字符,以演示如何跳过第一列,例如,包含月份名称。

Jan 100.20 303.50 150.25
Feb 130.44 1505.12 36.41
Mar 200.12 305.60 78.12

你只需要做一个切片,选择除第一列以外的所有列。

put 'Total ', [Z+] lines.map: *.words[1..*]

如你所见,我们甚至不需要自己去数列。1..* 的范围可以使这项工作。

3.11. 数字之和等于各数字的阶乘之和

所以,我们的任务是找到所有数字的总和,这些数字等于其数字的阶乘之和。听起来清楚了吗?你可以看一下解题单行程序,以便更好地理解它。

say [+] (3..50_000).grep({$_ == [+] .comb.map({[*] 2..$_})})

让我们从 …​ 开始。

我们在 3 .. 50_000 的范围内循环。上界是基于一些考虑的猜测。我在这里就不解释了,但如果你好奇的话,可以试着去找答案。基本上,在某些时候你就明白了,这个数字要么是包含了太多的数字,要么就是本身太大了。

第二步是 grep。我们要搜索的数字,是等于($_ ==)和的。它的计算方法是第二次还原加 [+],但你可以用和的方法代替。

{$_ == .comb.map({[*] 2..$_}).sum}

请注意,.comb 是一个调用默认变量 $_ 的方法。梳理方法将数字拆分开来(将它们视为字符)。map 方法将每个数字转换为阶乘(同样,使用还原运算符 [*])。

最后,最外 [+] 将所有 grepped 数字相加,并将结果传递给 say 例程。

虽然本书讲的是单行程序,但在使用之前,单独用一行代码来准备阶乘,会比较实用。

my @f = (0..9).map({[*] 1..$_});
say [+] (3..50_000).grep({$_ == [+] .comb.map({@f[$_]})});

3.12. 通过立方体

你可能已经看到了得到42的精确值的计算,即生命、宇宙和万物的答案。让我把它复制到这里,利用乐乐的强大和它的任意精密运算,更不用说在代码中直接使用上标的乐趣了。

$ time raku -e'say 80435758145817515³ - 80538738812075974³ + 12602123297335631³'

42

real 0m0.151s
user 0m0.173s
sys 0m0.035s

4. 使用字符串

4.1. 生成随机密码

下面是解决这个问题的完整代码:

('0'..'z').pick(15).join.say

运行几次:

Z4x72B8wkWHo0QD
J;V?=CE84jIS^r9
2;m6>kdHRS04XEL
O6wK=umZ]DqyHT5
3SP\tNkX5Zh@1C4
PX6QC?KdWYyzNOc
bq1EfBZNdK9vHxz

我们的代码行每次运行都会生成不同的字符串。如果你还没有安装 Raku,请尝试自己创建一个密码,或者使用上面显示的密码之一。

你也可以问一个问题。密码中会有哪些字符? 很高兴你能问到这个问题! Raku 是一种面向 Unicode 的语言,'0'..'z' 的范围似乎包含了具有不同 Unicode 属性的字符(即,至少是数字和字母)。要想知道里面的内容,只要把我们今天的代码中的 pick 方法去掉就可以了。

('0'..'z').join.say

这一行打印两个给定字符之间的 ASCII 表子集。

0123456789:;<=>?@
ABCDEFGHIJKLMNOPQRSTUVWXYZ[\]^_`
abcdefghijklmnopqrstuvwxyz

这些是密码中可能出现的字符。选取法可以确保字符不会重复出现。如果使用 roll,则允许字符出现一次以上。

4.2. Unicode 的乐趣

这里的代码使用的是 ASCII 地外的四个字符。

say π × $ρ²

在 Raku 中,你可以在标识符中自由使用 Unicode 字符,例如变量或函数名。但除此之外,还有许多预定义的符号,如 π,它们有 ASCII 的替代符号。检查名为 "Unicode 与 ASCII 符号"的文档页面,以查看可以在 Raku 中使用的 Unicode 字符的全部集合。

只使用 ASCII 码,上面的单行程序可以按以下方式重新编写。

say pi * $r ** 2

让我们回到上一章的一个帮助程序,看看哪里可以使用 Unicode 字符。

sub f($n) {
    ($n <<*>> (1...1000 / $n)).grep: * < 1000
}

say (f(3) ∪ f(5)).keys.sum;

这里有几个机会。

首先,超运算符 <<*>> 可以用适当的法式引号代替。"*",乘法字符可以用我们已经用过的十字形来代替,"×"。«×». 除法同样可以用:"÷"。

其次,序列运算符的三个点可以用一个 Unicode 字符代替:…​(如果你是在 Word 中编程,在输入三次句号后就会自动得到这个字符)。

最后,在最后一行中,一个 Unicode 字符 ∪ 被用来寻找两个集合的交集。这里的字符和你在数学中使用的字符是一样的(你呢?),但你可以使用它的 ASCII 版本来代替:f(3) (|) f(5)。

在编程语言中,Unicode 的支持是最好的,如果不是最好的话。使用时要注意不要让别人为你的代码而疯狂!

5. 使用日期

5.1. 今天是几号?

今天,我们就来回答一下今天是什么日子的问题。

所以,要打印答案,可以使用下面一行 Raku 的代码。

DateTime.now.yyyy-mm-dd.say

它看起来是透明的,并以 YYYY-MM-DD 的格式打印日期。好的部分是 DateTime 类可以直接使用,你不需要导入任何模块。

$ raku -e'DateTime.now.yyyy-mm-dd.say'
2019-10-17

正如你在前面的章节中已经看到的,链式方法调用在 Raku 中是一种常见的做法。另一种相反的做法是将 say 作为一个子程序,并在方法名后面使用括号。

say(DateTime.now().yyyy-mm-dd());

这段代码也可以用,它是完全正确的,但它看起来很重。

你还应该注意到并告诉你的朋友,在乐道中,你可以在标识符中使用破折号和省略号。

好吧,也许使用省略号并不是一个好主意,但连字符在乐都的源代码中已经被非常广泛地使用了。只要确保在算术表达式中的减号运算符周围加上空格,就可以避免在解析中出现任何冲突。

如果你阅读文档,你会发现另一个以同样方式命名的方法:hh-mm-ss。我打赌你一定明白它的作用。

> DateTime.now.hh-mm-ss.say
00:12:01

请注意,对于不同格式的输出,如 dd-mm-yy 或 hh-mm,你不会找到类似的方法。 使用 formatter 代替。它不是一个方法,而是在 Datish 角色中定义的一个属性。在 DateTime 类中有一个默认的 formatter,但你可以通过提供构造函数与你自己的子程序来重新定义它,例如。

DateTime.now(formatter => -> $dt {
    sprintf '%02d.%02d.%04d',
    $dt.day, $dt.month, $dt.year
}).say

这里的 formatter 接收一个匿名的子程序(由一个细小的箭头引入),有一个参数 $dt

我希望这段代码打印的日期和我们最初的单行程序一样,因为你很可能在一天之内读完了整节内容。

5.2. 本世纪有多少天符合条件的?

我们接下来的单行程序比较长,最好写成两行,但它会显示出 Raku 的 Date 对象的一个非常好的特点:它可以很容易地在一个范围内使用。

实质上,这个任务是计算 1901年1月1日到 2000年12月31日之间的星期天,并且只计算落在月份第一的星期天。

Raku 中的 Date 对象实现了 succ 和 prec 方法,用于递增和递减日期。也可以使用两个日期作为一个范围的边界。

say (
    Date.new(year => 1901) ..^ Date.new(year => 2001)
).grep({.day == 1 && .day-of-week == 7}).elems

这里有几个时刻要评论一下。

首先,两个 Date 对象是用一个命名的参数,即年份来创建的。之所以能够做到这一点,是因为构造函数的签名中包含了月份和日期的去错值。

multi method new(
    Date: Int:D() :$year!,
    Int:D() :$month = 1, Int:D() :$day = 1,
    :&formatter, *%_) {

    ...
}

所以,创建1月1日的日期很容易,但是对于一年的最后一天就不能这么做了。但是 Raku 有一个很好的范围运算符 ..^,它可以排除右边的边界,让我们节省不少字符(虽然我们还没有玩 Raku Golf,但这是第7章的主题)。

较长的版本,包括所有明确的日期部分,会像这样。

say (
    Date.new(year => 1901, month => 1, day => 1) ..
    Date.new(year => 2000, month => 12, day => 31)
).grep({.day == 1 && .day-of-week == 7}).elems

我们创建了一个范围,并使用一个组合条件来 grep 它的值。请记住,当你想在缺省变量上调用一个方法时,不需要显式地输入 $_。

另一种方法是使用两个带星号的 grep。

say (
    Date.new(year => 1901, month => 1, day => 1) ..
    Date.new(year => 2000, month => 12, day => 31)
).grep(*.day == 1).grep(*.day-of-week == 7).elems

你可以在家里做一个练习。打印出今年年底前的剩余天数。

5.3. 同一问题的另一种解决方案

下面是另一个问题的解决方法。任务是算出XX世纪中所有落在每月一号的星期天。

上一次,我们只是在扫描整个世纪的所有日子,选择那些是星期天(.day-of-week == 7),并且是本月第一天(.day == 1)的日子。

可以做一个更有效的算法。因为我们只对每月的第一天感兴趣,所以没有必要扫描100年的所有36525天。只需要扫描1901年到2000年之间每个月的第一天的1200天就够了。

所以,我们需要两个嵌套循环:过年和过月。我们需要两个 for 吗?不一定。让我们使用X运算符(详见第6章)。

这就是我们的新单行程序。

(gather for 1901..2000 X 1..12 {
    take Date.new(|@_, 1)
}).grep(*.day-of-week == 7).elems.say;

for 1901…​2000 X 1…​12 循环将XX世纪的每个月都过一遍。对于每个月,我们通过调用一个有三个参数的构造函数来创建一个Date对象。

注意,在循环中,你可以同时使用 $_[0] 和 $_[1],以及 @_[0] 和 @_[1]。在第一种情况下,$_ 变量包含两个元素的列表,而在第二种情况下,它是一个数组 @_。如果你只是用一个点来调用主题(默认)变量上的方法:.[0] 和 .[1],则可以实现最短的代码。

你可以键入 Date.new(.[0], .[1], 1) 来代替 Date.new(|@_, 1)。|@_ 语法是用来展开数组的,否则 Raku 会认为你传递的是一个数组作为第一个参数。

所有月份的第一天都会在 gather-take 对的帮助下以一个序列收集。

最后一步是和之前一样的 grep,但这次我们只需要选择太阳日,所以一个 *.day-of-week ==7 的条件就足够了。

elems 方法的调用会返回列表中元素的数量,也就是我们要找的星期天的数量。因此,用 say 打印出来。

根据读者的一个非常贴切的评论,这里有一个更好更简单的解决方法,即使用 + 前缀操作符将一个列表投向其长度。

say +(1901..2000 X 1..12).map(
    {Date.new(|@_, 1)}
).grep(*.day-of-week == 7);

6. Raku 语法

6.1. X 和 .. 和 …​

在前面的章节中,我们经常使用带有交叉操作符的构造。下面是几个例子。

(999...100) X* (999...100)
1..10 X* 1..10

第二行打印1到10的数字的乘法表的项。

(1 2 3 4 5 6 7 8 9 10 2 4 6 8 10 12 14 16 18 20 3 6 9 12 15 18 21 24 27 30 4 8 12 16 20 24 28 32 36 40 5 10 15 20 25 30 35 40 45 50 6 12 18 24 30 36 42 48 54 60 7 14 21 28 35 42 49 56 63 70 8 16 24 32 40 48 56 64 72 80 9 18 27 36 45 54 63 72 81 90 10 20 30 40 50 60 70 80 90 100)

关于这样的构造,有一些东西需要学习。

你可能已经注意到 999…​100 和 1..10 之间有两个微妙的区别。在一种情况下,有三个点,在另一种情况下,只有两个点。在第一种情况下,边界是递减的数字,在第二种情况下,边界是递增的。

Raku 中的两个点是范围运算符。它创建一个 Range,或者换句话说,一个 Range 类型的对象。

三个点是序列操作符。在我们的例子中,它创建了一个序列,或者 Seq 类型的对象。

你可以随时通过调用对象上的 WHAT 方法来检查类型(下面是 REPL 会话的一个片段)。

> (1..10).WHAT
(Range)

> (1...10).WHAT
(Seq)

研究一下 Raku 的源代码,看看 Range 和 Seq 有哪些属性,也很有意思。

my class Range is Cool does Iterable does Positional {
    has $.min;
    has $.max;
    has int $!excludes-min;
    has int $!excludes-max;
    has int $!infinite;
    has int $!is-int;
    ...
}

my class Seq is Cool does Iterable does Sequence {
    has Iterator $!iter;
    ...
}

在 Range 对象中,有最小值和最大值,而在 Seq 中,我们看到的是一个迭代器。

在某些情况下,例如,在一个简单的循环中,你可以选择范围或序列。

.say for 1..10;
.say for 1...10;

但如果你想往下数,范围就不行了。

> .say for 10..1;
Nil

有了序列,就没有问题了。

> .print for 10...1;
10987654321>

在你开始好好感受它们之前,玩玩范围和序列是明智的。将它们保存在变量中,在操作中使用,用 say 或 dd 打印。

作为一个起点,考虑以下三个操作。

(1, 2, 3) X* (4, 5, 6);
(1..3) X* (4..6);
(1...3) X* (4...6);

6.2. 化简运算符

我们的下一个客人是一个带有一对方括号的化简结构。当它们不包围一个数组索引时,它们在一个完全不同的领域工作。

6.2.1. 例子一: 阶乘

最经典的例子就是用化简运算符计算阶乘。

say [*] 1..2019

[ ] 是一个化简元运算符。名称中的元部分告诉我们,它可以作为另一个运算符的包络符(顺便说一下,不仅仅是一个运算符)。

在第一个例子中,运算符包含了另一个运算符,通过将范围注册为一个列表,并在所有元素之间放置 *,整个行可以被重新编写。

say 1 * 2 * 3 #`(more elements) * 2018 * 2019

6.2.2. 例子二: 使用函数

现在,让我们找出最小的数字,它可以被1到20的所有数字所除。

让我用 Raku 给你看一个直接的答案。

say [lcm] 1..20

这段代码看起来和前面的例子非常相似,但是使用了另一个运算器,lcm 例程,它是一个内置的中缀运算符,它的名字是最小公倍数,但是在文档中,你也可以读到它返回两个参数均等除以的最小整数。这个名字代表了最小公倍数,但在文档中,你也可以读到它返回的是被两个参数均分的最小整数。几乎相同的话,就是用来制定我们解决的问题。

say 1 lcm 2 lcm 3 lcm 4 lcm 5 lcm 6 lcm 7 # ... and up to 20

6.2.3. 例子三: 矩阵

其他已经内置在 Raku 中的中缀运算符,也是非常有优势的。下面是一个只用几个字符的代码就可以旋转矩阵的例子。

[Z] <A B C>, <D E F>, <H I J>

这里,我们要变换的是一个有9个元素的二维矩阵,也就是A到J的一字串,在输出中,行变成列,列变成行。

((A D H) (B E I) (C F J))

列表中的元素之间插入了zip后缀操作符Z,因此代码类似于下面的内容。

<A B C> Z <D E F> Z <H I J>

注意,如果你想强调操作顺序,你可能得到的不是你想要的。

好了,在我们没有对 Lisp 走得太远之前,我们换个话题。

6.3. Raku 中的星号

在本节中,我们将介绍使用 * 字符的构造。在 Raku 中,你可以根据上下文将其称为星号(或星号,如果你喜欢的话)或任何东西。

Raku 不是一种隐秘的编程语言。在另一方面,有些领域需要花费时间来开始对语法有信心。

让我们来看看 的不同用例,从最简单的开始,争取了解最伤脑筋的,比如 ** *

前面几个用法很简单,不需要多做评论。

6.3.1. 乘法运算符

单星用于乘法。严格来说,这是一个中缀运算符,其返回值是 Numeric。

say 20 * 18; # 360

6.3.2. 乘方运算符

双星 ** 是指数运算符。同样,这是一个引号:返回 Numeric 结果,计算给定两个值的幂。

say pi ** e; # 22.4591577183611

6.3.3. 正则重复量词

同样的两个标记,*,也被用在 regexes 中,它们意味着不同的东西。Raku 的一个特点是,它可以很容易地在自身内部的不同子语言之间进行切换。regexes 和 grammar 都是这种内部语言的例子,同样的符号在"主"语言中的含义可能不同(如果它们在那里有任何意义的话)。

量词 *。这个语法项的工作方式与一般 Perl 兼容的 regex 中的行为类似:允许原子的零次或多次重复。

my $weather = '*****';
my $snow = $weather ~~ / ('*'*) /;
say 'Snow level is ' ~ $snow.chars; # Snow level is 5

当然,我们在这里也看到了同一字符的另一种用法,即 '*' 的字面意思。

6.3.4. 最小到最大重复

** 是另一个量化符的一部分,指定最小和最大的重复次数。

my $operator = '..';
say "'$operator' is a valid Raku operator"
    if $operator ~~ /^ '.' ** 1..3 $/;

在这个例子中,预计点的重复次数是一、二、三,不能少,也不能多。

我们再往前看一点,在 Whatever 符号的角色(角色如戏曲中的角色,而不是乐乐的面向对象编程中的角色)中使用一个星。

my $phrase = 'I love you......';

say 'You are so uncertain...'
    if $phrase ~~ / '.' ** 4..* /;

范围的第二端是开放的,regex 接受所有包含四个点以上的短语。

6.3.5. 吞噬参数

在子签名中的数组参数前的星号意味着一个粗暴的参数—​将单独的标量参数消耗成一个数组。

list-gifts('chocolade', 'ipad', 'camelia', 'raku');

sub list-gifts(*@items) {
    say 'Look at my gifts this year:';
    .say for @items;
}

散列值还允许庆祝吞噬参数。

dump(alpha => 'a', beta => 'b'); # Prints:
                                 # alpha = a
                                 # beta = b

sub dump(*%data) {
    for %data.kv {say "$^a = $^b"}
}

请注意,如果你在函数签名中省略了星号,代码就不会被编译,因为 Raku 期望的正是宣布的内容。

Too few positionals passed; expected 1 argument but got 0

6.3.6. Slurpy-slurpy

**@ 也能用,但注意传递数组和列表时的区别。

用一颗星。

my @a = < chocolade ipad >;
my @b = < camelia raku >;

all-together(@a, @b);
all-together(['chocolade', 'ipad'], ['camelia', 'raku']);
all-together(< chocolade ipad >, < camelia raku >);

sub all-together(*@items) {
    .say for @items;
}

目前,每份礼物都会被打印在单独的一行上,而不考虑通过参数列表的方式。

用双星。

keep-groupped(@a, @b);
keep-groupped(['chocolade', 'ipad'], ['camelia', 'camel']);
keep-groupped(< chocolade ipad >, < camelia camel >);

sub keep-groupped(**@items) {
    .say for @items;
}

这次,@items 数组只得到两个元素,反映了参数的结构类型。

[chocolade ipad]
[camelia raku]

(chocolade ipad)
(camelia raku)

6.3.7. 动态作用域 twigil

* twigil,它引入了动态作用域。很容易将动态变量与全局变量混淆,但检查以下代码。

sub happy-new-year() {
    "Happy new $*year year!"
}

my $*year = 2020;
say happy-new-year();

如果省略了星号,代码就无法运行。

Variable '$year' is not declared

唯一的办法是将 $year 的定义移到函数定义之上,使其正确。有了动态变量 $*year,调用函数的地方就定义了结果。$*year 变量在子的外部作用域中是不可见的,但在动态作用域中是相当可见的。

对于一个动态变量来说,是给一个现有的变量赋新值还是创建一个新的变量并不重要。

sub happy-new-year() {
    "Happy new $*year year!"
}

my $*year = 2018;

say happy-new-year();     # 2018

{
    $*year = 2019;        # New value
    say happy-new-year(); # 2019
}

{
    my $*year = 2020;     # New variable
    say happy-new-year(); # 2020
}

say happy-new-year();     # 2019

6.3.8. 编译器变量

例如,Raku 自带的一些动态伪常数。

say @*ARGS;      # Prints command-line arguments
say %*ENV<HOME>; # Prints home directory

6.3.9. 所有以这个名字命名的方法

.* 后缀伪操作符调用所有给定名称的方法,可以为给定对象找到,并返回一个结果列表。在微不足道的情况下,你会得到一个学理上荒谬的代码。

> pi.gist.say
3.141592653589793

> pi.*gist.say
(3.141592653589793 3.141592653589793e0)

.* 后缀的真正威力来自于继承。它有时有助于揭示真相。

class Present {
    method giver() {
        'parents'
    }
}

class ChristmasPresent is Present {
    method giver() {
        'Santa Claus'
    }
}

my ChristmasPresent $present;

$present.giver.say;             # Santa Claus
$present.*giver.join(', ').say; # Santa Claus, parents

虽然只是一星半点,但差别很大啊!

现在,说说 Raku 星角最神秘的部分。接下来的两个概念,Whatever 和 WhateverCode 类,很容易互相混淆。让我们试着去做对。

6.3.10. Whatever

一个单一的 * 可以代表 Whatever。Whatever 是一个预定义的类,它在一些有用的情况下引入了一些规定的行为。

例如,在范围和序列中,最后的 * 意味着无穷大。我们已经看到过这样的例子。下面是另一个例子。

.say for 1 .. *;

这个单行本的能量转换效率真的很高,因为它产生了一个无限的递增整数列表。准备好后按 Ctrl+C 键继续前进。

范围 1 .. *1 .. Inf 是一样的。

回到我们更实际的问题上,让我们创建一个自己的类来使用 Whatever 符号 *。下面是一个简单的例子,它是一个多方法的类,可以使用 Int 值或 Whatever。

class N {
    multi method display(Int $n) {
        say $n;
    }

    multi method display(Whatever) {
        say 2000 + 100.rand.Int;
    }
}

在第一种情况下,该方法只是简单地打印数值。第二种方法则是打印一个 2000 到 2100 之间的随机数。由于第二种方法的唯一论据是 Whatever,所以在签名中不需要变量。

下面是你如何使用这个类。

my $n = N.new;

$n.display(2020);
$n.display(*);

第一个调用呼应其参数,而第二个调用则打印一些随机的东西。

Whatever 符号可以作为一个裸的 Whatever 来举行。比如说,你创建了一个 echo 函数,并将 * 传递给它。

sub echo($x) {
    say $x;
}

echo(2018); # 2018
echo(*);    # *

这一次,没有神奇的事情发生,程序打印出了一颗星星。而现在,我们已经到了一个小小的事情改变了很多的地步。

6.3.11. WhateverCode

最后,该说说 WhateverCode 了。

取一个数组,打印其中的最后一个元素。你不能用 @a[-1] 这样的类型来做,因为它会产生错误。

Unsupported use of a negative -1 subscript to index from the end; in Raku please
use a function such as *-1

编译器建议使用 *-1 这样的函数。它是一个函数吗?是的,更准确地说,是一个 WhateverCode 块。

say (*-1).WHAT; # (WhateverCode)

现在,打印一个数组的后半部分。

my @a = < one two three four five six >;
say @a[3..*]; # (four five six)

数组的索引范围是 3..。范围右端的 Whatever 星号指示从数组中取出其余的部分。3.. 的类型是 Range

say (3..*).WHAT; # (Range)

最后,少取一个元素。我们已经看到,要指定最后一个元素,必须使用 *-1 这样的函数。同样也可以在一个范围的右端进行。

say @a[3 .. *-2]; # (four five)

这时,就发生了所谓的 Whatever 柯里化,一个 Range 变成了一个 WhateverCode。

say (3 .. *-2).WHAT; # (WhateverCode)

WhateverCode 是一个内置的类名;它可以很容易地用于方法的分派。让我们更新上一节的代码,并添加一个方法变体,以期望一个 WhateverCode 参数。

class N {
    multi method display(Int $n) {
        say $n;
    }

    multi method display(Whatever) {
        say 2000 + 100.rand.Int;
    }

    multi method display(WhateverCode $code) {
        say $code(2000 + 100.rand.Int);
    }
}

现在,参数列表中的星星落入 display(Whatever)display(WhateverCode)

N.display(2018);     # display(Int $n)
N.display(*);        # display(Whatever)
N.display(* / 2);    # display(WhateverCode $code)
N.display(* - 1000); # display(WhateverCode $code)

再次,看一下 display 方法的签名。

multi method display(WhateverCode $code)

$code 参数被用作方法内部的函数引用。

say $code(2000 + 100.rand.Int);

函数需要一个参数,但它要去哪里呢?或者换句话说,函数主体是什么,在哪里?我们把这个方法称为 N.display(* / 2)N.display(* - 1000)。答案是,* / 2* - 1000 都是函数! 还记得编译器关于使用 *-1 等函数的提示吗?

这里的星号成为第一个函数参数,因此 * / 2 相当于 {$^a / 2},而 * - 1000 相当于 {$^a - 1000}

这是否意味着 $^b 可以用在 $^a 的旁边? 当然可以! 让一个 WhateverCode 块接受两个参数。如何表示其中的第二个参数?不出意外,再加一个星号! 让我们把显示方法的第四个变体添加到我们的类中。

multi method display(WhateverCode $code
                     where {$code.arity == 2}) {
    say $code(2000, 100.rand.Int);
}

这里,where 块用来缩小调度范围,只选择那些有两个参数的 WhateverCode 块。做到这一点后,在方法调用中允许有两个星号。

N.display( * + * );
N.display( * - * );

调用定义了用于计算结果的函数 $code。所以,N.display( * + * ) 后面的实际操作如下。2000 + 100.rand.Int

需要更多的雪花?增加更多的星星。

N.display( * * * );
N.display( * ** * );

同样,里面的实际计算也是。

2000 * 100.rand.Int

2000 ** 100.rand.Int

恭喜你!你现在可以像编译器一样毫不费力地解析 * ** * 构造。

6.3.12. 家庭作业

我们来做一个练习,回答下面的问题。在下面的代码中,每颗星星的含义是什么?

my @n = ((0, 1, * + * ... *).grep: *.is-prime).map: * * * * *;
.say for @n[^5];

D’oh. 我建议我们开始修改代码,去掉所有的星星,用不同的语法。

序列操作符后面的 * 是指生成序列的意思,所以用 Inf 代替。

((0, 1, * + * ... Inf).grep: *.is-prime).map: * * * * *

生成函数中的两颗星 * + * 可以用一个有两个显式参数的 lambda 函数代替。

((0, 1, -> $x, $y {$x + $y} ... Inf).grep:
    *.is-prime).map: * * * * *

现在,一个简单的语法交替。将 .grep: 替换为一个带括号的方法调用。它的参数 .is-prime 变成了一个代码块,星号被替换为默认变量 $_。注意,在代码使用 时,不需要大括号。

(0, 1, -> $x, $y {$x + $y} ... Inf).grep({
    $_.is-prime
}).map: * * * * *

最后,对于 .map 来说,同样的技巧:但这次这个方法有三个参数,因此,你可以写 {$^a * $^b * $^c} 而不是 * * * * *,这里是完整程序的新布局。

my @n = (0, 1, -> $x, $y {$x + $y} ... Inf).grep({
    $_.is-prime
}).map({
    $^a * $^b * $^c
});

.say for @n[^5];

现在很明显,代码打印的是三个质数斐波那契数组的五个乘积。

6.3.13. 其他任务

在课本中,最具挑战性的任务都会用 * 号来标示。下面为大家介绍几个,供大家自己解决。

  • 1. chdir('/')&*chdir('/') 的区别是什么?

  • 2. 解释下面的代码,并修改它以证明它的优点: .say for 1…​**

6.4. EVAL 例程

EVAL 例程编译并执行它作为参数得到的代码。

让我们从评估一个简单的程序开始。

EVAL('say 123');

这个程序说的是 123,这里就不奇怪了。

不过,也有更复杂的情况。你觉得,下面的程序是怎么打印的?

EVAL('say {456}');

我想它打印出来的东西和你想象的不一样。

-> ;; $_? is raw { #`(Block|140570649867712) ... }

它将大括号之间的内容解析为一个尖锐的块。

如果你试试双引号呢?

EVAL("say {789}");

现在它甚至拒绝编译。

===SORRY!=== Error while compiling eval.pl
EVAL is a very dangerous function!!! (use the MONKEY-SEE-NO- EVAL pragma to override this error,
but only if you're VERY sure your data contains no injection attacks)
at eval.raku:6
------> EVAL("say {789}") ⏏;

我们可以通过添加几个神奇的词来修复代码。

use MONKEY-SEE-NO-EVAL;

EVAL("say {789}");

这一次,它打印的是 789。

代码的执行(我们还不知道具体时间,这是明天文章的主题),所以你可以进行一些计算,例如。

use MONKEY-SEE-NO-EVAL;

EVAL("say {7 / 8 + 9}"); # 9.875

最后,如果你尝试直接传递一个代码块,即使是盲猴,也无法再次实现目标。

use MONKEY-SEE-NO-EVAL;

EVAL {say 123};

该错误发生在运行时。

Constraint type check failed in binding to parameter '$code'; expected anonymous constraint to be met but got
-> ;; $_? is raw { #`...
in block <unit> at eval.raku line 10

这条消息看起来很神秘,但至少我们再次看到,我们得到了一个匿名的尖块传递给函数。

在我们结束之前,尝试使用一个小写的函数名。

eval('say 42');

在 Raku 中没有这样的功能,我们会得到一个标准的错误信息。

===SORRY!=== Error while compiling eval2.raku Undeclared routine:
eval used at line 5. Did you mean 'EVAL', 'val'?

7. Raku 高尔夫

在高尔夫编程的内容中,你正在尝试使用尽可能少的字符来解决一个问题。最短的程序获胜。

7.1. 第一个测试

让我们玩高尔夫,打印所有 100 以下的质数。我的解决方案,需要 22 个字符,如下。

.is-prime&&.say for ^C

在 Raku 中没有更短的解决方案,而在 J 编程语言中,他们只用了 11 个字符。在 Raku 中,方法名已经包含了 8 个字符。我相信,为了赢得所有的高尔夫比赛,你需要一种特殊的语言,它的名字非常短(J就是这样),并且需要一套内置的例程来生成素数、斐波那契或任何其他数字序列的列表。它还应该大力利用 Unicode 字符空间。

在我们的 Raku 例子中,也有一个 Unicode 字符,.Ⅽ。这不是一个简单的 Ⅽ,即拉丁字母表的第三个字母,而是一个 Unicode 字符 ROMAN NUMERAL ONE HUNDRED(当然,它最初是拉丁字母表的第三个字母)。使用这个符号让我们在解决方案中保存了两个字符。

之所以能够使用 && 技巧,是因为如果第一个操作数为 False,则布尔表达式的第二部分不会被执行。请注意,这里不能使用单个 &。完整的非优化版本的代码将需要额外的空格,看起来像这样。

.say if .is-prime for ^100

7.2. 第二个测试

让我们解决另一个高尔夫任务,并打印前 30 个斐波那契数,一行一个。我们必须在代码中使用尽可能少的字符。

第一种方法比较啰嗦(即使用 ^31 代替 0..30,也需要 33 个字符)。

.say for (0, 1, * + * ... *)[^31]

有一些空间,允许压缩。当然,首先也是最明显的就是去掉空格(剩余28个字符)。

.say for (0,1,*+*...*)[^31]

另一个有趣的技巧是使用 >> 元操作符来调用序列中每个元素的 say 方法。它将代码进一步压缩到 24 个字符。

(0,1,*+*...*)[^31]>>.say

此刻,我们可以采用一些 Unicode,又获得了三个字符(剩下21)。

(0,1,*+*...*)[^31]».say

看起来已经很紧凑了,但还是有一些选项可以尝试。让我们去掉显式切片,并尝试在最后一个元素处停止序列。

(0,1,*+*...*>514229)».say

代码变长了(23个字符),但我们不需要一个确切的数字 514 229。给出一些接近的数字就足够了,这个数字比序列的第29个元素大,比第30个元素小。例如,它可以是 823 543,也就是7的幂7,用上标(19个字符)写下来。

(0,1,*+*...*>7⁷)».say

最后,可以通过使用另一个代表 80万的 Unicode 字符,让它少一个字符。并不是每一种(如果有的话)字体都能显示一些有视觉吸引力的东西,但乐乐毫无怨言地接受了这个字符。

(0,1,*+*...*>)».say

这18个字符比 code-golf.io 网站的最高结果长了一个字符。我有一种感觉,你可以通过将序列的前两个元素替换为 ^2 来获得另一个字符,但这在当前的 Rakudo 中是行不通的,你必须返回一个字符来扁平化列表。|^2, 这使得解决方案又变成了18个字符.

欲望将条件中的 *> 部分去掉,停止序列,用一个固定的数字代替。不幸的是,没有办法用1和90之间的数字的幂来表达832 040。能不能这样,我们可以用上标来计算这个数字。我们可以用上标来计算这个数字。

另一个想法是使用 regex,但我们在这里至少需要四个字符,这没有帮助。

(0,1,*+*.../20/)».say

但我在这里就不说了,让你再想一想。

7.3. 拉库高尔夫码的技巧和想法

7.3.1. 省略分号

在程序结束前的语句末尾或代码块末尾不需要分号。

say 42;

7.3.2. 省略主题变量

如果在主题变量 $_ 上调用了一个方法,那么变量的名字其实并不需要乐乐来理解你在说什么,所以,避免给主题变量明确命名。

$_.say for 1..10

7.3.3. 使用前缀形式

在许多情况下,一个操作同样可以表达为一个代码块或一个循环或条件的后缀形式的单一操作。后缀形式通常比较短。例如

for 1..10 {.say}
.say for 1..10

7.3.4. 使用范围制作循环

范围是表达循环细节的好东西:在几个字符中,你就可以确定循环变量的初始和最终状态。

.say for 1..10

但是想想如果你可以从0开始数,在这种情况下,可以用一个小括号字符来得到从0开始的范围,下面的代码可以打印0到9的数字。

.say for ^10

7.3.5. 在范围和序列之间进行选择

在循环中,序列的工作方式与范围的工作方式完全相同。这种选择可能取决于高尔夫软件是计算字节还是 Unicode 字符。在第一种情况下,一个范围中的两个点比一个范围中的三个点要好。在第二种情况下,使用 Unicode 字符。

.say for 1..10
.say for 1...10
.say for 1...10

当你需要向下计数时,序列是你的朋友,因为它们可以推断出改变循环计数器的方向。

.say for 10...1

7.3.6. 使用 map 代替循环

在某些情况下,特别是当你必须对循环变量进行多个操作时,可以尝试使用 map 来迭代所有的值。

(^10).map: *.say

7.3.7. 省略圆括号和引号

Raku 并不强迫你在常规形式的条件检查中使用括号。

if ($x > 0) {say $x;exit}

有时,你也会希望在函数或方法调用中省略括号。

say(42)
say 42

在声明数组或哈希时,都不需要括号。对于数组,在上面使用引号结构来避免引号。

my @a = ('alpha', 'beta')
my @b=<alpha beta>

7.3.8. 使用链式比较

另一个有趣的功能是在一个表达式中使用一个以上的条件。

say $z if $x < 10 < $y

7.3.9. 在方法和函数之间选择

在许多情况下,你可以在调用函数和使用方法之间进行选择。方法调用可以额外地在彼此之后进行链式调用,所以你可以节省大量的括号或空格。

(^10).map({.sin}).grep: *>0

当同时存在一个方法和一个独立的函数时,如果省去括号,方法调用往往会更短,或者至少是相同的长度。

abs($x)
abs $x
$x.abs

7.3.10. 使用 Unicode 字符

操作符通常有其 Unicode 对应的字符,你可以用一个字符表达一个冗长的结构。比较一下。

if $x=~=$y
if $x≅$y

在 Unicode 空间中也有内置常数,例如 pi 与 π,或 Inf 与 ∞。

有很多数字,无论大小,都可以用一个 Unicode 符号代替。1/3 与 ⅓,或 20 与 ⑳,或 100 与 Ⅽ。

7.3.11. 使用上标

上标是计算幂的好帮手。比较。

say $x**2
$x².say

7.3.12. 使用 \ 制作无符号变量

不要忘了以下绑定容器的方式,并创建一种无标号变量。

my \a=42;say a

7.3.13. 使用默认参数

当你在使用函数或类方法时,检查它们的签名中是否有默认值。同时检查是否有一个带有位置参数的替代变体。例如,比较三种创建日期对象的方法。

Date.new(year=>2019,month=>1,day=>1)
Date.new(year=>2019)
Date.new(2019,1,1)

7.3.14. 使用 && 代替 if

布尔表达式可以节省几个字符,因为如果第一个条件已经给出了结果,Raku 将不会计算第二个条件。例如

.say if $x>0

$x>0&&.say

7.3.15. 在 put 和 say 之间选择

最后,有时用 put 代替 say 更好。在某些情况下,当你打印数组时,你将在输出中不需要括号,例如:在另外一些情况下,当你处理范围时,你会得到所有的值,而不是简洁的输出,例如。

> say 1..10
1..10

> put 1..10
1 2 3 4 5 6 7 8 9 10

8. 关于编译器内部的附录

8.1. 0.1 + 0.2 的背后是什么?

今天,我们将研究一个计算零的单行本。

say 0.1 + 0.2 - 0.3

如果你熟悉编程,你就会知道,只要你开始使用浮点运算,你就会失去精度,你就会很快面临小错误。

你可能也看到了 0.30000000000000004.com 这个网站,里面有一长串不同的编程语言,以及他们如何打印一个简单的表达式 0.1+0.2。在大多数情况下,你不会得到一个 0.3 的精确值。而且往往当你得到它的时候,它实际上是打印操作过程中四舍五入的结果。

在 Raku 中,0.1+0.2 正好是 0.3,而上面提到的单行本打印的是一个精确的零。

让我们深入到编译器的内部来看看这是如何工作的。Raku 的语法(在乐道编译器中实现)有以下检测数字的片段。

token numish {
    [
    | 'NaN' >>
    | <integer>
    | <dec_number>
    | <rad_number>
    | <rat_number>
    | <complex_number>
    | 'Inf' >>
    | $<uinf>='∞'
    | <unum=:No+:Nl>
    ]
}

很有可能,你对 Raku 已经足够熟悉,你知道上述行为的解释是,它使用有理数来存储浮点数,比如 0.1。这是正确的,但看一下语法,你可以看到这个历程有点长。

语法中所谓的 rat_number 是一个写在角括号里的数字。

token rat_number { '<' <bare_rat_number> '>' }
token bare_rat_number {
    <?before <.[-−+0..9<>:boxd]>+? '/'>
    <nu=.signed-integer> '/' <de=integer>
}

所以,如果你把程序改成:

say <1/10> + <2/10> - <3/10>

那么你就会立即操作有理数。这里有一个相关的操作,可以转换这种格式的数字。

method rat_number($/) { make $<bare_rat_number>.ast }

method bare_rat_number($/) {
    my $nu := $<nu>.ast.compile_time_value;
    my $de := $<de>.ast;
    my $ast := $*W.add_constant(
        'Rat', 'type_new', $nu, $de, :nocache(1));
    $ast.node($/);
    make $ast;
}

在某些时候,抽象语法树得到一个节点,其中包含一个 Rat 类型的常量,$nu$de 部分作为分子和分母。

在我们的例子中,写成 0.1 形式的数字首先通过 dec_number 标记。

token dec_number {
    :dba('decimal number')
    [
    | $<coeff> = [ '.' <frac=.decint> ] <escale>?
    | $<coeff> = [ <int=.decint> '.' <frac=.decint> ]
                 <escale>?
    | $<coeff> = [ <int=.decint> ] <escale>
    ]
}

数字的整数和小数部分进入最终 Match 对象的 <int><frac> 键。这个语法标记的动作方法相当复杂。让我给你看看。

method dec_number($/) {
    if $<escale> { # wants a Num
        make $*W.add_numeric_constant: $/, 'Num', ~$/;
    } else { # wants a Rat
        my $Int := $*W.find_symbol(['Int']);
        my $parti;
        my $partf;

        # we build up the number in parts
        if nqp::chars($<int>) {
            $parti := $<int>.ast;
        } else {
            $parti := nqp::box_i(0, $Int);
        }

        if nqp::chars($<frac>) {
            $partf := nqp::radix_I(
                      10, $<frac>.Str, 0, 4, $Int);

            $parti := nqp::mul_I($parti, $partf[1], $Int);
            $parti := nqp::add_I($parti, $partf[0], $Int);
            $partf := $partf[1];
        } else {
            $partf := nqp::box_i(1, $Int);
        }

        my $ast := $*W.add_constant(
            'Rat', 'type_new', $parti, $partf,
            :nocache(1));
        $ast.node($/);
        make $ast;
    }
}

对于0.1、0.2和0.3这三个数字,上面的代码分别取它们的整数和小数部分,准备好两个整数 $parti$partf,并将它们传递给与我们在 rat_number 动作中看到的相同的新常数的构造函数,之后就得到了一个Rat数。

现在我们跳过一些细节,看看你必须了解的有理数的另一个重要部分。

在我们的例子中,整数和小数部分得到的值如下。

$parti=1, $partf=10
$parti=2, $partf=10
$parti=3, $partf=10

如果你黑掉你本地的乐道文件副本,你可以很容易地自己看到它。

或者,在命令行中使用 --target=parse 选项。

$ raku --target=parse -e'say 0.1 + 0.2 - 0.3'

一部分输出将包含我们想要看到的数据。

- 0: 0.1
- value: 0.1
    - number: 0.1
    - numish: 0.1
        - dec_number: 0.1
        - frac: 1
        - int: 0
        - coeff: 0.1
- 1: 0.2
- value: 0.2
    - number: 0.2
    - numish: 0.2
        - dec_number: 0.2
        - coeff: 0.2
        - frac: 2
        - int: 0

将数字以分数的形式呈现后,进行精确的计算就非常容易了,这就是为什么我们在输出中看到的是一个纯零。

回到我们的分数上。如果你同时打印分子和分母(例如使用裸体法),你会看到,如果可能的话,分数已经被归一化了。

> <1/10>.nude.say
(1 10)
> <2/10>.nude.say
(1 5)
> 0.2.nude.say
(1 5)
> 0.3.nude.say
(3 10)

如你所见,我们有 1/5,而不是 2/10,它代表的是同一个数,精度相同。当你使用这个数字时,你不应该担心找到两个分数的共同分界线,例如。

> (0.1 + 0.2).nude.say
(3 10)

1. Rakudo(rakudo.org)是 Raku 的一个实现。本书的其余部分假设你正在使用 Rakudo 编译器,并在命令行中以 raku 的形式运行它。如果你有一个旧的版本,其中有 perl6 可执行文件,在你的 .profile 中做一个别名:alias raku=perl6。