第九章. Associatives

Associatives

声明

本章翻译仅用于 Raku 学习和研究, 请支持电子版或纸质版

第九章. Associatives

Associative 使用被称为键的任何名字索引到值。Associative是无序的,因为键没有相对顺序。其他语言具有类似的数据类型,它们称为关联数组,字典,散列,映射或类似的东西。有几种类型的专用关联数据结构,你已经使用了其中的一些。

Pairs

A Pair has a single key and a value. You’ve already used these in their adverbial form, although you didn’t know they were Pairs. Create a Pair through general object construction with the name and value as arguments:

Pair 具有单个键和值。你已经以他们的副词形式使用了这些,虽然你不知道他们是 Pair。通过一般对象构造创建一个 Pair,名称和值作为参数:

my $pair = Pair.new: 'Genus', 'Hamadryas';

The => is the Pair constructor. You don’t have to quote the lefthand side because the => does that for you as long as it looks like a term:

=>Pair 构造函数。你不必将左侧用引号引起来,因为 => 为你做了,只要它看起来像一个项:

my $pair = Genus => 'Hamadryas';  # this works
my $nope = ⛇    => 'Hamadryas';  # this doesn't

Any value can be a Pair value. Here’s a value that’s a List:

Pair 的值可以是任意值。下面这个 Pair 的值是一个 List

my $pair = Pair.new: 'Colors', <blue black grey>;

Combining .new and => probably doesn’t do what you want. Passing it a single Pair means that you are missing its value. The .new method thinks that the Pair is the key and you forgot the value:

结合 .new=> 可能不会做你想要的。给 .new 传递单个 Pair 就意味着你丢掉了值。 .new 方法认为 Pair 是键,而你忘记了值:

my $pair = Pair.new: 'Genus' => 'Hamadryas';  # WRONG!

副词

A more common syntax is the adverbial form that you have already seen with Q quoting. Start with a colon, add the unquoted name, and specify the value inside <> for allomorphic quoting or inside () where you quote the value yourself:

更常见的语法是你在 Q 引用中已经看到的副词形式。从冒号开始,添加不带引号的名字,并在 <> 内指定用于异形引号的值,或在 () 中指定你自己用引号引起的值:

my $pair = :Genus<Hamadryas>;
my $pair = :Genus('Hamadryas');

my $genus = 'Hamadryas';
my $pair  = :Genus($genus);

Without an explicit value an adverb has the value True:

如果没有显式的值,那么副词的值为 True

my $pair = :name;  #  name => True

Using an adverb with Q with no value means you turn on that feature (everything else is False by default):

使用不带值的 Q 的副词意味着你开启该功能(默认情况下其他所有内容都为 False):

Q :double /The name is $butterfly/;

If you’d like it to be False instead, put a ! in front of the key name:

如果你想要它是假的,那就放一个 ! 在键名前面:

my $pair = :!name; #  name => False

You can create a Pair from a scalar variable. The identifier becomes the key and the value is the variable’s value. You’ll see more of this coming up:

你可以从标量变量创建一个 Pair。标识符成为键,值是变量的值。你会看到更多这样的事情:

my $Genus = 'Hamadryas';
my $pair = :$Genus;   # same as 'Genus' => 'Hamadryas';

There’s also a tricky syntax that reverses a numeric value and alpha-headed text key. This is a bit prettier for adverbs representing positions, such as 1st, 2nd, 3rd, and so on:

还有一种棘手的语法可以反转数值和带字母的文本键。这对于代表位置的副词来说有点漂亮,比如 1st, 2nd, 3rd等等:

my $pair = :2nd; # same as nd => 2

The .key and .value methods extract those parts of the Pair:

.key.value 方法提取 Pair 中的那些部分:

put "{$p.key} => {$pair.value}\n";

The .kv method returns both as a Seq:

.kv 方法作为 Seq 返回:

put join ' => ', $pair.kv;

EXERCISE 9.1Develop a subroutine that creates Pairs for the numbers from 0 to 10. Given the argument 1, it returns the Pair :1st. Given 2, it returns :2nd. Given 3, it returns :3rd. For all other numbers, it uses th as the suffix.

