Wait the light to fall

Raku 圣诞月历 2012

焉知非鱼

Raku Calendar 2012

一个日历 #

#!/usr/bin/env raku

constant @months = <January February March April May June July August September October November December>;
constant @days = <Su Mo Tu We Th Fr Sa>;

sub center(Str $text, Int $width) {
    my $prefix = ' ' x ($width - $text.chars) div 2;
    my $suffix = ' ' x $width - $text.chars - $prefix.chars;
    return $prefix ~ $text ~ $suffix;
}

sub MAIN(:$year = Date.today.year, :$month = Date.today.month) {
    my $dt = Date.new(:year($year), :month($month), :day(1) );
    my $ss = $dt.day-of-week % 7;
    my @slots = ''.fmt("%2s") xx $ss;
    my $days-in-month = $dt.days-in-month;

    for $ss ..^ $ss + $days-in-month {
        @slots[$_] = $dt.day.fmt("%2d");
        $dt++
    }

    my $weekdays = @days.fmt("%2s").join: " ";
    say center(@months[$month-1] ~ " " ~ $year, $weekdays.chars);
    say $weekdays;

    for @slots.kv -> $k, $v {
        print "$v ";
        print "\n" if ($k+1) %% 7 or $v == $days-in-month;
    }
}

Bags and Sets #

过去几年, 我写了很多这种代码的变种:

my %words;
for slurp.comb(/\w+/).map(*.lc) -> $word {
    %words{$word}++;
}

(此外: slurp.comb(/\w+/).map(*.lc) 从指定的标准输入或命令行读取文件, 遍历数据中的单词, 然后小写化该单词。 eg: raku slurp.pl score.txt)

Raku 引入了两种新的组合类型来实现这种功能。 在这种情况下, 半路杀出个 KeyBag 代替了 hash:

my %words := KeyBag.new;
for slurp.comb(/\w+/).map(*.lc) -> $word {
    %words{$word}++;
}

这种情况下, 为什么你会喜欢 KeyBag 多于散列呢, 难道是前者代码更多吗?很好, 如果你想要的是一个正整数值的散列的话, KeyBag 将更好地表达出你的意思。

%words{"the"} = "green";

未处理过的异常:不能解析数字:green

然而 KeyBag 有几条锦囊妙计。首先, 四行代码初始化你的 KeyBag 不是很罗嗦, 但是 Raku 能让它全部写在一行也不会有问题:

my %words := KeyBag.new(slurp.comb(/\w+/).map(*.lc));

KeyBag.new 尽力把放到它里面的东西变成 KeyBag 的内容。给出一个列表, 列表中的每个元素都会被添加到 KeyBag 中, 结果和之前的代码块是完全一样的。

如果你不需要在创建bag后去修改它, 你可以使用 Bag 来代替 KeyBag。不同之处是 Bag 是不会改变的;如果 %words 是一个 Bag, 则 %words{$word}++ 是非法的。如果对你的程序来说, 不变没有问题的话, 那你可以让代码更紧凑。

my %words := bag slurp.comb(/\w+/).map(*.lc);  # 散列 %words 不会再变化

bag 是一个有用的子例程, 它只是对任何你给它的东西上调用 Bag.new 方法。(我不清楚为什么没有同样功能的 keybag 子例程)

Bag 和 KeyBag 有几个雕虫小技。它们都有它们自己的 .roll 和 .pick 方法, 以根据给定的值来权衡它们的结果:

> my $bag = bag "red" => 2, "blue" => 10;
> say $bag.roll(10);
> say $bag.pick(*).join(" ");

blue blue blue blue blue blue red blue red blue
blue red blue blue red blue blue blue blue blue blue blue
This wouldn’t be too hard to emulate using a normal Array, but this version would be:
> $bag = bag "red" => 20000000000000000001, "blue" => 100000000000000000000;
> say $bag.roll(10);
> say $bag.pick(10).join(" ");
blue blue blue blue red blue red blue blue blue
blue blue blue red blue blue blue red blue blue
sub MAIN($file1, $file2) {
    my $words1 = bag slurp($file1).comb(/\w+/).map(*.lc);
    my $words2 = set slurp($file2).comb(/\w+/).map(*.lc);
    my $unique = ($words1 (-) $words2);

    for $unique.list.sort({ -$words1{$_} })[^10] -> $word {
        say "$word: { $words1{$word} }";
    }
}

