Wait the light to fall

Raku 圣诞月历(2010)

焉知非鱼

Raku Calendar(2010)

第二天: 用 MAIN 函数控制命令行交互 #

在 UNIX 环境下,很多脚本都是要从命令行里获取运行参数的。在 Raku 中实现这个相当简单。比如下面这样:

$ cat add.pl
sub MAIN ($x, $y) {
    say $x + $y
}
$ raku add.pl 3 4
7
$ raku add.pl too many arguments
Usage:
add.pl x y

只要定义一个带命名变量的 MAIN 函数,你就可以获得一个命令行分析器。然后命令行参数就被自动绑定到 $x$y 上了。如果不匹配,还有温馨的 Usage 提示。

当然,你可能更喜欢自己定制 Usage 信息。那么自己动手,编写 USAGE 函数好了:

$ cat add2.pl
sub MAIN($x, $y) {
    say $x + $y
}
sub USAGE () {
    say "Usage: add.pl <num1> <num2>";
}
$ raku add2.pl too many arguments
Usage: add.pl <num1> <num2>

更进一步的,你可以用 multi 指令声明多个 MAIN 函数以完成一种可替代的语法,或者根据某些常量做出不同反应,比如:

$ cat calc
#!/usr/bin/env raku

multi MAIN('add', $x, $y)  { say $x + $y }
multi MAIN('div', $x, $y)  { say $x / $y }
multi MAIN('mult', $x, $y) { say $x * $y }
$ ./calc add 3 5
8
$ ./calc mult 3 5
15
$ ./calc
Usage:
./calc add x y
or
./calc div x y
or
./calc mult x y

还有命名参数对应不同的选项的情况:

$ cat copy.pl
sub MAIN($source, $target, Bool :$verbose) {
    say "Copying '$source' to '$target'" if $verbose;
    run "cp $source $target";
}
$ raku copy.pl calc calc2
$ raku copy.pl  --verbose calc calc2
Copying 'calc' to 'calc2'

这里申明变量 $verbose 类型为 Bool,也就是不接受赋值。如果没有这个类型约束的话,它是需要赋值的,就像下面这样:

$ cat do-nothing.pl
sub MAIN(:$how = 'fast') {
    say "Do nothing, but do it $how";
}
$ raku do-nothing.pl
Do nothing, but do it fast

$ raku do-nothing.pl --how=well
Do nothing, but do it well

$ raku do-nothing.pl what?
Usage:
do-nothing.pl [--how=value-of-how]

总之,Raku 提供了内置的命令行解析功能和使用帮助说明,你只要声明好函数就行了。

文件操作 #

  • 目录

不再用 opendir 和其他神马滴,Raku 中有专门的 dir 函数,用来列出指定目录(默认是当前所在目录)下所有的文件。好了,直接贴代码:

dir
dir 't' # t 目录下的文件

dir 还有一个可选的命名参数 test,用来 grep 结果,这样:

dir 'src/core', test => any(/^C/, /^P/)

创建目录,还是 mkdir 函数没错啦。

  • 文件

最简单的读取文件的办法,是直接使用 slurp 函数,这个函数以标量形式返回文件的内容,这样:

slurp 'VERSION'

当然原始的文件句柄方式还是有效的,这样:

my $fh = open 'CREDITS'
$fh.getc #读取一个字符
$fh.get  #读取一行(译者注:这两看起来好有 C 语言的赶脚啊)
$fh.close;

$fh = open 'new', :w  # 以可写方式打开
$fh.print('foo')
$fh.say('bar')
$fh.close;
say slurp('new')
  • 文件测试

如果要测试文件是否存在以及文件的具体类型,直接使用 ~~ 操作符就搞定了,还是用代码说话:

'LICENSE'.IO ~~ :e     # 文件(广义的)是否存在
'LICENSE'.IO ~~ :d     # 那么他是目录么?
'LICENSE'.IO ~~ :f     # 那么是文件么(狭义的)?
  • File::Find

如果这些个标准特性还不够,那模块就派上用场了。File::Tools 包里的 File::Find 模块可以递归你指定的目录找你要的东西然后列出来。这个模块应该是跟着 Rakudo Star 一起打包了,如果你只裸装了 Rakudo,那么用 neutro 命令安装也是挺方便的。

额,还是要例子?好吧~很简单的一行 find(:dir, :type, :name(/foo/)),这就会在 t/dir1 目录下,寻找名字匹配 foo 的文件,然后以树的形式列出来~不过要注意的是:这命令的返回可不是文本标量,而是一个个包括他们的完整路径在内的对象,而且还提供文件本身以及文件所在目录的访问器!更多信息,直接看文档吧。

1、创建新文件

open('new', :w).close

2、匿名文件句柄

given open('foo', :w) {
    .say('Hello, world!');
    .close
}

第四天 – 序列操作符 #

去年,有一个序列操作符的简要梳理:

my @even-numbers  := 0, 2 ... *;    # 算术序列
my @odd-numbers   := 1, 3 ... *;
my @powers-of-two := 1, 2, 4 ... *; # 几何序列

这些现在在 Rakudo 里面实现了:

my @powers-of-two := 1, 2, 4 ... *;
@powers-of-two[^10];
# 1 2 4 8 16 32 64 128 256 512

