2012
一個(gè)日歷
#!/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
過(guò)去幾年,我寫(xiě)了很多這種代碼的變種:
my %words;
for slurp.comb(/\w+/).map(*.lc) -> $word {
%words{$word}++;
}
(此外: slurp.comb(/\w+/).map(*.lc) 從指定的標(biāo)準(zhǔn)輸入或命令行讀取文件,遍歷數(shù)據(jù)中的單詞蛋欣,然后小寫(xiě)化該單詞。 eg : perl6 slurp.pl score.txt)
Perl6引入了兩種新的組合類(lèi)型來(lái)實(shí)現(xiàn)這種功能。 在這種情況下,半路殺出個(gè)KeyBag 代替了 hash:
my %words := KeyBag.new;
for slurp.comb(/\w+/).map(*.lc) -> $word {
%words{$word}++;
}
這種情況下婿屹,為什么你會(huì)喜歡 KeyBag多于 散列呢,難道是前者代碼更多嗎推溃?很好昂利,如果你想要的是一個(gè)正整數(shù)值的散列的話(huà),KeyBag將更好地表達(dá)出你的意思美莫。
> %words{"the"} = "green";
未處理過(guò)的異常:不能解析數(shù)字:green
然而KeyBag有幾條錦囊妙計(jì)页眯。首先梯捕,四行代碼初始化你的 KeyBag 不是很羅嗦厢呵,但是Perl 6能讓它全部寫(xiě)在一行也不會(huì)有問(wèn)題:
my %words := KeyBag.new(slurp.comb(/\w+/).map(*.lc));
KeyBag.new 盡力把放到它里面的東西變成KeyBag的內(nèi)容。給出一個(gè)列表傀顾,列表中的每個(gè)元素都會(huì)被添加到 KeyBag 中襟铭,結(jié)果和之前的代碼塊是完全一樣的。
如果你不需要在創(chuàng)建bag后去修改它,你可以使用 Bag 來(lái)代替 KeyBag寒砖。不同之處是 Bag 是不會(huì)改變的赐劣;如果 %words 是一個(gè) Bag,則 %words{$word}++ 是非法的哩都。如果對(duì)你的程序來(lái)說(shuō)魁兼,不變沒(méi)有問(wèn)題的話(huà),那你可以讓代碼更緊湊漠嵌。
my %words := bag slurp.comb(/\w+/).map(*.lc); # 散列 %words不會(huì)再變化
bag 是一個(gè)有用的子例程咐汞,它只是對(duì)任何你給它的東西上調(diào)用 Bag.new 方法。(我不清楚為什么沒(méi)有同樣功能的 keybag 子例程)
Bag 和 KeyBag 有幾個(gè)雕蟲(chóng)小技儒鹿。它們都有它們自己的 .roll 和 .pick 方法化撕,以根據(jù)給定的值來(lái)權(quán)衡它們的結(jié)果:
> 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} }";
}
}
傳遞兩個(gè)文件名,這使得 Bag 從第一個(gè)文件中獲取單詞约炎,讓 Set 從第二個(gè)文件中獲取單詞植阴,然后使用 集合差 操作符 (-) 來(lái)計(jì)算只在第一個(gè)文件中含有的單詞,按那些單詞出現(xiàn)的頻率排序圾浅,然后打印出前10 個(gè)單詞掠手。
這是介紹 Set 的最好時(shí)機(jī)。就像你從上面猜到的一樣狸捕,Set 跟 Bag 的作用很像惨撇。不同的地方在于,它們都是散列府寒,而 Bag 是從Any到正整數(shù)的映射魁衙,Set 是從 Any 到 Bool::True的映射。集合Set 是不可改變的株搔,所以也有一個(gè) 可變的 KeySet .
在 Set 和 Bag 之間剖淀,我們有很豐富的操作符:
操作符 Unicode “Texas” 結(jié)果類(lèi)型
屬于 ∈ (elem) Bool
不屬于 ? !(elem) Bool
包含 ? (cont) Bool
不包含 ? !(cont) Bool
并集 ∪ (|) Set 或 Bag
交集 ∩ (&) Set 或 Bag
差集 (-) Set
子集 ? (<=) Bool
非子集 ? !(<=) Bool
真子集 ? (<) Bool
非真子集 ? !(<) Bool
超級(jí) ? (>=) Bool
非超級(jí) ? !(>=) Bool
真超級(jí) ? (>) Bool
非真超級(jí) ? !(>) Bool
bag multiplication ? (.) Bag
bag addition ? (+) Bag
set symmetric difference (^) Set
它們中的大多數(shù)都能不言自明。返回Set 的操作符在做運(yùn)算前會(huì)將它們的參數(shù)提升為 Set纤房。返回Bag 的操作符在做運(yùn)算前會(huì)將它們的參數(shù)提升為 Bag 纵隔。返回Set 或Bag 的操作符在做運(yùn)算前會(huì)將它們的參數(shù)提升為 Bag ,如果它們中至少有一個(gè)是 Bag 或 KeyBag炮姨,否則會(huì)轉(zhuǎn)換為 Set捌刮; 在任何一種情況下,它們都返回提升后的類(lèi)型舒岸。
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)
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
超棒的匿名函數(shù)
Perl6 對(duì)函數(shù)有很好的支持绅作。Perl6 令人驚嘆的把函數(shù)聲明包起來(lái),讓你可以用各種方法來(lái)定義一個(gè)函數(shù)又不丟失任何特性蛾派。你可以定義參數(shù)類(lèi)型俄认、可選參數(shù)个少、命名參數(shù),甚至在子句里也可以眯杏。如果我不知道更好的理由的話(huà)夜焦,我可能都在懷疑這是不是在補(bǔ)償 Perl5 里那個(gè)相當(dāng)基本的參數(shù)處理(咳咳 ,@_岂贩,你懂的)茫经。
除開(kāi)這些,Perl6 也允許你定義沒(méi)有命名的函數(shù)萎津。
sub {say "lol, I'm so anonymous!" }
這有什么用科平?你不命名它,就沒(méi)法調(diào)用它啊姜性,對(duì)不瞪慧?錯(cuò)!
你可以保存這個(gè)函數(shù)到一個(gè)變量里部念∑茫或者從另一個(gè)函數(shù)里 return 這個(gè)函數(shù)±芰叮或者傳參給下一個(gè)函數(shù)妓湘。事實(shí)上,當(dāng)你不命名你的函數(shù)的時(shí)候乌询,你隨后要運(yùn)行什么代碼就變得非常清晰了榜贴。就像一個(gè)可執(zhí)行的" todo "列表一樣。
現(xiàn)在讓我們說(shuō)說(shuō)匿名函數(shù)可以給我們做點(diǎn)什么妹田。在 Perl6 里它看起來(lái)會(huì)是什么樣子呢唬党?
嗯,就用最著名的排序來(lái)做例子吧鬼佣。你可能想象 Perl6 有一個(gè) sort_lexicographically 函數(shù)和一個(gè) sort_numberically 函數(shù)驶拱。不過(guò)其實(shí)沒(méi)有。只有一個(gè) sort 函數(shù)晶衷。當(dāng)你需要具體用某種形式的排序時(shí)蓝纲,你就可以傳遞一個(gè)匿名函數(shù)給 sort 。
my @sorted_words = @words.sort({ ~$_ });
my @sorted_numbers = @numbers.sort({ +$_ });
(從技術(shù)上來(lái)說(shuō)晌纫,這是塊税迷,不是函數(shù)。不過(guò)如果你不打算在里面使用 return 的話(huà)锹漱,差異不大箭养。)
當(dāng)然你可以做的比這兩個(gè)排序辦法多多了。你可以通過(guò)鞋子大小排序凌蔬,或者最大地面速度露懒,或者自燃可能性的降序等等闯冷。因?yàn)槟憧梢园讶魏芜壿嬜鳛橐粋€(gè)參數(shù)傳遞進(jìn)去砂心。面向?qū)ο蟮慕掏絺儗?duì)這種模式可非常自豪懈词,還專(zhuān)門(mén)命名為“依賴(lài)注入”。
想想看辩诞,map 坎弯、 grep 和 reduce 都很依賴(lài)這種函數(shù)傳遞。我們有時(shí)候把這種傳遞函數(shù)給函數(shù)的做法叫“高階編程”译暂,好像這是某些高手的特權(quán)似的抠忘。但其實(shí)這是一個(gè)非常有用而且可以普通使用的技能。
上面的示例都是在當(dāng)前執(zhí)行時(shí)就運(yùn)行函數(shù)了外永。其實(shí)這里沒(méi)什么限制崎脉。我們可以創(chuàng)建函數(shù),然后稍后再運(yùn)行:
sub make_surprise_for($name) {
return sub { say "Sur-priiise, $name!" };
}
my $reveal_surprise = make_surprise_for("Finn"); #
# 目前什么都沒(méi)發(fā)生
# 等著
# 繼續(xù)等著
# 等啊等啊等啊
$reveal_surprise(); # "Sur-priiise, Finn!"
$reveal_surpirse
里的函數(shù)記住了 $name
變量值伯顶,雖然原始函數(shù)是在很早之前傳遞進(jìn)去的參數(shù)囚灼。棒極了!這個(gè)效果就叫在 $name
變量上閉合的匿名函數(shù)祭衩。不過(guò)這里可沒(méi)什么技術(shù) -- 反正很棒就是了灶体。
事實(shí)上,如果放在其他主要存儲(chǔ)機(jī)制比如數(shù)組和散列旁邊再看匿名函數(shù)本身掐暮,這感覺(jué)是很自然的事情蝎抽。所有這些都可以存儲(chǔ)在變量里,作為參數(shù)傳遞或者從函數(shù)里返回路克。一個(gè)匿名數(shù)組允許你保存序列給以后調(diào)用樟结。一個(gè)匿名散列允許你存儲(chǔ)映射給以后調(diào)用。一個(gè)匿名函數(shù)允許你存儲(chǔ)計(jì)算或者行為給以后調(diào)用精算。
本月晚些時(shí)候狭吼,我會(huì)寫(xiě)篇介紹怎樣通過(guò) Perl6 的動(dòng)態(tài)域來(lái)創(chuàng)建漂亮的 DSL-y 接口。我們可以看到匿名函數(shù)在那里是怎么發(fā)揮作用的殖妇。
第九天:最長(zhǎng)標(biāo)示匹配
Perl6 正則表達(dá)式偏好盡可能的匹配最長(zhǎng)的選擇刁笙。
say "food and drink" ~~ / foo | food /; # food
這跟 Perl5 不一樣。Perl5 更喜歡上面例子中的第一個(gè)選擇谦趣,結(jié)果匹配的是 "foo" 疲吸。
如果你希望的話(huà),你依然可以按照優(yōu)先匹配的原則運(yùn)行前鹅,這個(gè)原則隱藏在稍長(zhǎng)選擇操作符 ||
背后:
say "food and drink" ~~ / foo || food /; # foo
...就是這樣摘悴。這就是最長(zhǎng)標(biāo)記匹配。 ? 短文完畢舰绘。
“喂蹂喻,等等葱椭!”你聽(tīng)見(jiàn)你絕望而驚訝的大叫了,滿(mǎn)足你希望讓每天的 Perl6 圣臨歷走的慢一點(diǎn)的愿望口四》踉耍“為什么說(shuō)最長(zhǎng)標(biāo)記匹配很重要?誰(shuí)會(huì)在意這個(gè)蔓彩?”
我很高興你這樣問(wèn)治笨。事實(shí)證明,最長(zhǎng)標(biāo)記匹配(簡(jiǎn)稱(chēng) LTM )在如何解析的時(shí)候和我們的直覺(jué)配合相當(dāng)默契赤嚼。如果你創(chuàng)造了一門(mén)語(yǔ)言旷赖,你希望人們可以聲明一個(gè)叫 forest_density 的變量而不用提及這個(gè)單詞和循環(huán)里用的 for 語(yǔ)法沖突,LTM 可以做到更卒。
我喜歡“奇怪的一致性”這個(gè)說(shuō)法 -- 尤其當(dāng)程序語(yǔ)言設(shè)計(jì)的共性讓大家越來(lái)越雷同的時(shí)候等孵。這里就是一種在類(lèi)和語(yǔ)法之間的一致性。 Perl6 基本上把這種一致性發(fā)揮到了極致蹂空。讓我簡(jiǎn)單的闡述下我的意思俯萌。
現(xiàn)在我們習(xí)慣于寫(xiě)一個(gè)類(lèi),總體來(lái)看腌闯,類(lèi)差不多是長(zhǎng)這個(gè)樣子的:
class {
method
method
method
}
奇怪的是绳瘟,語(yǔ)法有個(gè)非常類(lèi)似的結(jié)構(gòu):
grammar {
rule
rule
rule
}
(實(shí)際上關(guān)鍵詞有 regex,token 和 rule姿骏,不過(guò)當(dāng)我們把他當(dāng)作一個(gè)組來(lái)討論的時(shí)候糖声,我們暫時(shí)統(tǒng)一叫做 rules)
我們同樣習(xí)慣于派生子類(lèi)(class B is A),然后添加或者重寫(xiě)方法來(lái)產(chǎn)生一個(gè)新舊行為在一起的組合分瘦。Pelr6 提供了 multi methods 蘸泻,它允許你添加相同名字的新方法,而且不重寫(xiě)原有的嘲玫,它只嘗試匹配所有的到新方法而已悦施。這個(gè)調(diào)度是由一個(gè)(通常自動(dòng)生成的) proto method 處理的。它負(fù)責(zé)調(diào)度給所有合格的候選者去团。
這些是怎樣用語(yǔ)法和角色運(yùn)行起來(lái)的呢抡诞?額,首先它從原有的里面派生出新的語(yǔ)法土陪,和派生子類(lèi)一樣昼汗。(事實(shí)上,底層是 完全 相同的機(jī)制鬼雀。語(yǔ)法不過(guò)是有個(gè)不同元類(lèi)對(duì)象的類(lèi)罷了顷窒。)新的角色也會(huì)重寫(xiě)原有的角色,和你在方法上習(xí)慣的一樣源哩。
S05 有個(gè)漂亮的解析信件的示例鞋吉。然后派生出來(lái)解析正式信件的語(yǔ)法:
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, $=(.+) }
}
派生出來(lái)的 FormalLetter 重寫(xiě)了 greet 和 close鸦做,但是沒(méi)重寫(xiě) body。
但是這一切在 multi 方法下也能正常運(yùn)行嗎谓着?我們是不是可以定義一種“原型角色”來(lái)允許我們?cè)谝粋€(gè)語(yǔ)法里用同樣的名字有多種角色泼诱,內(nèi)容各不相同?比如漆魔,我們可能希望用一個(gè)角色 term 來(lái)解析語(yǔ)言坷檩,不過(guò)有很多不同的 terms:字符串却音、數(shù)字……而且數(shù)字可能是十進(jìn)制改抡、二進(jìn)制、八進(jìn)制系瓢、十六進(jìn)制等……
Perl6 語(yǔ)法可以包含一個(gè)原型角色阿纤,然后你可以定義、重定義同名角色隨便多少次夷陋。顯然讓我們回到文章最開(kāi)始的 / foo | food /欠拾。所有你起了相同名字的角色會(huì)編譯成一個(gè)大的 alternation(譯者注:輪流選擇,不確定怎么翻譯更好)骗绕。
不僅如此 -- 調(diào)用其他角色的角色藐窄,有些可能是原型角色,這些也會(huì)全部扁平化到一個(gè)大的 LTM 輪流選擇里酬土。實(shí)踐中荆忍,這意味著一個(gè) term 的所有可能會(huì)一次被全部嘗試一遍,機(jī)會(huì)平等撤缴。沒(méi)哪個(gè)會(huì)因?yàn)樽约菏窍榷x的所以勝出刹枉,只有最長(zhǎng)匹配的那個(gè)選擇才勝出。
這個(gè)奇怪的一致性說(shuō)明事實(shí)上屈呕,在調(diào)用某個(gè)方式的時(shí)候微宝,最具體的方法勝出,而且這個(gè)“最具體”必須加上引號(hào)虎眨。簽名里參數(shù)描述類(lèi)型越好蟋软,方法就越具體。
在分析某個(gè)角色的時(shí)候嗽桩,同樣是最具體的角色勝出岳守,不過(guò)這里“最具體”必須成功解析才行。角色描述下一步進(jìn)入的文本越詳細(xì)涤躲,角色就越具體棺耍。
這就是奇怪的一致性。因?yàn)楸砻嫔戏椒ê徒巧雌饋?lái)就是完全不一樣的怪獸种樱。
我們真心相信我們理解了派生語(yǔ)法的原理并且得到了一門(mén)新的語(yǔ)言蒙袍。 LTM 就是最合適的因?yàn)樗试S新舊角色通過(guò)一個(gè)公平和可預(yù)測(cè)的辦法混雜在一起俊卤。角色不是因?yàn)樗麄兌x的前后而勝出,而是因?yàn)樗茏詈玫慕馕鑫谋竞Ψ_@才是挑選精英的辦法消恍。
事實(shí)上,Perl6 編譯器自己就是這樣工作的以现。它使用 Perl6 語(yǔ)法解析你的程序狠怨,這個(gè)語(yǔ)法是可以派生的……不管你在程序里什么時(shí)候聲明了一個(gè)新操作符,都會(huì)給你派生出一個(gè)新的語(yǔ)法邑遏。新操作符的解析就作為新角色加入到新語(yǔ)法里佣赖。然后把解析剩余程序的任務(wù)交給新的語(yǔ)法。你的新操作符會(huì)勝過(guò)那寫(xiě)相同但匹配更短的记盒,不過(guò)輸給相同但匹配更長(zhǎng)的憎蛤。
開(kāi)開(kāi)心心玩Rakudo和Euler項(xiàng)目
Perl6 實(shí)現(xiàn)的領(lǐng)先者 Rakudo ,目前還不完美纪吮,說(shuō)起性能也尤其讓人尷尬俩檬。然而先行者不會(huì)問(wèn)“他快么?”碾盟,而會(huì)問(wèn)“他夠快么棚辽?”,甚至是“我怎樣能幫他變得更快呢冰肴?”屈藐。
為了說(shuō)服你Rakudo已經(jīng)能做到足夠快了。我們準(zhǔn)備嘗試做一組Euler項(xiàng)目測(cè)試嚼沿。其中很多涉及強(qiáng)行的數(shù)值計(jì)算估盘,Rakudo目前還不是很擅長(zhǎng)。不過(guò)我們可沒(méi)必要就此頓足:語(yǔ)言性能降低了骡尽,程序員就要更心靈手巧了遣妥,這正是樂(lè)趣所在啊。
所有的代碼都是在Rakudo 2012.11上測(cè)試通過(guò)的攀细。
We’ll start with something simple: 先從一些簡(jiǎn)單的例子開(kāi)始:
問(wèn)題2
想想斐波那契序列里數(shù)值不超過(guò)四百萬(wàn)的元素箫踩,計(jì)算這些值的總和。
辦法超級(jí)簡(jiǎn)單:
say [+] grep * %% 2, (1, 2, *+* ...^ * > 4_000_000);
運(yùn)行時(shí)間:0.4秒
注意怎樣使用操作符才能讓代碼即緊湊又保持可讀性(當(dāng)然這點(diǎn)大家肯定意見(jiàn)不一)谭贪。我們用了:
- 無(wú)論如何用 * 創(chuàng)建 lambda 函數(shù)
- 用序列操作符...^來(lái)建立斐波那契序列
- 用整除操作符%%來(lái)過(guò)濾元素
- 用[+]做reduce操作計(jì)算和
當(dāng)然境钟,沒(méi)人強(qiáng)制你這樣瘋狂的使用操作符 -- 香草(vanilla)命令式的代碼也沒(méi)問(wèn)題:
問(wèn)題3
600851475143的最大素因數(shù)是多少?
命令式的解決方案是這樣的:
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);
運(yùn)行時(shí)間:2.6秒
注意用的is copy
俭识,因?yàn)?Perl6 的綁定參數(shù)默認(rèn)是只讀
的慨削。還有用了整數(shù)除法div
,而沒(méi)用數(shù)值除法的/
。
到目前為止都沒(méi)有什么特別的缚态,我們繼續(xù):
問(wèn)題53
n從1到100磁椒, nCr的值,不一定要求不同玫芦,有多少大于一百萬(wàn)的浆熔?
我們將使用流入操作符==>來(lái)分解算法成計(jì)算的每一步:
[1], -> @p { [0, @p Z+ @p, 0] } ... * # 生成楊輝三角
==> (*[0..100])() # 生成0到100的n行
==> map *.list # 平鋪成一個(gè)列表
==> grep * > 1_000_000 # 過(guò)濾超過(guò)1000000的數(shù)
==> elems() # 計(jì)算個(gè)數(shù)
==> say; # 輸出結(jié)果
運(yùn)行時(shí)間:5.2s
注意使用了Z操作符和+來(lái)壓縮 0,@p 和 @p,0 的兩個(gè)列表。
這個(gè)單行生成楊輝三角的寫(xiě)法是從Rosetta代碼里偷過(guò)來(lái)的桥帆。那是另一個(gè)不錯(cuò)的項(xiàng)目医增,如果你對(duì) Perl6 的片段練習(xí)很感興趣的話(huà)。
讓我們做些更巧妙的:
問(wèn)題9
存在一個(gè)畢達(dá)哥拉斯三元數(shù)組讓 a +b + c = 1000
老虫。求a叶骨、b、c的值张遭。
暴力破解可以完成 (Polettix 的解決辦法)邓萨,但是這個(gè)辦法不夠快(在我機(jī)器上花了11秒左右)地梨。讓我們用點(diǎn)代數(shù)知識(shí)把問(wèn)題更簡(jiǎn)單的解決菊卷。
先創(chuàng)建一個(gè) (a, b, c) 組成的畢達(dá)哥拉斯三元數(shù)組:
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)
這就自動(dòng)符合了 b < c 的條件。
而 a < b 的條件則產(chǎn)生下面這個(gè)約束:
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 是否是整數(shù)
# 如果是宝剖,我們就找到了一個(gè)三元數(shù)組
if u %% v {
my \b = u div v;
my \c = N - a - b;
take $(a, b, c);
}
}
}
say [*] .list for gather triplets(1000);
運(yùn)行時(shí)間:0.5s
注意 sigilless (譯者注:實(shí)在不知道這個(gè)怎么翻譯)變量\N洁闰,\a……的聲明,$(...)是怎么用來(lái)把三元數(shù)組作為單獨(dú)元素返回的万细,用$_.list
的縮寫(xiě).list來(lái)恢復(fù)其列表性扑眉。
&triplets 子例程作為生成器,并且使用 &take 切換到結(jié)果赖钞。相應(yīng)的 &gather 用來(lái)劃定生成器的(動(dòng)態(tài))作用域腰素,而且它也可以放進(jìn) &triplets,這個(gè)可能返回一個(gè)懶惰列表雪营。
我們同樣可以使用流操作符改寫(xiě)成數(shù)據(jù)流驅(qū)動(dòng)的風(fēng)格:
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;
運(yùn)行時(shí)間:0.5s
注意我們是怎樣用解壓簽名綁定 -> [...] 來(lái)解壓傳遞過(guò)來(lái)的數(shù)組的弓千。
使用這種特殊的風(fēng)格沒(méi)有什么實(shí)質(zhì)的好處:事實(shí)上還很容易影響到性能,我們隨后會(huì)看到一個(gè)這方面的例子献起。
寫(xiě)純函數(shù)式算法是個(gè)超級(jí)好的路子洋访。不過(guò)原則上這就意味著讓那些足夠先進(jìn)的優(yōu)化器亂來(lái)(想想自動(dòng)向量化和線(xiàn)程)。不過(guò)Rakudo還沒(méi)到這個(gè)復(fù)雜地步谴餐。
但是如果我們沒(méi)有聰明到可以找到這么牛叉的解決辦法姻政,該怎么辦呢?
問(wèn)題47
求第一個(gè)連續(xù)四個(gè)整數(shù)岂嗓,他們有四個(gè)不同的素因數(shù)汁展。
除了暴力破解,我沒(méi)找到任何更好的辦法:
constant $N = 4;
my $i = 0;
for 2..* {
$i = factors($_) == $N ?? $i + 1 !! 0;
if $i == $N {
say $_ - $N + 1;
last;
}
}
這里,&fators 返回素因數(shù)的個(gè)數(shù)食绿,原始的實(shí)現(xiàn)差不多是這樣的:
sub factors($n is copy) {
my $i = 0;
for 2, 3, *+2 ...^ * > $n {
if $n %% $_ {
++$i;
repeat while $n %% $_ {
$n div= $_
}
}
}
return $i;
}
運(yùn)行時(shí)間:unknown (33s for N=3)
注意 repeat while ...{...} 的用法, 這是do {...} while(...);的新寫(xiě)法妹萨。
我們可以加上點(diǎn)緩存來(lái)加速程序:
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;
}
運(yùn)行時(shí)間:unknown (3.5s for N=3)
注意用 BEGIN 來(lái)初始化緩存,不管出現(xiàn)在源代碼里哪個(gè)位置炫欺。還有用 multi 來(lái)啟用對(duì) &factors 的多樣調(diào)度乎完。where 子句可以根據(jù)參數(shù)的值進(jìn)行動(dòng)態(tài)調(diào)度。
哪怕有緩存品洛,我們依然無(wú)法在一個(gè)合理的時(shí)間內(nèi)回答上來(lái)原來(lái)的問(wèn)題∈饕蹋現(xiàn)在我們?cè)趺崔k?只能用點(diǎn)騙子手段了Zavolaj – Rakudo版本的NativeCall – 來(lái)在C語(yǔ)言里實(shí)現(xiàn)因式分解.
事實(shí)證明這還不夠好桥状,所以我們繼續(xù)重構(gòu)剩下的代碼帽揪,添加一些原型聲明:
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;
運(yùn)行時(shí)間:1m2s (0.8s for N=3)
相比之下,完全使用C語(yǔ)言實(shí)現(xiàn)這個(gè)算法辅斟,運(yùn)行時(shí)間在0.1秒之內(nèi)转晰。所以目前Rakudo還沒(méi)法贏得任何一種速度測(cè)試。
重復(fù)一下士飒,用三種辦法做一件事:
問(wèn)題29
在 2 ≤ a ≤ 100 和 2 ≤ b ≤ 100 的情況下由ab生成的序列里有多少不一樣的元素查邢?
下面是一個(gè)很漂亮但很慢的解決辦法,可以用來(lái)驗(yàn)證其他辦法是否正確:
say +(2..100 X=> 2..100).classify({ .key ** .value });
運(yùn)行時(shí)間:11s
注意使用 X=> 來(lái)構(gòu)造笛卡爾乘積酵幕。用對(duì)構(gòu)造器 => 防止序列被壓扁而已扰藕。
因?yàn)镽akudo支持大整數(shù)語(yǔ)義,所以在計(jì)算像100100這種大數(shù)的時(shí)候沒(méi)有精密度上的損失芳撒。
不過(guò)我們并不真的在意冪的值邓深,不過(guò)用基數(shù)和指數(shù)來(lái)唯一標(biāo)示冪。我們需要注意基數(shù)可能自己本身就是前面某次的冪值:
constant A = 100;
constant B = 100;
my (%powers, %count);
# 找出那些是之前基數(shù)的冪的基數(shù)
# 分別存儲(chǔ)基數(shù)和指數(shù)
for 2..Int(sqrt A) -> \a {
next if a ~~ %powers;
%powers{a, a**2, a**3 ...^ * > A} = a X=> 1..*;
}
# 計(jì)算重復(fù)的個(gè)數(shù)
for %powers.values -> \p {
for 2..B -> \e {
# 上升到 \e 的冪
# 根據(jù)之前的基數(shù)和對(duì)應(yīng)指數(shù)分類(lèi)
++%count{p.key => p.value * e}
}
}
# 添加 +%count 作為一個(gè)需要保存的副本
say (A - 1) * (B - 1) + %count - [+] %count.values;
運(yùn)行時(shí)間:0.9s
注意用序列操作符 ...^ 推斷集合序列笔刹,只要提供至少三個(gè)元素芥备,列表賦值 %powers{...} = ... 就會(huì)無(wú)休止的進(jìn)行下去。
我們?cè)俅斡脭?shù)據(jù)驅(qū)動(dòng)的函數(shù)式的風(fēng)格重寫(xiě)一遍:
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();
運(yùn)行時(shí)間:1.5s
注意我們?cè)趺从?&tree 來(lái)防止壓扁的舌菜。我們可以像之前那樣用 X=> 替代 X 萌壳,不過(guò)這會(huì)讓通過(guò) -> \n, [\r, \e] 解構(gòu)變得很復(fù)雜。
和預(yù)想的一樣酷师,這個(gè)寫(xiě)法沒(méi)像命令式的那樣執(zhí)行出來(lái)讶凉。怎么才能正常運(yùn)行呢?這算是我留給讀者的作業(yè)吧山孔。
最后
解析 IPv4 地址
Perl6 的正則現(xiàn)在是一種子語(yǔ)言了懂讯,很多語(yǔ)法沒(méi)有變:
/\d+/
捕獲數(shù)字:
/(\d+)/
現(xiàn)在 $0
存儲(chǔ)著匹配到的數(shù)字,而不是 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.
修飾符現(xiàn)在放在前面了:
$_ = '1 23 456 78.9';
say .Str for m:g/(\d+)/; # 1 23 456 78 9
匹配所有看起來(lái)像這樣的東西很有用,以至于它有一個(gè)專(zhuān)門(mén)的 .comb
方法:
$str.comb(/\d+/);
如果你對(duì) .split
很熟悉军熏,你可以想到 .comb
就是它的表哥瘫里,它匹配 .split
丟棄的東西 实蔽。
Perl 5 中匹配 IPv4地址的正則如下:
/(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})/
這在 Perl6中是無(wú)效的。首先谨读,{} 塊在 Perl 6 的 正則中是真正的代碼塊局装;它們包含 Perl6 代碼。第二劳殖,在 Perl 6 中請(qǐng)使用 ** N..M
(或 ** N..*
) 代替 {N,M}
在 Perl 6 中匹配1到3位數(shù)字的正則如下:
/\d ** 1..3/
匹配 Ipv4地址:
/(\d**1..3) \. (\d**1..3) \. (\d**1..3) \. (\d**1..3)/
那仍有點(diǎn)笨拙铐尚。在Perl6的正則中,你可以使用重復(fù)操作符 % 哆姻,下面是重復(fù) (\d ** 1..3) 這個(gè)正則 4次宣增,并使用 . 點(diǎn)號(hào) 作為分隔符。
/ (\d ** 1..3) ** 4 % '.' /
% 操作符是一個(gè)量詞修飾符矛缨,所以它只跟在一個(gè)像 * 或 + 或 ** 的量詞后面爹脾。 上面的正則意思是 匹配 4 組數(shù)字,在每組數(shù)字間插入一個(gè)直接量 點(diǎn)號(hào) .
你也可能注意到 \.
變成了 '.'
,它們是一樣的箕昭。
$_ = "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
如果我們對(duì)單獨(dú)的數(shù)字感興趣:
$_ = "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")
引號(hào)
在很多地方灵妨,Perl6 都提供給你更合理的默認(rèn)設(shè)置以便在大多數(shù)情況下讓你的工作變得更簡(jiǎn)單有趣。引號(hào)也不例外盟广。
基礎(chǔ)
最常見(jiàn)的兩種引號(hào)就是單引號(hào)和雙引號(hào)闷串。單引號(hào)最簡(jiǎn)單:讓你引起一個(gè)字符串。唯一的“魔法”就是你可以用反斜杠轉(zhuǎn)義一個(gè)單引號(hào)筋量。而因?yàn)榉葱备艿倪@個(gè)作用,你可以用 \\
來(lái)表示反斜杠本身了碉熄。不過(guò)其實(shí)這個(gè)做法也是沒(méi)必要的桨武,反斜杠自己可以直接傳遞。下面是一組例子:
> 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
雙引號(hào)锈津,額呀酸,從字面上看就知道了,兩倍自然更強(qiáng)大了琼梆。:-) 它支持反斜杠轉(zhuǎn)義性誉,但更重要的是他支持內(nèi)插
。也就是說(shuō)變量
和閉包
可以放進(jìn)雙引號(hào)里茎杂。大大的幫你節(jié)約使用連接操作符或者字符串格式定義等等的時(shí)間错览。下面是幾個(gè)簡(jiǎn)單的例子:
> 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) 上面第二個(gè)例子展示了標(biāo)量?jī)?nèi)插,第三個(gè)則展示了閉包也可以插入雙引號(hào)字符串里煌往。閉包產(chǎn)生的值會(huì)被字符串化然后插入字符串中倾哺。那除了 $
開(kāi)頭的呢? 規(guī)則是這樣的:所有的都可以插入,但前提是它們被某些后置框綴(譯者注:postcircumfix)(也就是帶下標(biāo)或者擴(kuò)的數(shù)組或者哈希羞海,可以做引用或者方法調(diào)用)允許忌愚。事實(shí)上你也可以把他們都存進(jìn)標(biāo)量里。
> 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
這里你看到了一個(gè)數(shù)組元素的內(nèi)插却邓,一個(gè)被調(diào)用了方法的數(shù)組切片的內(nèi)插和一個(gè)函數(shù)調(diào)用的內(nèi)插硕糊。后置框綴規(guī)則意味著我們?cè)僖膊粫?huì)砸掉你口年的郵箱地址了(譯者注:郵箱地址里有@號(hào))。
> say "Please spam me at blackhole@jnthn.net"
Please spam me at blackhole@jnthn.net
選擇你自己的分隔符
單/雙引號(hào)對(duì)大多數(shù)情況下都很好用腊徙,不過(guò)如果你想在字符串里使用這些引號(hào)的時(shí)候咋辦癌幕?繼續(xù)用反斜杠不是什么好主意。其實(shí)你可以自定義其他字符做為引號(hào)字符昧穿。Perl6 替你選好了勺远。q和qq引號(hào)結(jié)構(gòu)后面緊跟的字符就會(huì)被作為分隔符。如果這個(gè)字符有相對(duì)應(yīng)的關(guān)閉符时鸵,那么就自動(dòng)查找這個(gè)(比如胶逢,如果你用了一個(gè)開(kāi)啟花括號(hào){,那么字符串就會(huì)在閉合花括號(hào)}處結(jié)束饰潜。注意你還可以使用多字符開(kāi)啟符和閉合符(不過(guò)要求是相同字符重復(fù)組成的多字符))初坠。另外,q的語(yǔ)義等同于單引號(hào)彭雾,qq的語(yǔ)義等同于雙引號(hào)碟刺。
> 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)
所有的引號(hào)結(jié)構(gòu)都允許你包含多行內(nèi)容。不過(guò)薯酝,還有更好的辦法:定界文檔半沽。還是用 q 或者 qq 開(kāi)始,然后跟上 :to 副詞來(lái)定義我們期望在文本最后某行匹配的字符吴菠。讓我們通過(guò)下面這個(gè)感人的故事看看它是怎么工作的者填。
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.
注意輸出文本并沒(méi)有像源程序那樣縮進(jìn)。定界符會(huì)自動(dòng)清楚縮進(jìn)到終端的級(jí)別做葵。如果我們用 qq 占哟,我們也可以往定界符里插入東西。注意這些都是通過(guò)字符串的 ident 方法實(shí)現(xiàn)的酿矢,但是如果你的字符串里沒(méi)有內(nèi)插榨乎,我們會(huì)在編譯期的時(shí)候調(diào)用 ident 作為一種優(yōu)化手段。
你同樣可以有多個(gè)定界符瘫筐,包括調(diào)用定界符里的數(shù)據(jù)的方法也是可以的(注意下面的程序就調(diào)用了 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";
}
這個(gè)程序輸出是:
Found beer
Didn't find masak
Found vacation
Didn't find whisky
自定義引號(hào)結(jié)構(gòu)的引號(hào)副詞
單/雙引號(hào)的語(yǔ)義,也是 q 和 qq 的語(yǔ)義严肪,已經(jīng)可以解決絕大多數(shù)情況了史煎。不過(guò)如果你有這么種情況:你要輸出內(nèi)插閉包而不是標(biāo)量怎么辦谦屑?這時(shí)候就要用上引號(hào)副詞了。它們決定你是否開(kāi)啟引號(hào)特性篇梭。下面是例子:
> say qq:!s"It costs $10 to {<eat nom>.pick} here."
It costs $10 to eat here.
這里我們使用了 qq 語(yǔ)義氢橙,但是關(guān)閉里標(biāo)量?jī)?nèi)插,這意味著我們可以放心往里寫(xiě)價(jià)錢(qián)而不用擔(dān)心他會(huì)試圖解析成上一次正則匹配的第十一個(gè)捕獲值恬偷。注意這里使用的標(biāo)準(zhǔn)的冒號(hào)對(duì)( colonpair )語(yǔ)法悍手。如果你希望從一個(gè)最基礎(chǔ)的引號(hào)結(jié)構(gòu)開(kāi)始,然后自己手動(dòng)的一個(gè)個(gè)打開(kāi)選項(xiàng)袍患,那么你應(yīng)該使用 Q 結(jié)構(gòu)坦康。
> 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
這里我們用了無(wú)特性引號(hào)結(jié)構(gòu),然后打開(kāi)附加特性诡延,地一個(gè)是標(biāo)量?jī)?nèi)插滞欠,然后是反斜杠轉(zhuǎn)義,然后函數(shù)內(nèi)插肆良。注意我們同樣可以選擇自己希望的任何分隔符筛璧。
引號(hào)結(jié)構(gòu)是一門(mén)語(yǔ)言
最后,值得一提的是:當(dāng)解析器進(jìn)入引號(hào)結(jié)構(gòu)的時(shí)候惹恃,其實(shí)他是切換成解析另外一個(gè)語(yǔ)言了夭谤。當(dāng)我們用副詞構(gòu)建引號(hào)結(jié)構(gòu)的時(shí)候,他只不過(guò)是把這些額外的角色混合進(jìn)基礎(chǔ)的引號(hào)語(yǔ)言里來(lái)開(kāi)啟額外的特性巫糙。好奇的童鞋可以看這里: Rakudo 怎么做到的朗儒。而當(dāng)我們碰到閉包或者其他內(nèi)插的時(shí)候,解析器再臨時(shí)切換回主語(yǔ)言参淹。所以你可以這樣寫(xiě):
> say "Hello, { prompt "Enter your name: " }!"
Enter your name: Jonathan
Hello, Jonathan!
解析器不會(huì)困惑于內(nèi)插的閉包里又帶有其他雙引號(hào)字符串的問(wèn)題醉锄。因?yàn)槲覀兘馕鲋髡Z(yǔ)言,然后切換到引號(hào)語(yǔ)言承二,然后返回主語(yǔ)言榆鼠,然后重新再返回引號(hào)語(yǔ)言來(lái)解析這個(gè)程序里的字符串里的閉包里的字符串。這就是 Perl6 解析器送給我們的圣誕節(jié)禮物亥鸠,俄羅斯套娃娃。