传递两个文件名, 这使得 Bag 从第一个文件中获取单词, 让 Set 从第二个文件中获取单词, 然后使用 集合差 操作符 (-) 来计算只在第一个文件中含有的单词, 按那些单词出现的频率排序, 然后打印出前10 个单词。

这是介绍 Set 的最好时机。就像你从上面猜到的一样, Set 跟 Bag 的作用很像。不同的地方在于, 它们都是散列, 而 Bag 是从Any到正整数的映射, Set 是从 Any 到 Bool::True 的映射。集合 Set 是不可改变的, 所以也有一个 可变的 KeySet。

在 Set 和 Bag 之间, 我们有很丰富的操作符:

操作符	Unicode	“Texas”	结果类型

属于	∈	(elem)	Bool
不属于	∉	!(elem)	Bool
包含	∋	(cont)	Bool
不包含	∌	!(cont)	Bool

并集	∪	(|)	Set 或 Bag
交集	∩	(&)	Set 或 Bag
差集	        (-)	Set

子集	⊆	(<=)	Bool
非子集	⊈	!(<=)	Bool
真子集	⊂	(<)	Bool
非真子集	⊄	!(<)	Bool

超级	⊇	(>=)	Bool
非超级	⊉	!(>=)	Bool
真超级	⊃	(>)	Bool
非真超级	⊅	!(>)	Bool

bag multiplication	⊍	(.)	Bag
bag addition	⊎	(+)	Bag
set symmetric difference (^)	Set

它们中的大多数都能不言自明。返回 Set 的操作符在做运算前会将它们的参数提升为 Set。返回 Bag 的操作符在做运算前会将它们的参数提升为 Bag。返回 Set 或 Bag 的操作符在做运算前会将它们的参数提升为 Bag, 如果它们中至少有一个是 Bag 或 KeyBag, 否则会转换为 Set;在任何一种情况下, 它们都返回提升后的类型。

> my $a = bag <a a a b b c>;  # bag(a(3), b(2), c)
> my $b = bag <a b b b>;      # bag(a, b(3))

> $a (|) $b;
bag("a" => 3, "b" => 3, "c" => 1)

> $a (&) $b;
bag("a" => 1, "b" => 2)

> $a (+) $b;
bag("a" => 4, "b" => 5, "c" => 1)

> $a (.) $b;
bag("a" => 3, "b" => 6)

下面是作者放在 github上的 Demo

A quick example of getting the 10 most common words in Hamlet which are not found in Much Ado About Nothing:

> raku bin/most-common-unique.pl data/Hamlet.txt data/Much_Ado_About_Nothing.txt

ham: 358 queen: 119 hamlet: 118 hor: 111 pol: 86 laer: 62 oph: 58 ros: 53 horatio: 48 clown: 47

超棒的匿名函数 #

Raku 对函数有很好的支持。Raku 令人惊叹的把函数声明包起来, 让你可以用各种方法来定义一个函数又不丢失任何特性。你可以定义参数类型、可选参数、命名参数, 甚至在子句里也可以。如果我不知道更好的理由的话, 我可能都在怀疑这是不是在补偿 Perl5 里那个相当基本的参数处理(咳咳 , @_, 你懂的)。 除开这些, Raku 也允许你定义没有命名的函数。

sub {say "lol, I'm so anonymous!" }

这有什么用?你不命名它, 就没法调用它啊, 对不?错!

你可以保存这个函数到一个变量里。或者从另一个函数里 return 这个函数。或者传参给下一个函数。事实上, 当你不命名你的函数的时候, 你随后要运行什么代码就变得非常清晰了。就像一个可执行的 “todo” 列表一样。

现在让我们说说匿名函数可以给我们做点什么。在 Raku 里它看起来会是什么样子呢?

嗯, 就用最著名的排序来做例子吧。你可能想象 Raku 有一个 sort_lexicographically 函数和一个 sort_numberically 函数。不过其实没有。只有一个 sort 函数。当你需要具体用某种形式的排序时, 你就可以传递一个匿名函数给 sort 。

my @sorted_words   = @words.sort({ ~$_ });
my @sorted_numbers = @numbers.sort({ +$_ });

(从技术上来说, 这是块, 不是函数。不过如果你不打算在里面使用 return 的话, 差异不大。) 当然你可以做的比这两个排序办法多多了。你可以通过鞋子大小排序, 或者最大地面速度, 或者自燃可能性的降序等等。因为你可以把任何逻辑作为一个参数传递进去。面向对象的教徒们对这种模式可非常自豪, 还专门命名为“依赖注入”。

