Perl 6圣誕月歷 (2012)

2012


一個日歷


#!/usr/bin/env perl6

    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


December 13, 2012
過去幾年,我寫了很多這種代碼的變種:

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

(此外: slurp.comb(/\w+/).map(*.lc) 從指定的標準輸入或命令行讀取文件,遍歷數據中的單詞,然后小寫化該單詞。 eg : perl6 slurp.pl score.txt)
Perl6引入了兩種新的組合類型來實現這種功能。 在這種情況下,半路殺出個KeyBag 代替了 hash:

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

這種情況下,為什么你會喜歡 KeyBag多于 散列呢,難道是前者代碼更多嗎?很好,如果你想要的是一個正整數值的散列的話,KeyBag將更好地表達出你的意思。
> %words{"the"} = "green";

未處理過的異常:不能解析數字:green
然而KeyBag有幾條錦囊妙計。首先,四行代碼初始化你的 KeyBag 不是很羅嗦,但是Perl 6能讓它全部寫在一行也不會有問題:

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; 在任何一種情況下,它們都返回提升后的類型。
eg:

> 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:

> perl6 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

超棒的匿名函數


Perl6 對函數有很好的支持。Perl6 令人驚嘆的把函數聲明包起來,讓你可以用各種方法來定義一個函數又不丟失任何特性。你可以定義參數類型、可選參數、命名參數,甚至在子句里也可以。如果我不知道更好的理由的話,我可能都在懷疑這是不是在補償 Perl5 里那個相當基本的參數處理(咳咳 ,@_,你懂的)。
除開這些,Perl6 也允許你定義沒有命名的函數。

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

這有什么用?你不命名它,就沒法調用它啊,對不?錯!
你可以保存這個函數到一個變量里。或者從另一個函數里 return 這個函數?;蛘邆鲄⒔o下一個函數。事實上,當你不命名你的函數的時候,你隨后要運行什么代碼就變得非常清晰了。就像一個可執行的" todo "列表一樣。

現在讓我們說說匿名函數可以給我們做點什么。在 Perl6 里它看起來會是什么樣子呢?
嗯,就用最著名的排序來做例子吧。你可能想象 Perl6 有一個 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 變量上閉合的匿名函數。不過這里可沒什么技術 -- 反正很棒就是了。
事實上,如果放在其他主要存儲機制比如數組和散列旁邊再看匿名函數本身,這感覺是很自然的事情。所有這些都可以存儲在變量里,作為參數傳遞或者從函數里返回。一個匿名數組允許你保存序列給以后調用。一個匿名散列允許你存儲映射給以后調用。一個匿名函數允許你存儲計算或者行為給以后調用。
本月晚些時候,我會寫篇介紹怎樣通過 Perl6 的動態域來創建漂亮的 DSL-y 接口。我們可以看到匿名函數在那里是怎么發揮作用的。

第九天:最長標示匹配


Perl6 正則表達式偏好盡可能的匹配最長的選擇。

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

這跟 Perl5 不一樣。Perl5 更喜歡上面例子中的第一個選擇,結果匹配的是 "foo" 。
如果你希望的話,你依然可以按照優先匹配的原則運行,這個原則隱藏在稍長選擇操作符 || 背后:

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

...就是這樣。這就是最長標記匹配。 ? 短文完畢。
“喂,等等!”你聽見你絕望而驚訝的大叫了,滿足你希望讓每天的 Perl6 圣臨歷走的慢一點的愿望。“為什么說最長標記匹配很重要?誰會在意這個?”
我很高興你這樣問。事實證明,最長標記匹配(簡稱 LTM )在如何解析的時候和我們的直覺配合相當默契。如果你創造了一門語言,你希望人們可以聲明一個叫 forest_density 的變量而不用提及這個單詞和循環里用的 for 語法沖突,LTM 可以做到。
我喜歡“奇怪的一致性”這個說法 -- 尤其當程序語言設計的共性讓大家越來越雷同的時候。這里就是一種在類和語法之間的一致性。 Perl6 基本上把這種一致性發揮到了極致。讓我簡單的闡述下我的意思。
現在我們習慣于寫一個類,總體來看,類差不多是長這個樣子的:

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:字符串、數字……而且數字可能是十進制、二進制、八進制、十六進制等……

Perl6 語法可以包含一個原型角色,然后你可以定義、重定義同名角色隨便多少次。顯然讓我們回到文章最開始的 / foo | food /。所有你起了相同名字的角色會編譯成一個大的 alternation(譯者注:輪流選擇,不確定怎么翻譯更好)。

