Elixir入门教程-Keywords和maps

  1. 关键字列表
  2. 映射
  3. 嵌套数据结构

到目前为止,我们还没讨论过任何关联数据结构,即该数据结构是将一个值(或者多个值)和一个键关联起来。不同的语言对这种数据结构的称呼不一样,比如:字典、哈希、关联数组,等等。

Elixir里有两种主要的关联数据结构:关键字列表和映射。现在让我们来学习它们!

关键字列表

在许多函数式语言里,通常使用两个元素的元组组成的列表来表示一个键值对数据结构。在Elixir里,当我们有一个元组组成的列表,并且元组的第一个元素(即键)是原子,则我们叫这个列表是关键字列表:

1
2
3
4
iex> list = [{:a, 1}, {:b, 2}]
[a: 1, b: 2]
iex> list == [a: 1, b: 2]
true

正如上述所示,Elixir支持一种特殊的语法来定义这样的列表:[key: value]。它在底层是表示上述例子的元组组成的列表。既然关键字列表是列表,我们可以使用列表的所有可用操作。例如,我们可以使用 ++ 增加一个值到关键字列表里。

1
2
3
4
iex> list ++ [c: 3]
[a: 1, b: 2, c: 3]
iex> [a: 0] ++ list
[a: 0, a: 1, b: 2]

注意:加到前面的值将被首先查询到:

1
2
3
4
iex> new_list = [a: 0] ++ list
[a: 0, a: 1, b: 2]
iex> new_list[:a]
0

关键字列表很重要,因为它有三个特别的特性:

  • 键必须是原子。
  • 键的顺序由开发者指定。
  • 键可以重复出现。

例如,Ecto库 使用这些特性来提供一个写数据库查询的优雅领域特定语言:

1
2
3
4
query = from w in Weather,
where: w.prcp > 0,
where: w.temp < 20,
select: w

这些特性使得关键字列表成为Elixir里传递选项给函数的默认机制。在第5章,我们讨论 if/2 宏的时候,我们提到下述语法是被支持的:

1
2
iex> if false, do: :this, else: :that
:that

do: 和 else: 对是关键字列表!实际上,上述的调用等价于下面的调用:

1
2
iex> if(false, [do: :this, else: :that])
:that

也和下面的调用相同:

1
2
iex> if(false, [{:do, :this}, {:else, :that}])
:that

通常,当关键字列表是函数的最后一个参数的时候,方括号是可省略的。

虽然我们可以在关键字列表上进行模式匹配,但是实际操作中很少用,因为在列表上进行模式匹配需要列表的元素数量和它们的顺序匹配:

1
2
3
4
5
6
7
8
iex> [a: a] = [a: 1]
[a: 1]
iex> a
1
iex> [a: a] = [a: 1, b: 2]
** (MatchError) no match of right hand side value: [a: 1, b: 2]
iex> [b: b, a: a] = [a: 1, b: 2]
** (MatchError) no match of right hand side value: [a: 1, b: 2]

为了操作关键字列表,Elixir提供了Keyword模块。记住,关键字列表就是列表,因此它们和列表一样提供了线性性能特性。列表越长,就要花越多的时间查找到一个键、计算元素个数,诸如此类的。因为这个原因,关键字列表在Elixir里主要用来传递可选值。如果你需要存储许多项数据或者要保证一个键只对应一个值,那么你应该用映射。

映射

在Elixir里,任何时候你需要键值对存储,则映射就是你所要的数据结构。映射使用 %{} 语法来创建:

1
2
3
4
5
6
7
8
iex> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> map[:a]
1
iex> map[2]
:b
iex> map[:c]
nil

与关键字列表比较,我们已经可以看到有两个不同:

  • 映射允许任何值作为键。
  • 映射的键不遵循任何顺序。

与关键字列表相反,映射非常有用于模式匹配。当一个映射用在一个模式中,它将总是匹配上一个给定值的子集:

1
2
3
4
5
6
7
8
iex> %{} = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> %{:a => a} = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> a
1
iex> %{:c => c} = %{:a => 1, 2 => :b}
** (MatchError) no match of right hand side value: %{2 => :b, :a => 1}

如上所述,一个映射一定匹配,只要模式里的键存在于给定的映射里。因此,一个空映射匹配所有映射。

当访问、匹配和增加映射的键时,变量可以被使用:

1
2
3
4
5
6
7
8
iex> n = 1
1
iex> map = %{n => :one}
%{1 => :one}
iex> map[n]
:one
iex> %{^n => :one} = %{1 => :one, 2 => :two, 3 => :three}
%{1 => :one, 2 => :two, 3 => :three}

Map模块提供和Keyword模块相似的API来便于操作映射:

1
2
3
4
iex> Map.get(%{:a => 1, 2 => :b}, :a)
1
iex> Map.to_list(%{:a => 1, 2 => :b})
[{2, :b}, {:a, 1}]

当映射中的所有键都是原子时,您可以使用关键字语法来提高便捷程度:

1
2
iex> map = %{a: 1, b: 2}
%{a: 1, b: 2}

映射另一个有趣的特性是提供它们自己的语法来修改和访问原子键:

1
2
3
4
5
6
7
8
9
10
11
12
iex> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> map.a
1
iex> map.c
** (KeyError) key :c not found in: %{2 => :b, :a => 1}
iex> %{map | :a => 2}
%{2 => :b, :a => 2}
iex> %{map | :c => 3}
** (KeyError) key :c not found in: %{2 => :b, :a => 1}

上述访问和修改语法需要给定的键存在。例如,访问和修改 :c 键失败,因为映射里没有这个键。

当处理映射的时候,Elixir开发者一贯喜欢用 map.field 语法和模式匹配来替代 Map 模块里的函数,因为它们形成了一种断言式的编程风格。这篇博客 提供了解释和例子,如何在Elixir里通过写断言代码获得更简洁和更快的软件。

注意:映射是最近才引入Erlang虚拟机的,并且仅从Elixir v1.2开始,它们才有能力高效处理上百万键。因此,如果你使用较老的Elixir版本(v1.0或v1.1),并且你需要最少支持数百个键,那么你可以考虑使用HashDict模块

嵌套数据结构

我们常常有映射中有映射,或者甚至在映射里有关键字列表,等等。Elixir通过 put_in/2 和 update_in/2 提供了操作嵌套数据结构的便利,以及你可以在命令式语言里发现其他宏也提供这样的遍历而同时保持这个语言的不可修改的特性。

假设你有如下的结构:

1
2
3
4
5
6
iex> users = [
john: %{name: "John", age: 27, languages: ["Erlang", "Ruby", "Elixir"]},
mary: %{name: "Mary", age: 29, languages: ["Elixir", "F#", "Clojure"]}
]
[john: %{age: 27, languages: ["Erlang", "Ruby", "Elixir"], name: "John"},
mary: %{age: 29, languages: ["Elixir", "F#", "Clojure"], name: "Mary"}]

我们有一个用户的关键字列表,它的每一个值是一个映射,这个映射包含名字,年龄和用户喜欢的编程语言组成的列表。如果我们想访问约翰的年龄,我们可以这么写:

1
2
iex> users[:john].age
27

我们也可以用相同的语法来修改这个值:

1
2
3
iex> users = put_in users[:john].age, 31
[john: %{age: 31, languages: ["Erlang", "Ruby", "Elixir"], name: "John"},
mary: %{age: 29, languages: ["Elixir", "F#", "Clojure"], name: "Mary"}]

update_in/2 宏也相似,不过它允许我们传递一个函数来控制如何修改值。例如,让我们从玛丽的编程语言列表里删除掉Clojure:

1
2
3
iex> users = update_in users[:mary].languages, &List.delete(&1, "Clojure")
[john: %{age: 31, languages: ["Erlang", "Ruby", "Elixir"], name: "John"},
mary: %{age: 29, languages: ["Elixir", "F#"], name: "Mary"}]

关于 put_in/2 和 update_in/2 还有很多需要学习的,包括 get_and_update_in/2 这个函数允许我们一次性获取一个值并且修改数据结构。也有 put_in/3,update_in/3 和 get_and_update_in/3 这些函数,它们允许动态访问数据结构。请在 Kernel模块里仔细阅读它们各自的文档来深入理解它们的作用。

到此我们结束了Elixir里关联数据结构的介绍。你将发现,给定关键字列表和映射,你将总会有适用的工具来解决Elixir里需要关联数据结构的问题。

原文链接: http://elixir-lang.org/getting-started/keywords-and-maps.html