想想看, map、 grep 和 reduce 都很依赖这种函数传递。我们有时候把这种传递函数给函数的做法叫“高阶编程”, 好像这是某些高手的特权似的。但其实这是一个非常有用而且可以普通使用的技能。

上面的示例都是在当前执行时就运行函数了。其实这里没什么限制。我们可以创建函数, 然后稍后再运行:

sub make_surprise_for($name) {
    return sub { say "Sur-priiise, $name!" };
}

my $reveal_surprise = make_surprise_for("Finn");    #

# 目前什么都没发生
# 等着
# 继续等着
# 等啊等啊等啊
$reveal_surprise();        # "Sur-priiise, Finn!"

$reveal_surpirse 里的函数记住了 $name 变量值, 虽然原始函数是在很早之前传递进去的参数。棒极了!这个效果就叫在 $name 变量上闭合的匿名函数。不过这里可没什么技术 - 反正很棒就是了。

事实上, 如果放在其他主要存储机制比如数组和散列旁边再看匿名函数本身, 这感觉是很自然的事情。所有这些都可以存储在变量里, 作为参数传递或者从函数里返回。一个匿名数组允许你保存序列给以后调用。一个匿名散列允许你存储映射给以后调用。一个匿名函数允许你存储计算或者行为给以后调用。

本月晚些时候, 我会写篇介绍怎样通过 Raku 的动态域来创建漂亮的 DSL-y 接口。我们可以看到匿名函数在那里是怎么发挥作用的。

第九天:最长标示匹配 #

Raku 正则表达式偏好尽可能的匹配最长的选择。

say "food and drink" ~~ / foo | food /;   # food

这跟 Perl5 不一样。Perl5 更喜欢上面例子中的第一个选择, 结果匹配的是 “foo”。 如果你希望的话, 你依然可以按照优先匹配的原则运行, 这个原则隐藏在稍长选择操作符 || 背后:

say "food and drink" ~~ / foo || food /;  # foo

…就是这样。这就是最长标记匹配。 ☺ 短文完毕。

“喂, 等等!”你听见你绝望而惊讶的大叫了, 满足你希望让每天的 Raku 圣临历走的慢一点的愿望。“为什么说最长标记匹配很重要?谁会在意这个?”

我很高兴你这样问。事实证明, 最长标记匹配(简称 LTM )在如何解析的时候和我们的直觉配合相当默契。如果你创造了一门语言, 你希望人们可以声明一个叫 forest_density 的变量而不用提及这个单词和循环里用的 for 语法冲突, LTM 可以做到。

我喜欢“奇怪的一致性”这个说法 - 尤其当程序语言设计的共性让大家越来越雷同的时候。这里就是一种在类和语法之间的一致性。 Raku 基本上把这种一致性发挥到了极致。让我简单的阐述下我的意思。 现在我们习惯于写一个类, 总体来看, 类差不多是长这个样子的:

class {
    method
    method
    method
}

奇怪的是, 语法有个非常类似的结构:

grammar {
    rule
    rule
    rule
}

(实际上关键词有 regex, token 和 rule, 不过当我们把他当作一个组来讨论的时候, 我们暂时统一叫做 rules)

我们同样习惯于派生子类(class B is A), 然后添加或者重写方法来产生一个新旧行为在一起的组合。Pelr6 提供了 multi methods , 它允许你添加相同名字的新方法, 而且不重写原有的, 它只尝试匹配所有的到新方法而已。这个调度是由一个(通常自动生成的) proto method 处理的。它负责调度给所有合格的候选者。

这些是怎样用语法和角色运行起来的呢?额, 首先它从原有的里面派生出新的语法, 和派生子类一样。(事实上, 底层是 完全 相同的机制。语法不过是有个不同元类对象的类罢了。)新的角色也会重写原有的角色, 和你在方法上习惯的一样。

S05 有个漂亮的解析信件的示例。然后派生出来解析正式信件的语法:

grammar Letter {
    rule text     {    }
    rule greet { [Hi|Hey|Yo] $=(\S+?) , $$}
    rule body     { +? }   # note: backtracks forwards via +?
    rule close { Later dude, $=(.+) }
}

grammar FormalLetter is Letter {
    rule greet { Dear $=(\S+?) , $$}
    rule close { Yours sincerely, $=(.+) }
}

派生出来的 FormalLetter 重写了 greet 和 close, 但是没重写 body。

但是这一切在 multi 方法下也能正常运行吗?我们是不是可以定义一种“原型角色”来允许我们在一个语法里用同样的名字有多种角色, 内容各不相同?比如, 我们可能希望用一个角色 term 来解析语言, 不过有很多不同的 terms:字符串、数字……而且数字可能是十进制、二进制、八进制、十六进制等……