不僅如此 -- 調用其他角色的角色,有些可能是原型角色,這些也會全部扁平化到一個大的 LTM 輪流選擇里。實踐中,這意味著一個 term 的所有可能會一次被全部嘗試一遍,機會平等。沒哪個會因為自己是先定義的所以勝出,只有最長匹配的那個選擇才勝出。

這個奇怪的一致性說明事實上,在調用某個方式的時候,最具體的方法勝出,而且這個“最具體”必須加上引號。簽名里參數描述類型越好,方法就越具體。
在分析某個角色的時候,同樣是最具體的角色勝出,不過這里“最具體”必須成功解析才行。角色描述下一步進入的文本越詳細,角色就越具體。
這就是奇怪的一致性。因為表面上方法和角色看起來就是完全不一樣的怪獸。
我們真心相信我們理解了派生語法的原理并且得到了一門新的語言。 LTM 就是最合適的因為它允許新舊角色通過一個公平和可預測的辦法混雜在一起。角色不是因為他們定義的前后而勝出,而是因為它能最好的解析文本。這才是挑選精英的辦法。

事實上,Perl6 編譯器自己就是這樣工作的。它使用 Perl6 語法解析你的程序,這個語法是可以派生的……不管你在程序里什么時候聲明了一個新操作符,都會給你派生出一個新的語法。新操作符的解析就作為新角色加入到新語法里。然后把解析剩余程序的任務交給新的語法。你的新操作符會勝過那寫相同但匹配更短的,不過輸給相同但匹配更長的。

開開心心玩Rakudo和Euler項目


Perl6 實現的領先者 Rakudo ,目前還不完美,說起性能也尤其讓人尷尬。然而先行者不會問“他快么?”,而會問“他夠快么?”,甚至是“我怎樣能幫他變得更快呢?”。
為了說服你Rakudo已經能做到足夠快了。我們準備嘗試做一組Euler項目測試。其中很多涉及強行的數值計算,Rakudo目前還不是很擅長。不過我們可沒必要就此頓足:語言性能降低了,程序員就要更心靈手巧了,這正是樂趣所在啊。
所有的代碼都是在Rakudo 2012.11上測試通過的。
We’ll start with something simple: 先從一些簡單的例子開始:
問題2

想想斐波那契序列里數值不超過四百萬的元素,計算這些值的總和。
辦法超級簡單:

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

運行時間:0.4秒

注意怎樣使用操作符才能讓代碼即緊湊又保持可讀性(當然這點大家肯定意見不一)。我們用了:

  • 無論如何用 * 創建 lambda 函數
  • 用序列操作符...^來建立斐波那契序列
  • 用整除操作符%%來過濾元素
  • 用[+]做reduce操作計算和

當然,沒人強制你這樣瘋狂的使用操作符 -- 香草(vanilla)命令式的代碼也沒問題:
問題3

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,因為 Perl6 的綁定參數默認是只讀的。還有用了整數除法div,而沒用數值除法的/。
到目前為止都沒有什么特別的,我們繼續:

問題53

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

我們將使用流入操作符==>來分解算法成計算的每一步:

[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代碼里偷過來的。那是另一個不錯的項目,如果你對 Perl6 的片段練習很感興趣的話。

讓我們做些更巧妙的:
問題9

存在一個畢達哥拉斯三元數組讓 a +b + c = 1000 。求a、b、c的值。

暴力破解可以完成 (Polettix 的解決辦法),但是這個辦法不夠快(在我機器上花了11秒左右)。讓我們用點代數知識把問題更簡單的解決。
先創建一個 (a, b, c) 組成的畢達哥拉斯三元數組:
a < b < c
a2 + b2 = c2
要求 N = a + b +c 就要符合:
b = N·(N - 2a) / 2·(N - a)
c = N·(N - 2a) / 2·(N - a) + a2/(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還沒到這個復雜地步。
但是如果我們沒有聰明到可以找到這么牛叉的解決辦法,該怎么辦呢?

問題47

求第一個連續四個整數,他們有四個不同的素因數。
除了暴力破解,我沒找到任何更好的辦法:

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 子句可以根據參數的值進行動態調度。
哪怕有緩存,我們依然無法在一個合理的時間內回答上來原來的問題?,F在我們怎么辦?只能用點騙子手段了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還沒法贏得任何一種速度測試。
重復一下,用三種辦法做一件事:
問題29

在 2 ≤ a ≤ 100 和 2 ≤ 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 地址


Perl6 的正則現在是一種子語言了,很多語法沒有變:
/\d+/
捕獲數字:
/(\d+)/
現在 $0 存儲著匹配到的數字,而不是 Perl 5 中的 $1. 所有的特殊變量 $0,$1,$2 在 Perl6 里就是 $/[0], $/[1], $/[2]. 在Perl 5 中,$0 是腳本或程序的文件名,但是這在 Perl6 中變成了 $*EXECUTABLE_NAME .

Should you be interested in getting all of the captured groups of a regex match, you can use @(), which is syntactic sugar for @($/).
The object in the $/ variable holds lots of useful information about the last match. For example, $/.from will give you the starting string position of the match.
But $0 will get us far enough for this post. We use it to extract individual features from a string.

修飾符現在放在前面了:

$_ = '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})/

這在 Perl6中是無效的。首先,{} 塊在 Perl 6 的 正則中是真正的代碼塊;它們包含 Perl6 代碼。第二,在 Perl 6 中請使用 ** N..M (或 ** N..*) 代替 {N,M}

在 Perl 6 中匹配1到3位數字的正則如下:

/\d ** 1..3/

匹配 Ipv4地址:

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

那仍有點笨拙。在Perl6的正則中,你可以使用重復操作符 % ,下面是重復 (\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")

引號


在很多地方,Perl6 都提供給你更合理的默認設置以便在大多數情況下讓你的工作變得更簡單有趣。引號也不例外。
基礎

最常見的兩種引號就是單引號和雙引號。單引號最簡單:讓你引起一個字符串。唯一的“魔法”就是你可以用反斜杠轉義一個單引號。而因為反斜杠的這個作用,你可以用 \\ 來表示反斜杠本身了。不過其實這個做法也是沒必要的,反斜杠自己可以直接傳遞。下面是一組例子:

> 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!

(that is, an array or hash subscript, parentheses to make an invocation, or a method call) 上面第二個例子展示了標量內插,第三個則展示了閉包也可以插入雙引號字符串里。閉包產生的值會被字符串化然后插入字符串中。那除了 $ 開頭的呢? 規則是這樣的:所有的都可以插入,但前提是它們被某些后置框綴(譯者注: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

選擇你自己的分隔符

單/雙引號對大多數情況下都很好用,不過如果你想在字符串里使用這些引號的時候咋辦?繼續用反斜杠不是什么好主意。其實你可以自定義其他字符做為引號字符。Perl6 替你選好了。q和qq引號結構后面緊跟的字符就會被作為分隔符。如果這個字符有相對應的關閉符,那么就自動查找這個(比如,如果你用了一個開啟花括號{,那么字符串就會在閉合花括號}處結束。注意你還可以使用多字符開啟符和閉合符(不過要求是相同字符重復組成的多字符))。另外,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!

解析器不會困惑于內插的閉包里又帶有其他雙引號字符串的問題。因為我們解析主語言,然后切換到引號語言,然后返回主語言,然后重新再返回引號語言來解析這個程序里的字符串里的閉包里的字符串。這就是 Perl6 解析器送給我們的圣誕節禮物,俄羅斯套娃娃。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,247評論 6 543
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,520評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,362評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,805評論 1 317
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,541評論 6 412
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,896評論 1 328
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,887評論 3 447
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,062評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,608評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,356評論 3 358
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,555評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,077評論 5 364
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,769評論 3 349
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,175評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,489評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,289評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,516評論 2 379

推薦閱讀更多精彩內容

  • 第5章 引用類型(返回首頁) 本章內容 使用對象 創建并操作數組 理解基本的JavaScript類型 使用基本類型...
    大學一百閱讀 3,264評論 0 4
  • 2010 第二天:用main函數控制命令行交互 2010 年 Perl6 圣誕月歷(二)用 main 函數控制命令...
    焉知非魚閱讀 497評論 0 0
  • 標題: Rakudo and NQP Internals子標題: The guts tormented imple...
    焉知非魚閱讀 1,423評論 1 3
  • 黛青色做底色,上有桐花刺繡,雖不是上等材料布藝,但正是這種樸素,讓人宜于歡喜接受。江北的烈日,不適合過于鮮艷的色彩...
    酒色的石頭閱讀 307評論 2 1
  • 又是一年。時間已把記憶消磨的只剩下棱角,若不是還有照片為證據,那些,或許,會當做前世了吧? 日子就在這樣忽慢忽快的...
    那年凌汛閱讀 98評論 0 1