Perl 6 中的 Setty 和 Baggy 类型

有一个很常见的计数场景。比如说计算 DNA 中各个碱基的个数:

1
2
3
my %counts;
%counts{$_}++ for 'AGTCAGTCAGTCTTTCCCAAAAT'.comb;
say %counts<A T G C>; # (7 7 3 6)

创建一个哈希。对于每一个你想计数的东西,
每遇到一次就在那个哈希中加 1。所以有什么问题?

Perl 6 通常有特定的更合适的类型来做这种操作; 例如,Bag 类型:

1
'AGTCAGTCAGTCTTTCCCAAAAT'.comb.Bag<A T G C>.say; # (7 7 3 6)

我们来说说这些类型还有那些时髦的运算符!

注意 Unicode

我将在这篇文章中使用花哨的 Unicode 版本的运算符和符号,因为它们看起来很纯。 然而, 他们都有我们称之为Texas的等同物, 你可以改用它们。

准备. Set. 走起

这些类型中最简单的就是 Set
它将仅保存每个项目之一, 因此如果您有多个相同的对象, 那么重复项将被丢弃:

1
2
say set 1, 2, 2, "foo", "a", "a", "a", "a", "b";
# OUTPUT: set(a, foo, b, 1, 2)

集合运算符是强制的, 因此我们不需要显式地创建集合; 他们会为我们做:

1
2
say 'Weeee \o/' if 'Zoffix' ∈ <babydrop iBakeCake Zoffix viki>;
# OUTPUT: Weeee \o/

但在使用变形
时要注意:

1
2
3
4
5
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,因此比较失败。 在第二种情况下, 我们使用超运算符强制变形到它们的数字组件,并且测试成功。

虽然测试成员很令人兴奋, 但是我们可以使用集合做更多的事情!
做集合的交集怎么样?

1
2
3
4
5
6
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, 交集运算符相交两个集合,
并且接收到一个集合, 其仅包含在两个原始集合中都存在的元素。
您还可以链接这些操作, 因此将在链中提供的所有集合中检查成员资格:

1
2
say <Zoffix lizmat> ∩ <huggable Zoffix> ∩ <TimToady huggable Zoffix>;
# OUTPUT: set(Zoffix)

另外一个集合运算符是集合差集运算符, 它的 Unicode 外观在我看来有点讨厌: ∖
。不,它不是反斜线(\), 而是一个 U+2216 SET MINUS 符(幸运的是,你可以使用更明显的 (-)) Texas 版本)。

差集运算符的才华弥补了它不算高的颜值:

1
2
3
4
5
6
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 副词只返回集合包含的元素:

1
2
3
4
5
my $meows = set <
Abyssinian Aegean Manx Siamese Siberian Snowshoe
Sokoke Sphynx Suphalak Thai
>;
say $meows<Sphynx Raas Ragamuffin Thai>:k;

输出.

(Sphynx Thai)

但是如果我们尝试从集合中删除一项会发生什么?

1
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, 所以我们使用构造函数代替:

1
2
3
4
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 氨基酸的一个切片:

1
'AGTCAGTCAGTCTTTCCCAAAAT'.comb.Bag<A T G C>.say; # (7 7 3 6)

虽然集合的所有键值设置为 True, 但是 Bag 的键值是整数权重。
如果你把两件相同的东西放到 Bag 里, 那么它们只有一个键, 但是键值为2:

1
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 版本 (+) 看起来更清楚:

1
2
3
4
5
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 中的所有对象都存在于较大的那个之中, 并且它们的权重最大。

这里有一个挑战:我们有一些材料和一些我们想要构建的东西。 问题是,
我们没有足够的材料来构建所有的东西, 所以我们想知道的是我们可以构建的东西的组合。让我们使用一些包!

1
2
3
4
5
6
7
8
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 块上创建一个隐式签名, 我们可以给这个变量任意命名。

我们做到了!程序的输出显示我们可以构建任何东西的组合,
除了包含所有三个项目之外。我想我们不能拥有这一切…​

…可是等等!还有更多!

混合起来

让我们回顾一下我们的配方代码。 有一些东西不是很完美:

1
2
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 权重, 包括负数权重。 因此, 我们的食谱最好用混合模型。

1
2
3
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 强转。

除了令人惊奇的创作, 让我们做一些混合! 假如说, 你是炼金术士。
你想制作一堆令人惊叹的药水, 你要知道你需要的配料总量。

然而, 你意识到一些反应所需的某些成分实际上是你正在做的其他反应的副产物。 那么, 什么是你需要的最有效的东西呢? 混合来拯救你来了!

1
2
3
4
5
6
7
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 怎么样

------ Young For Perl 6! ------