Raku 语法可以包含一个原型角色, 然后你可以定义、重定义同名角色随便多少次。显然让我们回到文章最开始的 / foo | food /。所有你起了相同名字的角色会编译成一个大的 alternation。

不仅如此 - 调用其他角色的角色, 有些可能是原型角色, 这些也会全部扁平化到一个大的 LTM 轮流选择里。实践中, 这意味着一个 term 的所有可能会一次被全部尝试一遍, 机会平等。没哪个会因为自己是先定义的所以胜出, 只有最长匹配的那个选择才胜出。

这个奇怪的一致性说明事实上, 在调用某个方式的时候, 最具体的方法胜出, 而且这个“最具体”必须加上引号。签名里参数描述类型越好, 方法就越具体。

在分析某个角色的时候, 同样是最具体的角色胜出, 不过这里“最具体”必须成功解析才行。角色描述下一步进入的文本越详细, 角色就越具体。

这就是奇怪的一致性。因为表面上方法和角色看起来就是完全不一样的怪兽。

我们真心相信我们理解了派生语法的原理并且得到了一门新的语言。 LTM 就是最合适的因为它允许新旧角色通过一个公平和可预测的办法混杂在一起。角色不是因为他们定义的前后而胜出, 而是因为它能最好的解析文本。这才是挑选精英的办法。

事实上, Raku 编译器自己就是这样工作的。它使用 Raku 语法解析你的程序, 这个语法是可以派生的……不管你在程序里什么时候声明了一个新操作符, 都会给你派生出一个新的语法。新操作符的解析就作为新角色加入到新语法里。然后把解析剩余程序的任务交给新的语法。你的新操作符会胜过那写相同但匹配更短的, 不过输给相同但匹配更长的。

开开心心玩 Rakudo 和 Euler 项目 #

Raku 实现的领先者 Rakudo , 目前还不完美, 说起性能也尤其让人尴尬。然而先行者不会问“他快么?”, 而会问“他够快么?”, 甚至是“我怎样能帮他变得更快呢?”。

为了说服你 Rakudo 已经能做到足够快了。我们准备尝试做一组 Euler 项目测试。其中很多涉及强行的数值计算, Rakudo 目前还不是很擅长。不过我们可没必要就此顿足:语言性能降低了, 程序员就要更心灵手巧了, 这正是乐趣所在啊。

所有的代码都是在 Rakudo 2012.11 上测试通过的。

先从一些简单的例子开始。

想想斐波那契序列里数值不超过四百万的元素, 计算这些值的总和。办法超级简单:

say [+] grep * %% 2, (1, 2, *+* ...^ * > 4_000_000);

运行时间:0.4秒

注意怎样使用操作符才能让代码即紧凑又保持可读性(当然这点大家肯定意见不一)。我们用了:

  • 无论如何用 * 创建 lambda 函数
  • 用序列操作符 ...^ 来建立斐波那契序列
  • 用整除操作符 %% 来过滤元素
  • [+] 做 reduce 操作计算和

当然, 没人强制你这样疯狂的使用操作符 - 香草(vanilla)命令式的代码也没问题:

600851475143 的最大素因数是多少?

命令式的解决方案是这样的:

sub largest-prime-factor($n is copy) {
    for 2, 3, *+2 ... * {
        while $n %% $_ {
            $n div= $_;
            return $_ if $_ > $n;
        }
    }
}

say largest-prime-factor(600_851_475_143);

运行时间:2.6秒

注意用的 is copy, 因为 Raku 的绑定参数默认是只读的。还有用了整数除法 div, 而没用数值除法的 /

到目前为止都没有什么特别的, 我们继续:

n从1到100, nCr 的值, 不一定要求不同, 有多少大于一百万的?

我们将使用 feed 操作符 ==> 来分解算法成计算的每一步:

[1], -> @p { [0, @p Z+ @p, 0] } ... * # 生成杨辉三角
==> (*[0..100])()                     # 生成0到100的n行
==> map *.list                        # 平铺成一个列表
==> grep * > 1_000_000                # 过滤超过1000000的数
==> elems()                           # 计算个数
==> say;                              # 输出结果

运行时间:5.2s

注意使用了 Z 操作符和 + 来压缩 0,@p@p,0 的两个列表。

这个单行生成杨辉三角的写法是从 Rosetta 代码里偷过来的。那是另一个不错的项目, 如果你对 Raku 的片段练习很感兴趣的话。

