有一個很常見的計數場景。比如說計算 DNA 中各個堿基的個數:
my %counts;
%counts{$_}++ for 'AGTCAGTCAGTCTTTCCCAAAAT'.comb;
say %counts<A T G C>; # (7 7 3 6)
創建一個哈希。對于每一個你想計數的東西,
每遇到一次就在那個哈希中加 1。所以有什么問題?
Perl 6 通常有特定的更合適的類型來做這種操作; 例如,Bag 類型:
'AGTCAGTCAGTCTTTCCCAAAAT'.comb.Bag<A T G C>.say; # (7 7 3 6)
我們來說說這些類型還有那些時髦的運算符!
注意 Unicode
我將在這篇文章中使用花哨的 Unicode 版本的運算符和符號,因為它們看起來很純。 然而, 他們都有我們稱之為Texas的等同物, 你可以改用它們。
準備. Set. 走起
這些類型中最簡單的就是 Set。
它將僅保存每個項目之一, 因此如果您有多個相同的對象, 那么重復項將被丟棄:
say set 1, 2, 2, "foo", "a", "a", "a", "a", "b";
# OUTPUT: set(a, foo, b, 1, 2)
集合運算符是強制的, 因此我們不需要顯式地創建集合; 他們會為我們做:
say 'Weeee \o/' if 'Zoffix' ∈ <babydrop iBakeCake Zoffix viki>;
# OUTPUT: Weeee \o/
但在使用變形
時要注意:
say 'Weeee \o/' if 42 ∈ <1 42 72>;
# No output
say 'Weeee \o/' if 42 ∈ +?<1 42 72>; # coerce allomorphs to Numeric
# OUTPUT: Weeee \o/
尖括號為數字創建了變形,因此在上面的第一種情況下, 我們的集合包含一堆IntStr 對象,而運算符的左側有一個常規 Int,因此比較失敗。 在第二種情況下, 我們使用超運算符強制變形到它們的數字組件,并且測試成功。
雖然測試成員很令人興奮, 但是我們可以使用集合做更多的事情!
做集合的交集怎么樣?
my $admins = set <Zoffix mst [Coke] lizmat>;
my $live-in-North-America = set <Zoffix [Coke] TimToady huggable>;
my $North-American-admins = $admins ∩ $live-in-North-America;
say $North-American-admins;
# OUTPUT: set(Zoffix, [Coke])
我們用 ∩, U+2229 INTERSECTION, 交集運算符相交兩個集合,
并且接收到一個集合, 其僅包含在兩個原始集合中都存在的元素。
您還可以鏈接這些操作, 因此將在鏈中提供的所有集合中檢查成員資格:
say <Zoffix lizmat> ∩ <huggable Zoffix> ∩ <TimToady huggable Zoffix>;
# OUTPUT: set(Zoffix)
另外一個集合運算符是集合差集運算符, 它的 Unicode 外觀在我看來有點討厭: ?
。不,它不是反斜線(\), 而是一個 U+2216 SET MINUS 符(幸運的是,你可以使用更明顯的 (-)
) Texas 版本)。
差集運算符的才華彌補了它不算高的顏值:
my @spammers = <spammety@sam.com spam@in-a-can.com yum@spam.com>;
my @senders = <perl6@perl6.org spammety@sam.com good@guy.com>;
for keys @senders ? @spammers -> $non-spammer {
say "Message from non-spammer";
}
輸出.
Message from perl6@perl6.org
Message from good@guy.com
我們定義了兩個數組: 一個包含了一組垃圾郵件發送者的郵件地址,
另外一個包含了一組郵件發送者。怎么得到一組不含垃圾郵件發送者的郵件發送者呢?
那么使用 ? ((-)
也可以)運算符好了。
然后我們使用 for 循環來迭代結果, 正如你看到的結果一樣,
所有的垃圾郵件發送者都被忽略了…? 但是那里為什么使用 keys 呢?
原因是在那些鍵擁有鍵和值的場景中, Setty 和 Mixy 類型很像哈希。
集合類型總是把 True 作為值, 因為我們不需要在我們的循環中迭代
Pair 對象, 所以我們僅僅使用 keys 來獲取這個集合的鍵: 即電子郵件地址。
但是, 類哈希語義在集合上不是無用的。 例如, 我們可以取一個切片, 并使用
:k
副詞只返回集合包含的元素:
my $meows = set <
Abyssinian Aegean Manx Siamese Siberian Snowshoe
Sokoke Sphynx Suphalak Thai
>;
say $meows<Sphynx Raas Ragamuffin Thai>:k;
輸出.
(Sphynx Thai)
但是如果我們嘗試從集合中刪除一項會發生什么?
say $meows<Siamese>:delete;
輸出.
Cannot call 'DELETE-KEY' on an immutable 'Set'
in block <unit> at <unknown file> line 1
我們刪除不了! 集合類型是不可變的。然而, 就像Map類型 擁有一個可變版本的
Hash 那樣, Set類型 也擁有一個可變的版本:SetHash。我們沒有一個好用的助手子程序來創建一個SetHash, 所以我們使用構造函數代替:
my $s = SetHash.new: <a a a b c d>;
say $s;
$s<a d>:delete;
say $s;
輸出.
SetHash.new(a, c, b, d)
SetHash.new(c, b)
對頭! 我們成功地刪除了一個切片。 那么, 圣誕老人的包里還有什么好東西?
Gag ’em ‘n’ Bag ’em
跟集合相關的另一種類型是 Bag, 是的,它也是不變的, 相應的可變類型是
BagHash。我們在本文開始時已經看到, 我們可以使用 Bag 來計算東西, 就像集合那樣, Bag 也是哈希式的, 這就是為什么我們可以看到四個 DNA 氨基酸的一個切片:
'AGTCAGTCAGTCTTTCCCAAAAT'.comb.Bag<A T G C>.say; # (7 7 3 6)
雖然集合的所有鍵值設置為 True, 但是 Bag 的鍵值是整數權重。
如果你把兩件相同的東西放到 Bag 里, 那么它們只有一個鍵, 但是鍵值為2:
my $recipe = bag 'egg', 'egg', 'cup of milk', 'cup of flour';
say $recipe;
輸出.
bag(cup of flour, egg(2), cup of milk)
當然, 我們可以使用我們的靈巧的運算符來組合 bags! 這里, 我們會使用 ?,
U+228E MULTISET UNION 運算符, 它的 Texas 版本 (+)
看起來更清楚:
my $pancakes = bag 'egg', 'egg', 'cup of milk', 'cup of flour';
my $omelette = bag 'egg', 'egg', 'egg', 'cup of milk';
my $shopping-bag = $pancakes ? $omelette ? <gum chocolate>;
say $shopping-bag;
輸出.
bag(gum, cup of flour, egg(5), cup of milk(2), chocolate)
我們使用了兩個 Bags 加上一個含有倆個項的列表, 它會為我們正確地進行強轉,
所以我們不需要做任何事情。
一個更令人印象深刻的運算符是 ?, U+227C PRECEDES OR EQUAL TO, 它是 ?,
U+227D SUCCEEDS OR EQUAL TO 的鏡像, 它告訴我們該運算符窄邊上的 Baggy
是否是另一邊 Baggy 的子集; 意味著較小的 Baggy 中的所有對象都存在于較大的那個之中, 并且它們的權重最大。
這里有一個挑戰:我們有一些材料和一些我們想要構建的東西。 問題是,
我們沒有足夠的材料來構建所有的東西, 所以我們想知道的是我們可以構建的東西的組合。讓我們使用一些包!
my $materials = bag 'wood' xx 300, 'glass' xx 100, 'brick' xx 3000;
my @wanted =
bag('wood' xx 200, 'glass' xx 50, 'brick' xx 3000) but 'house',
bag('wood' xx 100, 'glass' xx 50) but 'shed',
bag('wood' xx 50) but 'dog-house';
say 'We can build...';
.put for @wanted.combinations.grep: { $materials ? [?] |$^stuff-we-want };
輸出.
We can build...
house
shed
dog-house
house shed
house dog-house
shed dog-house
$materials
是一個含有我們的材料的 Bag。我們使用 xx重復運算符 來指定每種材料的數量。然后我們有一個含有三個 Bags 的 @wanted
數組: 它是我們想要構建的東西。我們還在它們身上使用了 but 運算符為它們混合進名字以覆蓋那些 bags 會最后輸出。
現在有趣的部分!我們在我們想要的東西的列表上調用 .combinations
, 正如名字所示, 我們得到了所有可能的東西的組合。然后, 我們 .grep 結果, 尋找任何組合, 最多需要我們擁有的所有材料(這是?運算符)。在它的末尾, 我們有我們的 $material
Bag 在它較窄的一端, 我們有 ? 運算符, 把我們想要的東西的每個組合的 bags 添加到一塊, 除了我們將它作為元操作符[?], 這是就像把運算符放在 $^stuff-we-want
的每個項目之間。如果你是新手:那么 $^stuff-we-want
上的 $^twigil 在我們的 .grep
塊上創建一個隱式簽名, 我們可以給這個變量任意命名。
我們做到了!程序的輸出顯示我們可以構建任何東西的組合,
除了包含所有三個項目之外。我想我們不能擁有這一切…?
…可是等等!還有更多!
混合起來
讓我們回顧一下我們的配方代碼。 有一些東西不是很完美:
my $recipe = bag 'egg', 'egg', 'cup of milk', 'cup of flour';
say $recipe';
輸出.
bag(cup of flour, egg(2), cup of milk)
如果食譜要求半杯牛奶而不是整杯牛奶怎么辦? 如果 Bag 只能有整數權重,
那么我們如何表示四分之一茶匙的鹽?
答案是 Mix類型(具有相應的可變版本, MixHash)。 與 Bag 不同, Mix
支持所有 Real 權重, 包括負數權重。 因此, 我們的食譜最好用混合模型。
my $recipe = Mix.new-from-pairs: 'egg' => 2, 'cup of milk' => ?,
'cup of flour' => ?, 'salt' => ?;
say $recipe;
輸出.
mix(salt(0.25), cup of flour(0.75), egg(2), cup of milk(0.5))
一定要用引號引起你的鍵, 不要使用 colonpair 形式(:42a, 或 :a(42)),
因為那些被視為命名參數。 還有一個混合例程, 但它不像 bag 例程那樣具有權重和功能, 除了返回一個 Mix。 當然, 你可以在一個哈希或一組 pairs 上使用 .Mix
強轉。
除了令人驚奇的創作, 讓我們做一些混合! 假如說, 你是煉金術士。
你想制作一堆令人驚嘆的藥水, 你要知道你需要的配料總量。
然而, 你意識到一些反應所需的某些成分實際上是你正在做的其他反應的副產物。 那么, 什么是你需要的最有效的東西呢? 混合來拯救你來了!
my %potions =
immortality => (:oxium(6.98), :morphics(123.3), :unobtainium(2) ).Mix,
invisibility => (:forma(9.85), :rubidium(56.3), :unobtainium(?0.3)).Mix,
strength => (:forma(9.15), :rubidium(?30.3), :kuva(0.3) ).Mix,
speed => (:forma(1.35), :nano-spores(1.3), :kuva(1.3) ).Mix;
say [?] %potions.values;
輸出.
mix(unobtainium(1.7), nano-spores(1.3), morphics(123.3),forma(20.35), oxium(6.98), rubidium(26), kuva(1.6))
為了方便起見, 我們設置了一個 哈希, 鍵是藥劑的名稱, 值是成分的量的混合。 為了產生我們尋求的成分之一的反應, 我們使用負權重, 指定產生的量。
然后, 我們使用了之前看到的相同的?集添加運算符, 以它的元格式:[?]。
我們提供的哈希值是我們的混合, 它愉快地把我們的所有成分加起來, 我們在輸出中會看到。
看看unobtainium和rubidium:集合運算符正確地考慮了反應產生的數量,
那些成分具有負權重!
不朽的藥水成功地混合, 我們現在需要做的是弄清楚在接下來的幾千年做什么…?編寫一些 Perl 6 怎么樣