我们需要削减这个无限列表让 Rakudo 不会花费无限长的时间来计算它。这种情况下,我使用 [^10], 这其实是说 “给我前 10 个元素”。注意,当你把一个惰性列表绑定到一个数组变量上时,被计算过的值是会被记忆的,这是一种快捷的缓存。

序列操作符 ... 是一个生成惰性列表的强大工具。上面的例子仅仅暗示了它能做什么。给定一个数字,它就从这个数字开始往下计数(除非序列的终点是一个更小的数字,这种情况下,它会倒数。

1 ... 10; # 1 2 3 4 5 6 7 8 9 10
5 ... 1;  # 5 4 3 2 1

给定两个数字来开始一个序列,它会把这当作一个算术序列,把前两个元素的差异添加到最后一个生成的元素上来产生下一个元素。如果给定三个元素,它会检查它们是否代表一个算术序列的开始或者它是否是一个几何序列,然后继续这个序列。

当然,很多有趣的序列既非算术序列也非几何序列,这时,你需要显式地提供一个 sub 来生成序列中的下一个数:

my @Fibonacci := 0, 1, -> $a, $b { $a + $b } ... *;
@Fibonacci[^10]; # 0 1 1 2 3 5 8 13 21 34

上面的 -> $a, $b { $a + $b } 是一个 pointy block (或者是一个匿名函数),它带有 2 个参数并返回它们的和。这个序列操作符计算出该 block 有多少个参数,然后从当前序列的末尾传递所需的参数来生成序列的下一个数字,以此类推,循环下去。

或者也可以中断循环,目前为止,所有的例子都有一个星号 * 放在右边,它意味着“没有终止条件”。如果你反而在那里放上一个数字,这个列表就会终止在那个数字。

1, 1.1 ... 2;          # 1 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 2
1, 1.1 ... 2.01;       # 1 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 2
(1, 1.1 ... 2.01)[^14] # (1 1.1 1.2 1.3 1.4 1.5 1.6 1.7 1.8 1.9 2 Nil Nil Nil)

第一个列表很自然地终止了,但是第二个列表漏掉了终止数,它会循环下去。结果就是一个无限列表,所以我把它限制到前 14 个元素,以至于我们能明白它正在做什么。

那些有做浮点数学背景的人可能会气急败坏地说反复增加 0.1 直到精确到 2 为止很危险。

在 Raku 中,没有这个问题,因为它会在可能的地方使用有理数(例如.分数)。如果我想找出所有 10000 以下的斐波纳契数,要找到到何处停止的那个精确的数字是很大的问题。幸运的是,就像你能使用块来指定怎样生成序列中的下一个元素一样,你可以使用块来测试序列是否结束:

0, 1, -> $a, $b { $a + $b } ... -> $a { $a > 10000 };
# 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765 10946

尖头块 -> $a { $a > 10000 } 创建了一个含有一个参数的块,并且当参数大于 10000 时返回真;这就是我们需要的测试。

除了我们所期待的所有斐波那契数小于 10000。 我们生成的裴波纳契数有一个大于 10000 的,当传递一个块作为终止测试时,该序列操作符所有的元素直到那个块返回真为止,然后它返回最后一个元素,然后停止。但是有一种替代形式的操作符能做同样的事情:

0, 1, -> $a, $b { $a + $b } ...^ -> $a { $a > 10000 };
# 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765

... 转换为 ...^ 意味着结果列表不包含让终止测试返回真时的第一个元素。

在 Raku 中这真是一种冗长的指定序列的方法。在这里我没有地方解释所谓的闭包,但是去年的文章已经说过它们。使用闭包,你可以将上一个序列写为:

0, 1, * + * ...^ * > 10000;
# 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765

这样写是否清晰完全取决于你,条条大路通罗马。并且,序列操作符的左侧可以是任何列表,甚至是惰性的。这意味着你可以很容易的使用一个终止块来得到已存在的惰性列表的有限的一部分:

my @Fibonacci := 0, 1, * + * ... *;
@Fibonacci ...^ * > 10000;
# 0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987 1597 2584 4181 6765
say @Fibonacci[30]; # 832040

(我坚持最后的检查只是为了说明 @Fibonacci 在超过 10000 之后依然会继续。

这才触及到序列能做什么的皮毛,更多的信息,查看细则中的 “List infix precedence”,然后下拉到序列操作符(尽管如此,注意这还没有全部实现!它是一个极其复杂的操作符。)

我还要告诉你的是,序列操作符不局限于工作于数字,如果你显式地指定了你自己的生成器,你可以生成任何类型的序列。但是我喜欢将这保留到未来的圣诞节礼物。

第五天 – 为什么 Raku 语法是你想要的 #

圣临月第五天,您或许有些失望没能看到 Raku 酷呆了的新玩法。这次会是直观的解释一些编程语言的运行原理。 作为样例,先说下面这两行吧:

say 6 / 3;
say 'Price: 15 Euro' ~~ /\d+/;

嗯,两行代码的运行结果分别是 2 和 15。相信这对 Raku 程序员来说没什么可奇怪的。但你再细看看,两行都有斜杠 /,却为了完全不一样的目的,第一个是数值运算,第二 个是正则匹配。

Raku 怎么知道一个 / 号意味着什么?这当然不是简单的看 / 号后面的文本来决定,因为正则表达式可以看起来跟普通代码一样。 答案是:Raku 会持续跟踪他的预期。Raku 预期中最重要的两样东西就是:词和操作符。

一个词可以是像 23 或者 str 这样的文字。当解释器发现这样一个文字,然后后面就会是一个语句的结束(即分号;),或者一个操作符像 +/* 等等。过了这个操作符,解 释器又开始预期下一个词。

其实这就是问题的答案了:当解释器预期为词的时候,斜线 / 就代表正则表达式的开始;当预期为操作符的时候,斜线 / 就代表数字的除法运算。

这种做法造成了深远的后果。子函数运行可以不加括号,而在函数名后面,perl 预期一个词开端的参数列表。另一方面,类型名必须跟在操作符后面,所以,所有的类型名必 须在解析时就是已知的。

这样,很多字符都可以重复使用在不同的语法环境下了。

第六天 – X 和 Z 元操作符 #

Raku 中一个新的创意就是元操作符,这种元操作符和普通的操作符结合改变了普通操作符的行为。这种元操作符有很多,但这里我们只关注它们中的两个: XZ

X 操作符你可能已经见过它作为中缀交叉操作符的普通角色。它将列表联合在一起,每个列表中各取一个元素:

say ((1, 2) X ('a', 'b')).raku;
#  ((1, "a"), (1, "b"), (2, "a"), (2, "b"))

然而, 这个中缀操作符 infix:<X> 其实是将 X 操作符应用到列表连接操作符 infix:<,> 上的简便形式。事实上,你可以这样写:

say ((1, 2) X, (10, 11)).raku;
# ((1, 10), (1, 11), (2, 10), (2, 11))

如果你将 X 应用到不同的中缀操作符上面会发生什么?应用到 infix:<+> 上呢?

say ((1, 2) X+ (10, 11)).raku;
# (11, 12, 12, 13).list

它做了什么?它不是从每个组合中挑出所有的元素列表,这个操作符将中缀操作符 + 应用到元素间,并且结果不是一个列表,而是一个数字,是那个组合中所有元素的和。

这对任何中缀操作符都有效。看看字符串连接 infix:< ~ >

say ((1, 2) X~ (10, 11)).raku;
# ("110", "111", "210", "211")

或者也许数值相等操作符 infix:<==>

say ((1, 2) X== (1, 1)).raku
# (Bool::True, Bool::True, Bool::False, Bool::False)

但是这篇文章也是关于 Z 元操作符的。我们期望你已经知道他是什么了。如果你遇见过中缀操作符 Z,它当然是 Z, 的便捷形式。

say ((1, 2) Z, (3, 4)).raku;
# ((1, 3), (2, 4))
say ((1, 2) Z+ (3, 4)).raku;
# (4, 6).list
say ((1, 2) Z== (1, 1)).raku;
# (Bool::True, Bool::False)

Z, 然后, 依次操作每个列表的每个元素,同时操作每个列表中的第一个元素,然后同时操作第二对儿,然后第三对儿,不管有多少。当到达列表的结尾时停止。

Z 也是惰性的,所以你可以将它用在两个无限列表上,它会尽可能多地生成你需要的结果。X 只能处理左边是无限列表的列表,否则它不会设法得到任何东西。

有一个键和值得列表,你想得到一个散列? 容易!

my %hash = @keys Z=> @values;

或者,也许你想并行遍历两个列表?

for @a Z @b -> $a, $b { ... }

或者三个?

for @a Z @b Z @c -> $a, $b, $c { ... }

或者你能从扔3次有10个面的骰子的所有数字组合中,得到所有可能的总数:

my @d10 = 1 ... 10;
my @scores = (@d10 X+ @d10) X+ @d10;

如果你想看到一些在真实世界这些原操作符的用途,看看 Moritz Lenz’s 写的 Sudoku.pm 数独解算器。

第七天 词法变量 #

编程总是件很难持续做下去的事情。串几行代码很容易,根据想法做一个原型也是轻松愉快的。但随着程序慢慢变大,维护时间慢慢变长,事情慢慢就棘手起来了……最后,如果不幸的话,我们就得被迫重构——不是因为早先的问题复杂,而是因为程序本身复杂了……在不断的调试中急白了头的程序员们,早就不记得到底要怎么扩展程序以完成目的了……

所以我们回溯一下编程史,找找对着复杂性的办法。而答案就在那里,不来不去——限制长度。当你架构一个成百上千模块组成的大型程序的时候,你必须能够让这些组件通过表面上很小的设置进行交互——否则你就等着被乱七八糟的组合干死吧。

在各层次的编程上,我们都可以看到这么一个原则。因为他就只关心这一件事情:分散注意,专一的做一件事情!BCNF 范式、monads (译者注:不知道这东东咋翻译)、单子,例程,类,角色,模块,包等。这些都是在督促和指导我们限制编程的长度。这样我们才不会输在组 合学上。而这方面最简单的例子,就是词法变量。

{
    my $var;
    # $var可见
}
# $var不可见

哈哈,这就是今天要介绍的一个非常酷的功能了!非常有趣的说。

Perl 从第一版开始,在这方面一直不太对。比如 Perl5 的默认变量作用域是包。而这就是全局变量的一种。我在某个代码块里定义了一个变量,其他地方居然也能看到……

$ perl -v
This is perl 5, version 12, subversion 1 (v5.12.1)
$ perl -E '{ $var = 42 }; say $var'
42
$ perl -wE '{ my $var= 42 }; say $var'
Name "main::var" used only once: possible typo at -e line 1.
Use of uninitialized value $var in say at -e line 1.

在 Raku 里,词法变量变成了默认设置。在 Rakudo 上运行上面的代码,根本无法通过编译:

$ raku -e '{ $var = 42 }; say $var'
===SORRY!===
Symbol '$var' not predeclared in <anonymous>
$ raku -e '{ my $var = 42 }; say $var'
===SORRY!===
Symbol '$var' not predeclared in <anonymous>

好了,你可能说:“嗯,可以减少点打错字的可能了”。这当然没错,但是更重要的是:这让你认真坦诚的对待变量作用域。这对你控制代码复杂性很有利!

我们可以说出很多很多解释来说明为啥 Perl5 这么做。比如 Perl5 已经建议大家 use warnings; use strict; ,比如 Perl5 承诺的向后兼容,嗯,很伟大的做法,而 Perl1 压根没打算用来写大型程序和管理带来的复杂性;比如全局变量在单行模式下的各种方便……

Raku 内置的强制你从小处着手,帮你在系统扩容的时候,更苛责的关注架构基础。在变量方面,也就是在脚本和模块中,将词法变量作为默认设置。不过在 perl -e 执行的单行命令中,默认依然是全局变量。( Rakudo 还没有实现这个,目前单行依然是词法变量,期待实现的那天~)

继续。好像到这里你感觉词法变量的价值已经说完了?没有!正确设计的结果可是令人惊讶和奖金源源不断啊~考虑一下这个子程序:

sub counter($start_value) {
    my $count = $start_value;
    return { $count++ };
}

这里返回的是一个代码块。所以每次我们调用 counter() 的时候,得到的都是一小片断开的代码。然后看看当我创建两片这样的代码后的结果:

my $c1 = counter(5);
say $c1();           # 5
say $c1();           # 6
&nbsp_place_holder;
my $c2 = counter(42);
say $c2();           # 42
say $c1();           # 7
say $c2();           # 43

看到了吧, $c1$c2 是完全分开的,他们相互独立互不影响。尽管他们都写成 $count 的样子,看起来真是差不多,但他们都有自己独立的存储单元——因为每次我们运行进入那个代码块的时候,就是一次重新开始。这个小代码块从运行中的计数器里返回,这些计数器里保留了存储单元的对应关系。(他“关闭”这个存储单元,保护它不被 GC 回收掉。这类代码块叫闭包)

这个闭包看起来像是个轻量级的对象?gxgx,他们确实就是。闭包背后的原则,即规范对闭包值的访问方式,与面向对象背后的封装和信息的原则是一样的。他们都是尽力限制事情的程度,在事情变得糟糕的时候,帮忙减少其影响和损失。

你可以用词法变量做些很有趣的事情,比如闭包;而包变量不行。词法变量最酷啦!吼吼~~

第八天 - 不同东西用不同名字 #

Perl5 的新手们,总会很奇怪的说为啥自己没法倒装呢?Perl 里有内置的 reverse 命令,但好像压根不起作用啊:

$ perl -E "say reverse 'hello'"
hello

当他们去问一些有经验的 perler 的时候,解决办法很快就有了。因为 reverse 有两种操作模式,分别工作在标量和列表环境下,用来倒装字符串和列表元素:

$ perl -E "say scalar reverse 'hello'"
olleh

比较悲剧的是这个情况和大多数的 perl 语境是不一致的。比方说,绝大多数的操作符和函数由自己决定语境,并在这个语境下分析数据。好比 + 和 * 作用于数字,. 作用于字符串。所以说他们代表一个操作并且提供语境,而 reverse 却不是。

在 Raku 里,我们从过去的错误里吸取教训以摆脱历史的窘境。所以我们把列表倒叙,字符串翻转,哈希反演分开成了三个操作:

# 字符串翻转,改名叫flip
$ raku -e 'say flip "hello"'
olleh
# 列表倒叙
$ raku -e 'say join ", ", reverse <ab cd ef>'
ef, cd, ab
# 哈希反转,叫 invert

my %capitals = France => "Paris", UK => "London";
say %capitals.invert.raku;
("Paris" => "France", "London" => "UK")

注意哈希的反演和其他两个不同。因为哈希的值不要求是唯一的,所以反演后,哈希结构可能会被改变,或者某些值被覆盖…… 如果必要的话,使用者可以自己决定返回哈希结构时的操作方式。比如下面就是一种无损的方式:

my %inverse;
%inverse.push( %original.invert );

这个方法会在键值对存在的情况下,把新值 push 在原有值的队尾变成一个数组:

my %h;
%h.push('foo' => 1);    # foo => 1
%h.push('foo' => 2);    # foo => [1, 2]
%h.push('foo' => 3);    # foo => [1, 2, 3]

这三个函数,都会强制转换他们的参数。也就是说,如果你传递一个列表给 flip,这个列表会被强制成字符串后再翻转返回。

第十天 – Feed operators #

使用Perl 5 编程一段时间的人可能遇到或写过下面这样相似的代码:

my @new = sort { ... } map { ... } grep { ... } @original;

在这个构造中,数据从 @original 数组流进 grep,然后按顺序,流进 map ,然后流进 sort,最后将结果赋值给 @new 数组。因为它们每个都将列表作为它们最终的参数,仅仅通过位置,数据从一个操作符向左流进下一个操作符。

Raku, 从另一方面,通过引入流向操作符让数据从一个操作符流进另一个操作符,让这种思想更明确。上面的 Perl 5 代码能使用 Raku 重写:

my @new <== sort { ... } <== map { ... } <== grep { ... } <== @original;

注意条条大路通罗马,这个在 Raku 中更能体现。你也可以跟 Perl 5 的写法相同:

my @new = sort { ... }, map { ... }, grep { ... }, @original;

唯一不同的是额外的逗号。

所以,我们从这些流向操作符得到了什么?通常,当我们阅读代码的时候,你是从左向右读的,在原来的 Perl 5 代码中,你可能从左到右阅读你的代码直到你发现正在处理的结构,其流向是从右向左的,然后你跳到末尾,按照从右往左的方式再读一遍。

在 Raku 中,现在有一个突出的句法标记,告诉你数据向左流动的性质。

这样写也可以:

@original ==> grep { ... } ==> map { ... } ==> sort { ... }  ==> my @new;

下面是一些使用流向操作符的例子:

my @random-nums = (1..100).pick(*);  # 100个随机数
my @odds-squared <== sort <== map { $_ ** 2 } <== grep { $_ % 2 } <== @random-nums;
say ~@odds-squared;

my @a= (1..100).pick(*);

@a ==> grep {$_ % 2} ==> map { $_ ** 2} ==> sort {$^a <=> $^b} ==> my @c;
# 1 9 25 49 81 121 169 225 289 361 441 529 625 729 841 961 1089 1225 1369 1521 1681 1849 2025 2209 2401 2601 2809 3025 3249 3481 3721 3969 4225 4489 4761 5041 5329 5625 5929 6241 6561 6889 7225 7569 7921 8281 8649 9025 9409 9801

my @odds-squared <== sort {$^b <=> $^a} <== map { $_ ** 2 } <== grep { $_ % 2 } <== @random-nums   # 降序排列
# 9801 9409 9025 8649 8281 7921 7569 7225 6889 6561 6241 5929 5625 5329 5041 4761 4489 4225 3969 3721 3481 3249 3025 2809 2601 2401 2209 2025 1849 1681
1521 1369 1225 1089 961 841 729 625 529 441 361 289 225 169 121 81 49 25 9 1

my @rakudo-people = <scott patrick carl moritz jonathan jerry stephen>;
@rakudo-people ==> grep { /at/ } ==> map { .ucfirst } ==> my @who-it's-at;
say ~@who-it's-at;    # Patrick Jonathan

[+](my @a) <== map {$_ **2} <==  1..10   # 385, 1 到 10 的平方和
[+]() <== map {$_ **2} <==  1..10        # 385

第十二天 – 智能匹配 #

还记得Raku Advent 序列操作符吗?因为最后一个参数它接受的是一个上限,这让序列的生成停止了,例如:

1, 2, 4 ... 32;         # 1 2 4 8 16 32
1, 2, 4 ... * > 10;     # 1 2 4 8 16
1,2,4 ... * > 100;      # 1 2 4 8 16 32 64 128
1,2,4 ...^ * > 100;     # 1 2 4 8 16 32 64

你能看到,在第一种情况下,使用了数值相等。第二个更有意思:*>10 在内部被重写为一个闭包,像这样 -> $x { $x > 10 } (through currying).

序列操作符做了一些不可思议的比较,根据匹配者的类型。这种比较就叫做智能匹配,并且是在 Raku 中重复出现的一个概念,例如:

# after the 'when' keyword:
given $age {
    when 100    { say "congratulations!"      }
    when * < 18 { say "You're not of age yet" }
}

# after 'where':
subset Even of Int where * %% 2;

# 显式地使用智能匹配操作符:
if $input ~~ m/^\d+$/ {
    say "$input is an integer";
}

# arguments to grep(), first() etc.:
my @even = @numbers.grep: Even;

在智能操作符 ~~ 的右侧,并且在 whenwhere 的后面,要匹配的值被设置为 主题变量 $_

下面是一些智能操作符的用法:

$foo ~~ Str             #它的类型是 Str吗?
$foo ~~ 6               #它等于 6 吗?
$foo ~~ "bar"           #或者它是 "bar" 吗?
$foo ~~ / \w+ '-' \d+ / # 它匹配某个模式吗?
$foo ~~ (15..25)        # 它的值在 15 和 25 之间吗?
$foo ~~ -> $x { say 'ok' if 5 < $x < 25 } # 调用闭包
$foo ~~ [1, *, 1, *, 1, *] # 含有6个元素的数组,是否其所有的奇数元素的值都为 1?

智能匹配的全部表现可以在这找到:.

智能匹配没有特殊的操作符,而大部分智能匹配的情况会返回 Bool 值,对正则进行匹配会返回一个 Match 对象

你可能开始怀疑:一个正确的,内置的类型,我怎么将它用在我自己的类中?你需要为它写一个特别的 ACCEPTS 方法。假如我们有一个叫Point 的类:

class Point {
    has $.x;
    has $.y;

    method ACCEPTS(Positional $p2) {
        return $.x == $p2[0] and $.y == $p2[1]
    }
}

一切都清楚了吗?让我们看看它是如何工作的:

my $a = Point.new(x => 7, y => 9);
say [3, 5] ~~ $a; # Bool::False
say (7, 9) ~~ $a; # Bool::True

现在能恰当地做到你想要的,甚至使用你自己的类。

第 16 天 - Raku 里的时间 #

今天是圣诞月历的第 0x10 天,是时候学习一下 raku 里的时间了。S32::Temporal 简介在过去一年中有了大量的修改,今天我们就来介绍一下在 raku 实现中关于时间的一些基础知识。

timenow 是两个可以返回当前时间(至少是你的系统认为的当前时间)的词。简单的展示一下:

say time;
say now;
1292460064
Instant:2010-12-16T00:41:4.873248Z

第一个明显的区别,前者返回的是 POSIX 格式的数值型的结果;而后者返回的是一个瞬间的对象。如果你想获取秒级以下小数点位或者说闰秒,请用 now ;如果不用,那用 time 获取 POSIX 格式就够了。随你的便。

  • DateTime 和他的伙伴

大多数时候,你要的不是当前时间。这种时候,你需要的是 DateTime 。比如还是获取当前时间:

my $moment = DateTime.new(now); # 或者 DateTime.new(time)

你有两种方式来创建 DateTime 对象:

my $dw = DateTime.new(:year(1963), :month(11), :day(23), :hour(17), :minute(15));

这是 UTC 时区,如果你要更改时区的话,再加上 :timezone 就好了。这个格式里,只有 :year 是必须的,其他的默认就是1月1号半夜0点0分。

上面这种写法确实乏味,你可以采用 ISO8601 格式的输入,来创建一个 DateTime 对象:

my $dw = DateTime.new("1963-11-23T17:15:00Z");

其中 Z 表示 UTC ,想改变的话,把 Z 替换成 +hhmm 或者 -hhmm 就好了。hh 表示小时,mm 表示分钟。 此外,还有一个更简略的 Date 对象。只包括年月日的:

my $jfk = Date.new("1963-11-22"); # 你也可以用:year 等的写法

引入 Date 对象,是吸取了 CPAN 上 DateTime 模块的教训:有时候你压根不关心什么时区啊闰秒啊的。Date 对象非常容易处理,比如它有内置的 .succ.pred 方法,用来简单的递增和递减。

$jfk++; # 肯尼迪遇刺后的第二天

最后…

以上就是关于 Raku 里的时间的内容了,想了解更多细节,去看看规范吧;或者去社区里提问

第十九天 - 假作真时真亦假 #

今天的圣临礼物是教大家怎么用混淆完成一个小邪恶滴目的,吼吼~看起来这个功能挺疯狂的,其实有时候蛮有用的。先看下面这个用 but 的例子:

my $value = 42 but role { method Bool  { False } };
say $value;    # 42
say ?$value;   # False

你看,我们改变了 $value.Bool 方法。他不影响程序里其他所有的整数,哪怕别的变量也是 42。一般情况下,对于 Int 型,.Bool 方法(通过?操作符)返回值依据是是否等于 0。但这次它永远都返回 false 了。 事实上,我们还可以写的更简单,因为 False 是一个枚举值:

my $value = 42 but False;

因为 False 是 Bool 值,所有它会自动重载 .Bool 方法。这是 Raku 的一种转换方法。其他的值,也会对应的重载。

这样在有的时候,这个东西就比较有用了:在 Perl5 里,你用 system 调用 shell 的时候,得牢牢记住在 shell 里,返回 0 才是正常的:

if ( system($cmd) == 0 ) {  # 或者!system($cmd)
    # ...
}

而在 Raku 中,对应的 run 命令返回的是上面说的这种重载过的 Int,当且仅当返回值是 0 的时候,它的 bool 变成了 True,这正是我们想要的额!

if run($cmd) {  #不需要否定了
    # ...
}

好了,现在进入最疯狂的部分 —— 我们可以重载布尔值的布尔方法:

my $value = True but False;
say $value;    # True
say ?$value;   # False

没错,Raku 允许你这样自己踢自己屁股~~虽然我也不知道除了捣乱外怎么会有人愿意这么做,但是我还是很高兴看到 Raku 保持这种微妙的跟踪和重载类型的心态。我可没有……

Day 21 – transliteration and beyond #

转换听起来像拉丁词根,意味着字母的变化。这就是 Str.trans 方法所做的。

say "GATTACA".trans( "TCAG" => "0123" );  # prints "3200212\n"

使用过Perl5 的人马上意识到这就是 tr/tcag/0123/ .

下面是一个例子,使用 ROT-13算法加密文本:

sub rot13($text) { $text.trans( "A..Za..z" => "N..ZA..Mn..za..m" ) }

.trans 方法看到那些 .. 区间时,它会在内部将那些字母展开 (所以 “n..z” 意思是 “nopqrstuvwxyz”). 因此,rot13子例程的最终效果是将ASCII字母表的特定部分映射到其他部分。

在 Perl5 中,两个点跟一个破折号相同,但是在Raku 中我们让那两个点 .. 代表 范围的概念,在主程序中,在正则中,还有在这里,转换。

要注意的是,.trans 方法是不会改变原来的字符串; 它不会噶边 $text, 而是返回一个新的值。这在 Raku 中也是一个通用的旋律。要改变原值,请使用 .=trans

$kabbala.=trans("A..Ia..i" => "1..91..9");

(并且,它不仅仅适用于 .trans 方法,它对所有方法都适用。)

.trans 方法包含了一个秘密武器:假如我们想转义一些HTML,即,根据下面这个表来替换东西:

& => &amp;
< => &lt;
> => &gt;

但是我们不想关心替换还要按顺序进行:

foo         => bar
foolishness => folly

在上面的例子中,如果前面的替换先发生,就不回有后面的替换出现了 - 这可能不是你想要的。通常,我们想在短的子串之前,尝试并匹配最长的子串。

所以,这看起来我们需要一个最长记号的替换匹配,以避免因为偶然的重复替换而产生的无限循环。 那就是 Raku 的 .trans 方法所提供的。这就是它的秘密武器:嵌入两个数组而非字符串。对于 HTML 转义,我们所需要的就是:

my $escaped = $html.trans(
    [ '&',     '<',    '>'    ] =>
    [ '&amp;', '&lt;', '&gt;' ]
);

替换的顺序问题和避免循环就不用我们关心了。

第二十二天 - Meta-Object Protocol #

你有没有想过用你最爱的编程语言写一个类——但是不是按部就班的写类定义,而是通过几行代码?有些语言提供了 API 来完成这个功能。这些 API 的后面,就是元对象协议( Meta-Object Protocol ),简称 MOP。

Raku 就有 MOP,你可以自己创建类、角色、语法,添加方法和属性,并且内省类。比如我们可以调用 MOP 查看 Rakudo 是如何实现 Rat 类型(有理数)的。调用 MOP ,只要把一般的 . 换成 .^ 就可以了。

$ raku
> say join ', ', Rat.^attributes
$!numerator, $!denominator
> # 列出全部方法比较多,所以随机选几个
> say join ', ', Rat.^methods(:local).pick(5)
unpolar, ceiling, reals, Str, round
> say Rat.^methods(:local).grep('log').[0].signature.raku
:(Numeric $x: Numeric $base = { ... };; *%_)

显示出来的这几行信息相信都是不言自明了。Rat 有两个属性,$!numerator$!denominator;有很多方法,其中 log 方法可接受的第一个变量是数值型 invocant(译者注:不知道怎么翻译,反正就是对象本身的引用 $_[0] ),用冒号标记过;第二个变量参数是可选的,名字是 $base,它设有一个默认值,不过 Rakudo 不打算告诉你……

Raku 的数据库接口代码里有一个很不错的使用实例。它有一个选项用来记录对象的调用,但是只是记录一部分特定角色(比如和连接管理或者数据检索有关的)。下面是 dbi 里的代码:

sub log-calls($obj, Role $r) {
     my $wrapper = RoleHOW.new;

     for $r.^methods -> $m {
         $wrapper.^add_method($m.name, method (|$c) {
             # 打印日志信息,note() 函数输出到标准错误
             note ">> $m";
             nextsame;
         });
     }

     $wrapper.^compose();
     # does 操作符和 but 类似,不过只修改一个对象的拷贝
     $obj does $wrapper;
}

role Greet {
     method greet($x) {
         say "hello, $x";
     }
}

class SomeGreeter does Greet {
     method LOLGREET($x) {
         say "OH HAI "~ uc $x;
     }
}

my $o = log-calls(SomeGreeter.new, Greet);
# 记录日志啦,因为由 Greet 角色提供了
$o.greet('you');
# 没记录,因为没角色提供这个
$o.LOLGREET('u');

运行结果如下:

>> greet
hello, you
OH HAI U

所以说,有了 MOP ,除了指定的语法,你还可以像普通接口一样访问类、角色、语法和属性。这给了面向对象更大的灵活性,可以轻松的内省和修改对象了。

第23天 - 一些精彩的排序示例 #

继续我们的圣临礼物。

排序是一个非常非常常见的编程任务。Raku 加强了它的 .sort 功能来帮助大家更好的排序。 最最正常的默认写法是这样的:

my @sorted = @unsorted.sort; # 或者这样
sort @unsorted;

和 Perl 5 一样,也是可以自定义函数的:

# 数值比较
my @sorted = @unsorted.sort: { $^a <=> $^b };
# 或者用函数调用的形式
my @sorted = sort { $^a <=> $^b }, @unsorted;
# 字符串比对 ( 跟Perl5的cmp一样 )
my @sorted = @unsorted.sort: { $^a leg $^b };
# 类型依赖比对
my @sorted = @unsorted.sort: { $^a cmp $^b };

你也可以把 : 换成 () ,然后再跟上一些方法进行后续处理,比如:

my @topten = @scores.sort( { $^b <=> $^a } ).list.munch(10);

小提示: $a$b 不再像在 Perl5 中那样有特殊含义了,在 sort 代码块里用别的命名变量 $var、位置变量 $^var 或者其他任何的都跟在其他代码段里一样。

你可以直接在排序的时候直接就做好变换函数:

my @sorted = @unsorted.sort: { foo($^a) cmp foo($^b) };

不过 foo() 会在重复执行,如果列表不大也就罢了,如果比较大的话……如果 foo() 还是个计算密集型的……你懂的!

在这种情况下,Perl 5 里有个习惯就是使用施瓦茨( Schwartzian )变换。施瓦茨变换的做法就是 decorate-sort-undecorate,foo() 函数只用执行一次:

@sorted =
    map  { $_->[0] }
    sort { $a->[1] cmp $b->[1] }
    map  { [$_, foo($_)] }
    @unsorted;

Raku 里,你一样可以使用施瓦茨变换,不过 Raku 内置了一些智能方法。如果你有一个函数,它接受的参数个数是 0 或 1,Raku 会自动的替你启用施瓦茨变换。

现在让我们来看一些例子吧。

  • 不区分大小写的排序

把每个元素都改成小写,然后把数组按照小写的次序排好返回。

my @sorted = @unsorted.sort: { .lc };
  • 单词长度排序

把每个元素的单词按照从短到长排序。

my @sorted = @unsorted.sort: { .chars };

或者从长到短:

my @sorted = @unsorted.sort: { -.chars };
  • 多次排序比较

你可以在 sort 代码块里放多个比较函数,sort 会注意执行直到退出。比如在单词长度的基础上,再按照 ASCII 码的顺序排序。

.say for @a.sort: { $^a.chars, $^a } ;

不过,在 Rakudo 里好像运行有点问题……它只会比较长度不会比较数值,也就是说,10 排在 2 的前面。(没关系,TMTONTDI)

Raku 里的 sort 本身是稳定工作的,你可以重复使用。

.say for @a.sort.sort: { $^a.chars };

不过这样 sort 有两次调用,no fashion!所以你还可以这么写:

.say for @a.sort: { $^a.chars <=> $^b.chars || $^a leg $^b };

不过这下你有两个参数了,Raku 没法自动给你启动施瓦茨变换了。

又或者,你可以加上一个给自然数排序的函数:

.say for @a.sort: { $^a.chars.&naturally, $^a };

“给自然数排序?”我好像听到你们的哭声了,“哪里有?” 很高兴你们这么问,现在继续解决这个问题。

  • 自然数排序

标准的词法排序是按照 ASCII 次序的。先是自然数,然后是大写字母,最后是小写字母。所以人们在排序的时候经常得到这样的结果:

0
1
100
11
144th
2
21
210
3rd
33rd
AND
ARE
An
Bit
Can
and
by
car
d1
d10
d2

完全正确,但是没用……尤其是对非程序员来说,更郁闷了就……真正的自然排序,应该是先按数学量级排自然数,然后才是大小写字母。比如上面那个例子,应该排成这样:

0
1
2
3rd
11
21
33rd
100
144th
210
An
AND
and
ARE
Bit
by
Can
car
d1
d2
d10

所以,我们必须的在排序的时候加上一点转换了。我使用 .subst 方法,这是我们所熟悉的 s/// 操作符的面向对象形式。

.subst(/(\d+)/, -> $/ { 0 ~ $0.chars.chr ~ $0 }, :g)

第一部分,捕获一个连续的数字,然后由 -> $/ {} 构成一个尖号块,意思是:“传递匹配到 $/ 的数组到 {} 代码里”。然后代码里替换成用 0 按照数量级排序的顺序联结的字符串。这个 0 是以 ASCII 字符串出现,联结在原始字符串上的。最后 /g 表示全局替换。

如果也不区分大小写,那么:

.lc.subst(/(\d+)/, -> $/ { 0 ~ $0.chars.chr ~ $0 }, :g)

改成子例程的方式:

sub naturally ($a) {
    $a.lc.subst(/(\d+)/, -> $/ { 0 ~ $0.chars.chr ~ $0 }, :g)
}

看起来很不错了,不过还有点小问题,比如 THE 、 The 和 the 会按照他们在列表里的顺序返回,而不是我们预计的顺序。有个简单的解决办法,就是在转换过的元素的结尾,加上一个中断。所以最终结果是:

sub naturally ($a) {
    $a.lc.subst(/(\d+)/, -> $/ { 0 ~ $0.chars.chr ~ $0 }, :g) ~ "\x0" ~ $a
}

然后你看,这个子例程只有一个参数,所以我们还可以用上施瓦茨变换了:

.say for <0 1 100 11 144th 2 21 210 3rd 33rd AND ARE An Bit Can and by car d1 d10 d2>.sort: { .&naturally };

或者用来给 ip 排序:

my @ips = ((0..255).roll(4).join('.')for 0..99);
.say for @ips.sort: { .&naturally };

输出:

4.108.172.65
5.149.121.70
10.24.201.53
11.10.90.219
12.83.84.206
12.124.106.41
12.162.149.98
14.203.88.93
16.18.0.178
17.68.226.104
21.201.181.225
23.61.166.202

以及目录排序啊等等各种数字与字母的混合体。

最后,圣诞快乐,排序快乐,愿施瓦茨与你同在!