让我们做些更巧妙的。

存在一个毕达哥拉斯三元数组让 a +b + c = 1000 。求 a、b、c 的值。

暴力破解可以完成 (Polettix 的解决办法), 但是这个办法不够快(在我机器上花了11秒左右)。让我们用点代数知识把问题更简单的解决。

先创建一个 (a, b, c) 组成的毕达哥拉斯三元数组:

a < b < c
a² + b² = c²

要求 N = a + b +c 就要符合:

b = N·(N - 2a) / 2·(N - a)
c = N·(N - 2a) / 2·(N - a) + a²/(N - a)

这就自动符合了 b < c 的条件。 而 a < b 的条件则产生下面这个约束:

a < (1 - 1/√2)·N

我们就得到以下代码了:

sub triplets(\N) {
    for 1..Int((1 - sqrt(0.5)) * N) -> \a {
        my \u = N * (N - 2 * a);
        my \v = 2 * (N - a);

        # 检查 b = u/v 是否是整数
        # 如果是, 我们就找到了一个三元数组
        if u %% v {
            my \b = u div v;
            my \c = N - a - b;
            take $(a, b, c);
        }
    }
}

say [*] .list for gather triplets(1000);

运行时间:0.5s

注意 sigilless 变量 \N, \a …… 的声明, $(...) 是怎么用来把三元数组作为单独元素返回的, 用$_.list 的缩写 .list 来恢复其列表性。

&triplets 子例程作为生成器, 并且使用 &take 切换到结果。相应的 &gather 用来划定生成器的(动态)作用域, 而且它也可以放进 &triplets, 这个可能返回一个惰性列表。

我们同样可以使用流操作符改写成数据流驱动的风格:

constant N = 1000;

1..Int((1 - sqrt(0.5)) * N)
==> map -> \a { [ a, N * (N - 2 * a), 2 * (N - a) ] }
==> grep -> [ \a, \u, \v ] { u %% v }
==> map -> [ \a, \u, \v ] {
    my \b = u div v;
    my \c = N - a - b;
    a * b * c
}
==> say;

运行时间:0.5s

注意我们是怎样用解压签名绑定 -> [...] 来解压传递过来的数组的。

使用这种特殊的风格没有什么实质的好处:事实上还很容易影响到性能, 我们随后会看到一个这方面的例子。 写纯函数式算法是个超级好的路子。不过原则上这就意味着让那些足够先进的优化器乱来(想想自动向量化和线程)。不过 Rakudo 还没到这个复杂地步。

但是如果我们没有聪明到可以找到这么牛叉的解决办法, 该怎么办呢?

求第一个连续四个整数, 他们有四个不同的素因数。

除了暴力破解, 我没找到任何更好的办法:

constant $N = 4;

my $i = 0;
for 2..* {
    $i = factors($_) == $N ?? $i + 1 !! 0;
    if $i == $N {
        say $_ - $N + 1;
        last;
    }
}

这里, &fators 返回素因数的个数, 原始的实现差不多是这样的:

sub factors($n is copy) {
    my $i = 0;
    for 2, 3, *+2 ...^ * > $n {
        if $n %% $_ {
            ++$i;
            repeat while $n %% $_ {
                $n div= $_
            }
        }
    }
    return $i;
}

运行时间:unknown (33s for N=3)

注意 repeat while ...{...} 的用法, 这是 do {...} while(...); 的新写法。 我们可以加上点缓存来加速程序:

BEGIN my %cache = 1 => 0;

multi factors($n where %cache) { %cache{$n} }
multi factors($n) {
    for 2, 3, *+2 ...^ * > sqrt($n) {
        if $n %% $_ {
            my $r = $n;
            $r div= $_ while $r %% $_;
            return %cache{$n} = 1 + factors($r);
        }
    }
    return %cache{$n} = 1;
}

运行时间:unknown (3.5s for N=3)

注意用 BEGIN 来初始化缓存, 不管出现在源代码里哪个位置。还有用 multi 来启用对 &factors 的多样调度。where 子句可以根据参数的值进行动态调度。

哪怕有缓存, 我们依然无法在一个合理的时间内回答上来原来的问题。现在我们怎么办?只能用点骗子手段了Zavolaj – Rakudo 版本的 NativeCall – 来在C语言里实现因式分解。

事实证明这还不够好, 所以我们继续重构剩下的代码, 添加一些原型声明:

use NativeCall;

sub factors(int $n) returns int is native('./prob047-gerdr') { * }

