Elixir入門 07: キーワードリストとマップ
Tags: elixir, webdev, tutorial, programming
Published: 2018-10-30 01:56:29

---
title: Elixir入門 07: キーワードリストとマップ
published: true
description: Elixir公式サイトの許諾を得て「Keyword lists and maps」の解説にもとづき、加筆補正を加えてElixirのキーと値を関連づけたデータ構造についてご説明します。
tags: #elixir, #webdev, #tutorial, #programming
---

本稿はElixir公式サイトの許諾を得て「[Keyword lists and maps](https://elixir-lang.org/getting-started/keywords-and-maps.html)」の解説にもとづき、加筆補正を加えてElixirのキーと値を関連づけたデータ構造についてご説明します。

値をキーに関連づけたデータ構造として、Elixirにはキーワードリストとマップが備わっています。他の言語で、ディクショナリーとかハッシュあるいは連想配列などと呼ばれるデータと似た仕組みです。


# キーワードリスト
多くの関数型プログラミング言語では、キーと値のデータ構造をふたつの項目のタプルで表します。Elixirでは、はじめの項目つまりキーにアトムが用いられた2項目のタプルのリストを、キーワードリストと呼びます。キーワードリストは`[キー: 値]`の構文で書くと便利です。データはタプルの基本構文`[{キー: 値}]`を用いた場合と変わりがありません。

```elixir
iex> list = [{:a, 1}, {:b, 2}]
[a: 1, b: 2]
iex> list == [a: 1, b: 2]
true
```

キーワードリストはリストとして操作できます。たとえば、別のキーワードリストの要素を加えたいとき使うのは`++/2`演算子です。

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

リストに同じキーがあるとき、キーの参照は位置が前の要素を取り出します。

```elixir
iex> new_list = [a: 0] ++ list
[a: 0, a: 1, b: 2]
iex> new_list[:a]
0
```

キーワードリストのキーにはつぎの3つの特徴があります。

- アトムである
- 順序づけされる
- 一意でなくてよい

たとえば、Elixirのライブラリ[Ecto](https://hexdocs.pm/ecto/Ecto.html)は、キーワードリストでデータベースクエリが書ける優れたDSLを提供しています。

```elixir
query = from w in Weather,
      where: w.prcp > 0,
      where: w.temp < 20,
     select: w
```

Elixirでは、キーワードリストは関数にオプションを渡すときの標準の仕組みとなっています。`if/2`マクロのつぎの構文はその例です(「[Elixir入門 05: 条件 - case/cond/if](https://dev.to/gumi/elixir-05----casecondif-60o)」「do/endブロック」参照)。

```elixir
iex> if(false, do: :this, else: :that)
:that
```

`:do`と`:else`はひとつのキーワードリストの要素です。したがって、つぎのように書き替えられます。一般に、キーワードリストが関数の最後の引数のとき、角かっこ`[]`は省けるのです。

```elixir
iex> if(false, [do: :this, else: :that])
:that
```

もちろん、つぎのようにも書けます。

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

キーワードリストにもパターンマッチングは使えます。けれど、要素の数とその順序までマッチしなければなりません。そのため、実際に用いられることは少ないでしょう。

```elixir
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`モジュール](https://hexdocs.pm/elixir/Keyword.html)が備わっています。ただ、ご注意いただきたいのは、キーワードリストはリストだということです。パフォーマンスについては、リストと同じ考慮が要ります。キーを探したり、要素を数えたりするのは、リストが長くなるほど時間がかかるということです。そのため、キーワードリストは、おもにオプションの値を渡すのに用いられます。多くの要素を納めたいときや、キーを一意にしたいときには、マップを使うのがよいでしょう。


# マップ
Elixirでキーと値の組みを納めるデータ構造として、お勧めするのはマップです。マップは[`%{}/2`](https://hexdocs.pm/elixir/Kernel.SpecialForms.html#%25%7B%7D/1)の構文でつくります。

```elixir
iex> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> map[:a]
1
iex> map[2]
:b
iex> map[:c]
nil
```

リストと比べて、マップのキーにはつぎの特徴があります。

- どのようなデータ型でも使える
- 順序は問わない

キーワードリストとは異なり、パターンマッチングはマップではとても有用です。マップのパターンは、サブセットとマッチします。つまり、パターンの中に含まれるキーさえマッチしていればよいのです。したがって、空のマップ`%{}`はすべてのマップにマッチします。

```elixir
iex> %{} = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> %{:a => a} = %{2 => :b, :a => 1}
%{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}
```

変数は参照やパターンマッチング、あるいはマップに加えるキーにも使えます。

```elixir
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`](https://hexdocs.pm/elixir/Map.html)には、`Keyword`と似た便利な関数が備わっています。つぎのコードは、関数[`Map.get/3`](https://hexdocs.pm/elixir/Map.html#get/3)(第3引数はデフォルト`nil`)と[`Map.put/3`](https://hexdocs.pm/elixir/Map.html#put/3)および[`Map.to_list/1`](https://hexdocs.pm/elixir/Map.html#to_list/1)を使った例です。

```elixir
iex> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> Map.get(map, :a)
1
iex> Map.put(map, :c, 3)
%{2 => :b, :a => 1, :c => 3}
iex> Map.to_list(map)
[{2, :b}, {:a, 1}]
```

マップのキーの値は、`|`を用いたつぎの構文で変えられます。ただし、キーはマップにすでに備わっていなければなりません。つまり、この構文で新たなキーは加えられないということです。

```elixir
iex> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> %{map | 2 => :two}
%{2 => :two, :a => 1}
iex> %{map | :c => 3}
** (KeyError) key :c not found in: %{2 => :b, :a => 1}
    (stdlib) :maps.update(:c, 3, %{2 => :b, :a => 1})
    (stdlib) erl_eval.erl:255: anonymous fn/2 in :erl_eval.expr/5
    (stdlib) lists.erl:1263: :lists.foldl/3
```

キーを加えるときは`Map.put/3`関数をお使いください。

```elixir
iex> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> Map.put(map, :c, 3)
%{2 => :b, :a => 1, :c => 3}
```

キーが重複したときは、あとの値で上書きされます。

```elixir
iex> %{:a => 1, 2 => :b, :a => :one}
%{2 => :b, :a => :one}
```

マップの中のキーがすべてアトムの場合は、つぎのように出力されます。

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

そして、この簡略な構文は、マップをつくるときにも使えるのです。また、アトムのキーはドット`.`でも参照できます。

```elixir
ie> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> map.a
1
iex> map.2
** (SyntaxError) iex:15: syntax error before: "2"
iex> map[2]
:b
iex> map.c
** (KeyError) key :c not found in: %{2 => :b, :a => 1}
```

Elixirの開発者には`Map`モジュールの関数より、`map.field`の構文とパターンマッチングでマップを扱う方が好まれるようです。はっきりとわかりやすいスタイルでプログラミングができるからでしょう(「[Writing assertive code with Elixir](http://blog.plataformatec.com.br/2014/09/writing-assertive-code-with-elixir/)」参照)。


# 入れ子のデータ構造
マップやキーワードリストは、それぞれ入れ子にできます。また、マップとキーワードリストを互いに入れ子にすることも可能です。値はそれぞれの構文を組み合わせて得られます。

```elixir
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"}
]
iex> users[:mary]
%{age: 29, languages: ["Elixir", "F#", "Clojure"], name: "Mary"}
iex> users[:john].age
27
```

値を書き替えるには[`put_in/2`](https://hexdocs.pm/elixir/Kernel.html#put_in/2)を用います。

```elixir
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"}
]
iex> users[:john].age
31
```

[`update_in/2`マクロ](https://hexdocs.pm/elixir/Kernel.html#update_in/2)は、第1引数の値を第2引に数渡した関数で処理します。

```elixir
iex> users = update_in(users[:mary].languages, fn languages ->
...>   List.delete(languages, "Clojure")
...> end)
[
  john: %{age: 31, languages: ["Erlang", "Ruby", "Elixir"], name: "John"},
  mary: %{age: 29, languages: ["Elixir", "F#"], name: "Mary"}
]
iex> users[:mary].languages
["Elixir", "F#"]
```


[`get_and_update_in/2`](https://hexdocs.pm/elixir/Kernel.html#get_and_update_in/2)
[`put_in/3`](https://hexdocs.pm/elixir/Kernel.html#put_in/3)
[`update_in/3`](https://hexdocs.pm/elixir/Kernel.html#update_in/3)
[`get_and_update_in/3`](https://hexdocs.pm/elixir/Kernel.html#get_and_update_in/3)

#### Elixir入門もくじ
- [Elixir入門 01: コードを書いて試してみる](https://dev.to/gumi/elixir-01--2585)
- [Elixir入門 02: 型の基本](https://dev.to/gumi/elixir-02--30n1)
- [Elixir入門 03: 演算子の基本](https://dev.to/gumi/elixir-03--33im)
- [Elixir入門 04: パターンマッチング](https://dev.to/gumi/elixir-04--1346)
- [Elixir入門 05: 条件 - case/cond/if](https://dev.to/gumi/elixir-05----casecondif-60o)
- [Elixir入門 06: バイナリと文字列および文字リスト](https://dev.to/gumi/elixir-06--35na)
- Elixir入門 07: キーワードリストとマップ
- [Elixir入門 08: モジュールと関数](https://dev.to/gumi/elixir-08--1c4c)
- [Elixir入門 09: 再帰](https://dev.to/gumi/elixir-09--1a0p)
- [Elixir入門 10: EnumとStream](https://dev.to/gumi/elixir-10-enumstream-4fpb)
- [Elixir入門 11: プロセス](https://dev.to/gumi/elixir-11--2mia)
- [Elixir入門 12: 入出力とファイルシステム](https://dev.to/gumi/elixir-12--4og6)
- [Elixir入門 13: aliasとrequireおよびimport](https://dev.to/gumi/elixir-13-aliasrequireimport-55c1)
- [Elixir入門 14: モジュールの属性](https://dev.to/gumi/elixir-14--3511)
- [Elixir入門 15: 構造体](https://dev.to/gumi/elixir-15--4f43)
- [Elixir入門 16: プロトコル](https://dev.to/gumi/elixir-16--lif)
- [Elixir入門 17: 内包表記](https://dev.to/gumi/elixir-17--5gci)
- [Elixir入門 18: シギル](https://dev.to/gumi/elixir-18--5791)
- [Elixir入門 19: tryとcatchおよびrescue](https://dev.to/gumi/elixir-19-trycatchrescue-50i8)
- [Elixir入門 20: 型の仕様とビヘイビア](https://dev.to/gumi/elixir-20--j50)
- [Elixir入門 21: デバッグ](https://dev.to/gumi/elixir-21--21a1)
- [Elixir入門 22: Erlangライブラリ](https://dev.to/gumi/elixir-22-erlang-2492)
- [Elixir入門 23: つぎのステップ](https://dev.to/gumi/elixir-23--50ik)

##### 番外
- [Elixir入門: Plugを使うには](https://dev.to/gumi/elixir-plug-40lb)