练习9.1 开发一个子例程,为 0 到 10 之间的数字创建 Pair。给定参数 1,它返回 Pair :1st。给定 2,它返回::2nd。给定 3,它返回::3rd。对于所有其他数字,它使用 th 作为后缀。

修改 Pair

You can’t change the key of a Pair. You’d have to make a new Pair to get a different key:

你无法更改 Pair 的键。你必须创建一个新的 Pair 才能获得不同的键:

my $pair = 'Genus' => 'Hamadryas';
$pair.key = 'Species';  # Nope!

If the Pair value is a container you can change the value inside the container, but if you constructed it with a literal Str there’s no container and you can’t change the value:

如果 Pair 的值是容器,则可以更改容器内的值,但如果使用文字字符串构造它,则没有容器,并且你无法更改值:

my $pair = 'Genus' => 'Hamadryas';
$pair.value = 'Papillo'; # Nope!

You can assign to it if that value came from a variable storing a container:

如果该值来自存储容器的变量,则可以为其分配:

my $name = 'Hamadryas';
my $pair = 'Genus' => $name;
$pair.value = 'Papillo';

Remember that not all variables are mutable. You may have bound to a value:

请记住,并非所有变量都是可变的。你可能已经绑定了一个值:

my $name := 'Hamadryas';  # bound directly to value, no container
my $pair = 'Genus' => $name;
$pair.value = 'Papillo';  # Nope! Still a fixed value

To ensure that you get a container, assign the value to an anonymous scalar. You don’t create a new named variable and you end up with a container:

要确保获得容器,请将值指定给匿名标量。你不创建新的命名变量,最终得到一个容器:

my $pair = 'Genus' => $ = $name;
$pair.value = 'Papillo';  # Works!

.freeze the Pair to make the value immutable no matter how it came to you:

冻结(.freeze) Pair 以使值不可变,无论它以怎样呈现给你:

my $name = 'Hamadryas';
my $pair = 'Genus' => $name;
$pair.freeze;
$pair.value = 'Papillo';  # Nope!

There’s one last thing about Pairs. You can line up the colon forms head-to-foot and it will create a list even though you don’t use commas:

Pair 的最后一件事。你可以将冒号形式从头到脚排列,即使你不使用逗号,它也会创建一个列表:

my $pairs = ( :1st:2nd:3rd:4th );

That’s the same as the form with commas:

这与逗号形式相同:

my $pairs = ( :1st, :2nd, :3rd, :4th );

You’ve already seen this with Q: you can turn on several features by lining up adverbs. The :q:a:c here arethree separate adverbs:

你已经在 Q 中看过这个:你可以通过排列副词来开启几个功能。 :q:a:c 这里有三个单独的副词:

Q :q:a:c /Single quoting @array[] interpolation {$name}/;

Maps

A Map is an immutable mapping of zero or more keys to values. You can look up the value if you know the key. Here’s a translation of color names to their RGB values. The .new method takes a list of Strs and their values:

Map 是零或多个键与值的不可变映射。如果你知道键,则可以查找该值。这是颜色名到 RGB 值的转换。 .new 方法接收字符串及其值的列表:

my $color-name-to-rgb = Map.new:
    'red',    'FF0000',
    'green',  '00FF00',
    'blue',   '0000FF',
    ;

You can use the fat arrow to make a list of Pairs:

你可以使用胖箭头来创建 Pair 列表:

my $color-name-to-rgb = Map.new:
    'red'    => 'FF0000',
    'green'  => '00FF00',
    'blue'   => '0000FF',
    ;

Using the fat arrow notation with autoquoting won’t work; the method thinks these are named arguments instead of Pairs for the Map and they are treated as options to the method instead of keys and values. This gives you a Map with no keys or values:

使用带有自动引用功能的胖箭头符号将不起作用;该方法认为这些是命名参数而不是映射的Pair,它们被视为方法的选项而不是键和值。这为你提供了没有键或值的Map

# don't do this!
my $color-name-to-rgb = Map.new:
    red    => 'FF0000',
    green  => '00FF00',
    blue   => '0000FF',
    ;

A Map is fixed; you can’t change it once you’ve created it. This may be exactly what you want since it can keep something else from accidentally modifying it:

Map 是固定的;一旦你创建它就不能改变它。这可能正是你想要的,因为它可以防止其他东西意外修改它:

$color-name-to-rgb<green> = '22DD22'; # Error!

To look up one of the color codes you subscript the object. This is similar to Positionals but uses different postcircumfix characters. Use the autoquoting <> or quote it yourself with {}:

要查找其中一个颜色代码,你可以下标该对象。这与 Positional 类似,但使用不同的 postcircumfix 字符。使用autoquoting <> 或使用 {} 自行引用:

put $color-name-to-rgb<green>;    # quoted key with allomorph
put $color-name-to-rgb{'green'};  # quoted key
put $color-name-to-rgb{$color};   # quoted key with interpolation

If you want to look up more than one key at a time, you can use a slice to get a List of values:

如果要一次查找多个键,可以使用切片获取值列表

my @rgb = $color-name-to-rgb<red green>

Checking Keys

To check that a key exists before you try to use it, add the :exists adverb after the single-element access. This won’t create the key. You’ll get True if the key is in the Map and False otherwise:

要在尝试使用键之前检查键是否存在,请在单元素访问后添加 :exists 副词。这不会创建键。如果键在 Map 中,则为 True,否则为 False

if $color-name-to-rgb{$color}:exists {
    $color-name-to-rgb{$color} = '22DD22';
}

The .keys method returns a Seq of keys:

.keys 方法返回一个键的序列

for $color-name-to-rgb.keys {
    put "$^key => {$color-name-to-rgb{$^key}}";
}

You do something similar to get only the values:

你做类似的事情只得到值:

my @rgb-values = $color-name-to-rgb.values;

The .kv method returns a key and its value at the same time. This saves you some complexity inside the Block:

.kv 方法同时返回键及其值。这可以节省Block内部的一些复杂性:

for $color-name-to-rgb.kv -> $name, $rgb {
    put "$name => $rgb";
}

Placeholder values inside the Block (but not a pointy Block) can do most of the work for you:

Block 中的占位符值(但不是尖头)可以为你完成大部分工作:

for $color-name-to-rgb.kv {
    put "$^k => $^v";
}

Creating from a Positional

You can create a Map from a Positional using .map. That returns a Seq that you can use as arguments to .new. These create new values based on the original ones:

你可以使用 .mapPositional 创建 Map。返回一个可以用作 .new 参数的 Seq。这些基于原始值创建新值:

my $plus-one-seq =  (1..3).map: * + 1;
my $double       = (^3).map: { $^a + $^a }
注意

Although the Map type and the .map method have the same name and do a similar job, one is an immutable object that provides the translation whereas the other is a method that transforms a Positional into a Seq.

虽然 Map 类型和 .map 方法具有相同的名称并执行类似的工作,但是一个是提供转换的不可变对象,而另一个是将 Positional 转换为 Seq 的方法。

Your Block or thunk can take more than one parameter. Use two parameters to construct Pairs:

你的Block或实形转换程序可以接受多个参数。使用两个参数构建Pair

my $pairs = (^3).map: { $^a => 1 }; # (0 => 1 1 => 1 2 => 1)
my $pairs = (^3).map: * => 1;       # same thing

There’s also a routine form of map where the code comes first and the values come after it. There’s a required comma between them in either form:

还有一种例程形式的 map,其中代码先出现,值出现在代码之后。两种形式之间都有一个必需的逗号:

my $pairs = map { $^a => 1 }, ^3;
my $pairs = map * => 1, ^3;

The result of the .map can go right into the arguments for .new:

.map 的结果可以直接进入 .new 的参数:

my $map-thingy = Map.new: (^3).map: { $^a => 1 }

These examples work because you want to produce Pairs. If you want to simply create multiple items for a larger list you need to create a Slip so you don’t end up with a List of Lists:

这些示例有效,因为你想要生成 Pair。如果你只想为更大的列表创建多个项目,则需要创建一个 Slip,这样你就不会得到列表列表

my $list = map { $^a, $^a * 2 }, 1..3; # ((1 2) (2 4) (3 6))
put $list.elems;  # 3

You can fix that with slip. This creates a Slip object that automatically flattens into the structure that contains it:

你可以用 slip 修复它。这将创建一个Slip对象,该对象会自动展平到包含它的结构中:

my $list = map { slip $^a, $^a * 2 }, 1..3; # (1 2 2 4 3 6)
put $list.elems;  # 6

EXERCISE 9.2Rewrite the subroutine from the previous section using a Map to decide which Pair you should return. If a number is not in the Map, use th. Add a new rule that numbers ending in 5 (but not 15) should get the suffix ty (like, 5ty).

练习9.2使用 Map 重写上一节中的子程序,以决定应该返回哪一个 Pair。如果数字不在 Map 中,请使用 th。添加一个新规则,以 5 (但不是 15)结尾的数字应该得到后缀 ty(如,5ty)。

检查允许的值

A common use for a Map is to look up permissible values. Perhaps you only allow certain inputs in your subroutines. You can make those the keys of a Map. If they are in the Map they are valid. If they aren’t, well, they aren’t.

Map 的常见用途是查找允许的值。也许你只允许子程序中的某些输入。你可以把它们作为Map的键。如果他们在Map 中他们是有效的。如果他们不是,那么,他们不是。

Go through the list of colors and return the color names, which you’ll use as the keys, and some value (1 is serviceable). You really want just the keys, so you can look them up later:

浏览颜色列表并返回颜色名称(你将用作键)和一些值(1可用)。你真的只想要键,所以你可以稍后查看它们:

my @permissable_colors = <red green blue>;
my $permissable_colors =
    Map.new: @permissable_colors.map: * => 1;

loop {
    my $color = prompt 'Enter a color: ';
    last unless $color;

    if $permissable_colors{$color}:exists {
        put "$color is a valid color";
        }
    else {
        put "$color is an invalid color";
        }
    }

This sort of data structure makes the lookup time the same no matter how many keys you have. Consider what you’d have to do with just the List. This scan-array subroutine checks each element of the array until it finds a match:

无论你拥有多少个键,这种数据结构都会使查找时间保持不变。考虑一下你只需要列表。此扫描数组子例程检查数组的每个元素,直到找到匹配项:

sub scan-array ( $list, $item ) {
    for @$list {
        return True if $^element eq $item;
        }
    return False;
    }

You might shorten your search by using .first to stop when it finds an appropriate element. At worst this checks every element every time:

你可以使用 .first 缩短搜索范围,以便在找到合适的元素时停止搜索。在最坏的情况下,每次检查每个元素:

sub first-array ( Array $array, $item ) {
    $array.first( * eq $item ).Bool;
    }

EXERCISE 9.3Use the .map technique to construct a Map from numbers between 1 and 10 (inclusively) to their squares. Create a loop to prompt for a number. If the number is in the Map, print its square.

练习9.3使用 .map 技术从1到10之间的数字(包括)构造一个Map到它们的正方形。创建一个循环以提示输入数字。如果数字在Map中,则打印其正方形。

Hashes

The Hash is like a Map but mutable. You can add or delete keys and update values. This is the Associativetype you’ll probably use the most. Create a Hash through its object constructor:

Hash就像 Map,但却是可变的。你可以添加或删除键并更新值。这是你可能最常使用的关联类型。通过其对象构造函数创建一个Hash

my $color-name-to-rgb = Hash.new:
    'red',    'FF0000',
    'green',  '00FF00',
    'blue',   '0000FF',
    ;

That’s a bit tedious. You can enclose the key-value list in %() instead:

这有点乏味。你可以将键值列表括在 %() 中:

my $color-name-to-rgb = %(  # Still makes a Hash
    'red',    'FF0000',
    'green',  '00FF00',
    'blue',   '0000FF',
    );

Curly braces also work, but this is a discouraged form. With the fat arrow there are Pairs inside the braces so the parser thinks this is a Hash:

花括号也有效,但这是一种沮丧的形式。使用胖箭头,在大括号内有Pair,所以解析器认为这是一个Hash

my $color-name-to-rgb = {  # Still makes a Hash
    'red'   => 'FF0000',
    'green' => '00FF00',
    'blue'  => '0000FF',
    };

If the parser doesn’t get enough hints about the contents of the braces, you might end up with a Block instead of a Hash:

如果解析器没有获得关于花括号内容的足够提示,那么最终可能会使用Block而不是Hash