my int $N = 4;

my int $n = 2;
my int $i = 0;

while $i != $N {
    $i = factors($n) == $N ?? $i + 1 !! 0;
    $n = $n + 1;
}

say $n - $N;

运行时间:1m2s (0.8s for N=3)

相比之下, 完全使用C语言实现这个算法, 运行时间在0.1秒之内。所以目前 Rakudo 还没法赢得任何一种速度测试。

重复一下, 用三种办法做一件事:

2 ≤ a ≤ 1002 ≤ b ≤ 100 的情况下由 ab 生成的序列里有多少不一样的元素? 下面是一个很漂亮但很慢的解决办法, 可以用来验证其他办法是否正确:

say +(2..100 X=> 2..100).classify({ .key ** .value });

运行时间:11s

注意使用 X=> 来构造笛卡尔乘积。用对构造器 => 防止序列被压扁而已。

因为 Rakudo 支持大整数语义, 所以在计算像 100100 这种大数的时候没有精密度上的损失。

不过我们并不真的在意幂的值, 不过用基数和指数来唯一标示幂。我们需要注意基数可能自己本身就是前面某次的幂值:

constant A = 100;
constant B = 100;

my (%powers, %count);

# 找出那些是之前基数的幂的基数
# 分别存储基数和指数
for 2..Int(sqrt A) -> \a {
    next if a ~~ %powers;
    %powers{a, a**2, a**3 ...^ * > A} = a X=> 1..*;
}

# 计算重复的个数
for %powers.values -> \p {
    for 2..B -> \e {
        # 上升到 \e 的幂
        # 根据之前的基数和对应指数分类
        ++%count{p.key => p.value * e}
    }
}

# 添加 +%count 作为一个需要保存的副本
say (A - 1) * (B - 1) + %count - [+] %count.values;

运行时间:0.9s

注意用序列操作符 ...^ 推断集合序列, 只要提供至少三个元素, 列表赋值 %powers{...} = ... 就会无休止的进行下去。

我们再次用数据驱动的函数式的风格重写一遍:

sub cross(@a, @b) { @a X @b }
sub dups(@a) { @a - @a.uniq }

constant A = 100;
constant B = 100;

2..Int(sqrt A)
==> map -> \a { (a, a**2, a**3 ...^ * > A) Z=> (a X 1..*).tree }
==> reverse()
==> hash()
==> values()
==> cross(2..B)
==> map -> \n, [\r, \e] { (r) => e * n }
==> dups()
==> ((A - 1) * (B - 1) - *)()
==> say();

运行时间:1.5s

注意我们怎么用 &tree 来防止压扁的。我们可以像之前那样用 X=> 替代 X , 不过这会让通过 -> \n, [\r, \e] 解构变得很复杂。

和预想的一样, 这个写法没像命令式的那样执行出来。怎么才能正常运行呢?这算是我留给读者的作业吧。

解析 IPv4 地址 #

Raku 的正则现在是一种子语言了, 很多语法没有变:

/\d+/

捕获数字:

/(\d+)/

现在 $0 存储着匹配到的数字, 而不是 Perl 5 中的 $1. 所有的特殊变量 $0,$1,$2 在 Raku 里就是 $/[0], $/[1], $/[2]. 在 Perl 5 中, $0 是脚本或程序的文件名, 但是这在 Raku 中变成了 $*EXECUTABLE_NAME

如果你对获得一个正则匹配的所有捕获组感兴趣, 你可以使用 @(), 它是 @($/) 的语法糖。 $/ 变量中的对象拥有许多关于最后一次匹配的有用信息。例如, $/.from 将给你匹配的起始字符串位置。 但是 $0 将使我们在这篇文章中走得足够远。我们用它来从一个字符串中提取单个特征。

修饰符现在放在前面了:

$_ = '1 23 456 78.9';
say .Str for m:g/(\d+)/; # 1 23 456 78 9

匹配所有看起来像这样的东西很有用, 以至于它有一个专门的 .comb 方法:

$str.comb(/\d+/);

如果你对 .split 很熟悉, 你可以想到 .comb 就是它的表哥, 它匹配 .split 丢弃的东西 。 Perl 5 中匹配 IPv4 地址的正则如下:

/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/

这在 Raku 中是无效的。首先, {} 块在 Raku 的 正则中是真正的代码块;它们包含 Raku 代码。第二, 在 Raku 中请使用 ** N..M (或 ** N..*) 代替 {N,M}

在 Raku 中匹配1到3位数字的正则如下:

/\d ** 1..3/

匹配 Ipv4地址:

/(\d**1..3) \. (\d**1..3) \. (\d**1..3) \. (\d**1..3)/

那仍有点笨拙。在 Raku 的正则中, 你可以使用重复操作符 % , 下面是重复 (\d ** 1..3) 这个正则 4次, 并使用 . 点号作为分隔符。

/ (\d ** 1..3) ** 4 % '.' /

% 操作符是一个量词修饰符, 所以它只跟在一个像 *+** 的量词后面。上面的正则意思是 匹配 4 组数字, 在每组数字间插入一个直接量 点号 .

你也可能注意到 \. 变成了 '.' , 它们是一样的。

$_ = "Go 127.0.0.1, I said! He went to 173.194.32.32.";

say .Str for m:g/ (\d ** 1..3) ** 4 % '.' /;
# output: 127.0.0.1 173.194.32.32

或者我们可以使用 .comb:

$_ = "Go 127.0.0.1, I said! He went to 173.194.32.32.";
my @ip4addrs = .comb(/ (\d ** 1..3) ** 4 % '.' /);   # 127.0.0.1 173.194.32.32

如果我们对单独的数字感兴趣:

$_ = "Go 127.0.0.1, I said! He went to 173.194.32.32.";
say .list>>.Str.perl for m:g/ (\d ** 1..3) ** 4 % '.' /;
# output: ("127", "0", "0", "1") ("173", "194", "32", "32")

引号 #

在很多地方, Raku 都提供给你更合理的默认设置以便在大多数情况下让你的工作变得更简单有趣。引号也不例外。

最常见的两种引号就是单引号和双引号。单引号最简单:让你引起一个字符串。唯一的“魔法”就是你可以用反斜杠转义一个单引号。而因为反斜杠的这个作用, 你可以用 \\ 来表示反斜杠本身了。不过其实这个做法也是没必要的, 反斜杠自己可以直接传递。下面是一组例子:

> say 'Everybody loves Magical Trevor’;
Everybody loves Magical Trevor
> say 'Oh wow, it\'s backslashed!’;
Oh wow, it's backslashed!
> say 'You can include a \\ like this’;
You can include a \ like this
> say 'Nothing like \n is available’;
Nothing like \n is available
> say 'And a \ on its own is no problem’;
And a \ on its own is no problem

双引号, 额, 从字面上看就知道了, 两倍自然更强大了。:-) 它支持反斜杠转义, 但更重要的是他支持内插。也就是说变量闭包可以放进双引号里。大大的帮你节约使用连接操作符或者字符串格式定义等等的时间。

下面是几个简单的例子:

> say "Ooh look!\nLine breaks!"
Ooh look!
Line breaks!
> my $who = 'Ninochka'; say "Hello, dear $who"
Hello, dear Ninochka
> say "Hello, { prompt 'Enter your name: ' }!"
Enter your name: _Jonathan_
Hello, Jonathan!

上面第二个例子展示了标量内插, 第三个则展示了闭包也可以插入双引号字符串里。闭包产生的值会被字符串化然后插入字符串中。那除了 $ 开头的呢? 规则是这样的:所有的都可以插入, 但前提是它们被某些后环缀(译者注:postcircumfix)(也就是带下标或者扩的数组或者哈希, 可以做引用或者方法调用)允许。事实上你也可以把他们都存进标量里。

> my @beer = <Chimay Hobgoblin Yeti>;
Chimay Hobgoblin Yeti
> say "First up, a @beer[0]"
First up, a Chimay
> say "Then @beer[1,2].join(' and ')!"
Then Hobgoblin and Yeti!
> say "Tu je &prompt('Ktore pivo chces? ')"
Ktore pivo chces? _Starobrno_
Tu je Starobrno

这里你看到了一个数组元素的内插, 一个被调用了方法的数组切片的内插和一个函数调用的内插。后环缀规则意味着我们再也不会砸掉你口年的邮箱地址了(译者注:邮箱地址里有@号)。

> say "Please spam me at blackhole@jnthn.net"
Please spam me at blackhole@jnthn.net

选择你自己的分隔符 #

单/双引号对大多数情况下都很好用, 不过如果你想在字符串里使用这些引号的时候咋办?继续用反斜杠不是什么好主意。其实你可以自定义其他字符做为引号字符。Raku 替你选好了。qqq 引号结构后面紧跟的字符就会被作为分隔符。如果这个字符有相对应的关闭符, 那么就自动查找这个(比如, 如果你用了一个开启花括号{, 那么字符串就会在闭合花括号}处结束。注意你还可以使用多字符开启符和闭合符(不过要求是相同字符重复组成的多字符))。另外, q 的语义等同于单引号, qq 的语义等同于双引号。