my $color-name-to-rgb = {  # This is a Block!
    'red',   'FF0000',
    'green', '00FF00',
    'blue',  '0000FF',
    };

There’s a special sigil for Associative. If you use the % sigil you can assign a List to create your Hash:

Associative 有一个特殊的印记。如果你使用 % sigil,你可以指定一个列表来创建你的哈希

my %color-name-to-rgb =
    'red',    'FF0000',
    'green',  '00FF00',
    'blue',   '0000FF'
    ;

Perhaps you don’t like your definition of blue. You can assign a new value to it. Notice that the sigil does not change for the single-element access:

也许你不喜欢你对蓝色的定义。你可以为其重新赋值。请注意,单元素访问的sigil不会更改:

%color<blue> = '0000AA';  # a bit darker

You can remove a key with the :delete adverb. It returns the value of the just-deleted key:

你可以使用 :delete 副词删除键。它返回刚刚删除的键的值:

my $rgb = %color<blue>:delete

You can add new colors by assigning to the key that you want:

你可以通过分配所需的键来添加新颜色:

%color<mauve> = 'E0B0FF';

EXERCISE 9.4Update your ordinal suffix program to use a Hash. That’s the easy part. Once you’ve got that working, use your Hash to cache values so you don’t have to compute their result again.

练习9.4 更新你的序数后缀程序以使用Hash。这很容易。一旦起效了,使用你的Hash缓存值,这样你就不必再次计算他们的结果。

Accumulating with a Hash

Counting is another common use for a Hash. The key is the thing you want to count and its value is the number of times you’ve encountered it. First, you need something to count. Here’s a program that simulates rolling some dice:

计数是Hash的另一种常见用法。键是你想要计算的东西,它的值是你遇到它的次数。首先,你需要一些东西来计算。这是一个模拟滚动骰子的程序:

sub MAIN ( $die-count = 2, $sides = 6, $rolls = 137 ) {
    my $die_sides = 6;

    for ^$rolls {
        my $roll = (1..$sides).roll($die-count).List;
        my $sum = [+] $roll;
        put "($roll) is $sum";
    }
}

The .roll method picks an element from your List the number of times you specify. Each time it picks an element is independent of other times, so it might repeat some values. This produces output that shows the individual die values and the sum of the values:

.roll 方法从List中选择一个元素指定的次数。每次拾取元素都与其他时间无关,因此它可能会重复某些值。这会生成输出,显示各个芯片值和值的总和:

(3 4) is 7
(4 1) is 5
(6 4) is 10
(2 6) is 8
(6 6) is 12
(1 4) is 5
(5 6) is 11

Now you have several things to count. Start by counting the sums. Inside that for, use the sum as the Hash key and the number of times you encounter it as the value:

现在你需要考虑几件事。首先计算总和。在 for 里面,使用 sum 作为 Hash 键以及你遇到它的次数作为值:

sub MAIN ( $die-count = 2, $sides = 6, $rolls = 137 ) {
    my $die_sides = 6;

    my %sums;
    for ^$rolls {
        my $roll = (1..$sides).roll($die-count).List;
        my $sum = [+] $roll;
        %sums{$sum}++;
        }

    # sort the hash by its value
    my $seq = %sums.keys.sort( { %sums{$^a} } ).reverse;

    for @$seq {
        put "$^a: %sums{$^a}"
        }
    }

Now you get a dice sum frequency report:

现在你得到一个骰子和的频率报告:

7: 27
8: 25
5: 19
4: 13
9: 12
6: 11
11: 9
10: 8
3: 7
2: 3
12: 3

If you are motivated enough, you can compare those values to the probabilities for perfect dice. But there’s another interesting thing you can count—the rolls themselves. If you use $roll as the key it stringifies it. You can then count the unique stringifications. Sort the results so equivalent rolls such as (1 6) and (6 1) become the same key:

如果你有足够的动力,你可以将这些值与完美骰子的概率进行比较。但是你可以计算另一个有趣的事情 - rolls 本身。如果你使用 $roll 作为键,则将其字符串化。然后,你可以计算唯一的字符串。对结果进行排序,使等效的卷(如(1 6)(6 1))成为相同的键:

sub MAIN ( $die-count = 2, $sides = 6, $rolls = 137 ) {
    my $die_sides = 6;

    my %sums;
    for ^$rolls {
        my $roll = (1..$sides).roll($die-count).sort.List;
        %sums{$roll}++;
        }


    my $seq = %sums.keys.sort( { %sums{$^a} } ).reverse;

    for @$seq {
        put "$^a: %sums{$^a}"
        }
    }

Now you get a sorted list of your dice rolls:

现在你得到一个你的骰子卷的排序列表:

3 4: 15
1 4: 11
1 2: 10
2 5: 9
3 5: 9
3 6: 9
2 3: 8

EXERCISE 9.5Create a program to count the occurrences of words in a file and output the words sorted by their count. Store each lowercased word as the key in a hash and increment its value every time you see it. Don’t worry about punctuation or other characters; you’ll learn how to deal with those later. What happens if two words have the same count?

练习9.5 创建一个程序来计算文件中单词的出现次数,并输出按其计数排序的单词。将每个小写单词作为键存储在哈希中,并在每次看到它时增加其值。不要担心标点符号或其他字符;你将学习如何在以后处理这些问题。如果两个单词的计数相同,会发生什么?

Multilevel Hashes

Hash values can be almost anything, including another Hash or Array. Here’s an example with a couple of Hashes that count the number of butterflies in the Hamadryas and Danaus genera:

散列值几乎可以是任何值,包括另一个散列或数组。这是一个有几个哈希数的例子,它们计算了哈马德里亚斯和丹那属的蝴蝶数量:

my %Hamadryas = map { slip $_, 0 }, <
    februa
    honorina
    velutina
    >;

my %Danaus = map { slip $_, 0 }, <
    gilippus
    melanippus
    >;

But you want to contain all of that in one big Hash, so you construct that. The Hash value is another Hash:

但是你想在一个大Hash中包含所有这些,所以你构建它。哈希值是另一个哈希:

my %butterflies = (
    'Hamadryas' => %Hamadryas,
    'Danaus'    => %Danaus,
    );

say %butterflies;

The %butterflies data structure looks like this (using the discouraged braces form):

%butterflies 数据结构看起来像这样(使用不鼓励的花括号形式):

{Danaus => {gilippus => 0, melanippus => 0},
Hamadryas => {februa => 0, honorina => 0, velutina => 0}}

Suppose you want to see the count for Danaus melanippus. You have to look in the top-level Hash to get the value for Danaus, then take that value and look at its melanippus keys:

假设你想看看Danaus melanippus的数量。你必须查看顶级Hash以获取Danaus的值,然后获取该值并查看其melanippus键:

my $genus = %butterflies<Danaus>;
my $count = $genus<melanippus>;

That’s too much work. Line up the subscripts in one expression:

那工作太多了。在一个表达式中排列下标:

put "Count is  %butterflies<Danaus><melanippus>";

When you want to count a particular butterfly, you can do that:

当你想要计算一只特定的蝴蝶时,你可以这样做:

%butterflies<Danaus><melanippus>++;

EXERCISE 9.6Read the lines from the butterfly census file (from https://www.learningraku.com/downloads/) and break each line into a genus and species. Count each combination of genus and species. Report your results.

练习9.6 读取蝴蝶人口普查文件中的行(来自https://www.learningraku.com/downloads/),并将每行划分为属和种。计算属和物种的每个组合。报告你的结果。

EXERCISE 9.7Modify the previous exercise to write the genus and species counts to a file. Each line of the file should have the genus, species, and count separated by a tab. You’ll need this file for an exercise in Chapter 15.

练习9.7 修改上一个练习,将属和种类计数写入文件。文件的每一行都应具有由制表符分隔的属,种和计数。你将需要此文件进行第15章的练习。

总结

Associatives let you quickly get from a Str to another value. There are several types that facilitate this. At the lowest level is the Pair of one key and one value. A Map fixes those once created (much like a List), while a Hash is more flexible (much like an Array). These will probably be some of the most useful and hard-working data structures that you’ll encounter.

Associative让你快速从字符串到另一个值。有几种类型可以促进这一点。最低级别是一对一键和一个值。 Map 修复了曾经创建的(很像列表),而Hash更灵活(很像数组)。这些可能是你将遇到的一些最有用和最勤奋的数据结构。

comments powered by Disqus