> say q{C'est la vie}
C'est la vie
> say q{{Unmatched } and { are { OK } in { here}}
Unmatched } and { are { OK } in { here
> say qq!Lottery results: {(1..49).roll(6).sort}!
Lottery results: 12 13 26 34 36 46

定界符(Heredoc) #

所有的引号结构都允许你包含多行内容。不过, 还有更好的办法:定界文档。还是用 q 或者 qq 开始, 然后跟上 :to 副词来定义我们期望在文本最后某行匹配的字符。让我们通过下面这个感人的故事看看它是怎么工作的。

print q:to/THE END/
    Once upon a time, there was a pub. The pub had
    lots of awesome beer. One day, a Perl workshop
    was held near to the pub. The hackers drank
    the pub dry. The pub owner could finally afford
    a vacation.
    THE END

脚本的输出如下:

Once upon a time, there was a pub. The pub had
lots of awesome beer. One day, a Perl workshop
was held near to the pub. The hackers drank
the pub dry. The pub owner could finally afford
a vacation.

注意输出文本并没有像源程序那样缩进。定界符会自动清楚缩进到终端的级别。如果我们用 qq , 我们也可以往定界符里插入东西。注意这些都是通过字符串的 ident 方法实现的, 但是如果你的字符串里没有内插, 我们会在编译期的时候调用 ident 作为一种优化手段。

你同样可以有多个定界符, 包括调用定界符里的数据的方法也是可以的(注意下面的程序就调用了 lines 方法)。

my ($input, @searches) = q:to/INPUT/, q:to/SEARCHES/.lines;
    Once upon a time, there was a pub. The pub had
    lots of awesome beer. One day, a Perl workshop
    was held near to the pub. The hackers drank
    the pub dry. The pub owner could finally afford
    a vacation.
    INPUT
    beer
    masak
    vacation
    whisky
    SEARCHES

for @searches -> $s {
    say $input ~~ /$s/
        ?? "Found $s"
        !! "Didn't find $s";
}

这个程序输出是:

Found beer
Didn't find masak
Found vacation
Didn't find whisky

自定义引号结构的引号副词 #

单/双引号的语义, 也是 q 和 qq 的语义, 已经可以解决绝大多数情况了。不过如果你有这么种情况:你要输出内插闭包而不是标量怎么办?这时候就要用上引号副词了。它们决定你是否开启引号特性。下面是例子:

> say qq:!s"It costs $10 to {<eat nom>.pick} here."
It costs $10 to eat here.

这里我们使用了 qq 语义, 但是关闭里标量内插, 这意味着我们可以放心往里写价钱而不用担心他会试图解析成上一次正则匹配的第十一个捕获值。注意这里使用的标准的冒号对(colonpair)语法。如果你希望从一个最基础的引号结构开始, 然后自己手动的一个个打开选项, 那么你应该使用 Q 结构。

> say Q{$*OS\n&sin(3)}
$*OS\n&sin(3)
> say Q:s{$*OS\n&sin(3)}
MSWin32\n&sin(3)
> say Q:s:b{$*OS\n&sin(3)}
MSWin32
&sin(3)
> say Q:s:b:f{$*OS\n&sin(3)}
MSWin32
0.141120008059867

这里我们用了无特性引号结构, 然后打开附加特性, 地一个是标量内插, 然后是反斜杠转义, 然后函数内插。注意我们同样可以选择自己希望的任何分隔符。

引号结构是一门语言 #

最后, 值得一提的是:当解析器进入引号结构的时候, 其实他是切换成解析另外一个语言了。当我们用副词构建引号结构的时候, 他只不过是把这些额外的角色混合进基础的引号语言里来开启额外的特性。好奇的童鞋可以看这里: Rakudo 怎么做到的。而当我们碰到闭包或者其他内插的时候, 解析器再临时切换回主语言。所以你可以这样写:

> say "Hello, { prompt "Enter your name: " }!"
Enter your name: Jonathan
Hello, Jonathan!

解析器不会困惑于内插的闭包里又带有其他双引号字符串的问题。因为我们解析主语言, 然后切换到引号语言, 然后返回主语言, 然后重新再返回引号语言来解析这个程序里的字符串里的闭包里的字符串。这就是 Raku 解析器送给我们的圣诞节礼物, 俄罗斯套娃娃。