本稿はElixir公式サイトの許諾を得て「Keyword lists and maps」の解説にもとづき、加筆補正を加えてElixirのキーと値を関連づけたデータ構造についてご説明します。
値をキーに関連づけたデータ構造として、Elixirにはキーワードリストとマップが備わっています。他の言語で、ディクショナリーとかハッシュあるいは連想配列などと呼ばれるデータと似た仕組みです。
キーワードリスト
多くの関数型プログラミング言語では、キーと値のデータ構造をふたつの項目のタプルで表します。Elixirでは、はじめの項目つまりキーにアトムが用いられた2項目のタプルのリストを、キーワードリストと呼びます。キーワードリストは[キー: 値]
の構文で書くと便利です。データはタプルの基本構文[{キー: 値}]
を用いた場合と変わりがありません。
iex> list = [{:a, 1}, {:b, 2}]
[a: 1, b: 2]
iex> list == [a: 1, b: 2]
true
キーワードリストはリストとして操作できます。たとえば、別のキーワードリストの要素を加えたいとき使うのは++/2
演算子です。
iex> list ++ [c: 3]
[a: 1, b: 2, c: 3]
iex> [a: 0] ++ list
[a: 0, a: 1, b: 2]
リストに同じキーがあるとき、キーの参照は位置が前の要素を取り出します。
iex> new_list = [a: 0] ++ list
[a: 0, a: 1, b: 2]
iex> new_list[:a]
0
キーワードリストのキーにはつぎの3つの特徴があります。
- アトムである
- 順序づけされる
- 一意でなくてよい
たとえば、ElixirのライブラリEctoは、キーワードリストでデータベースクエリが書ける優れたDSLを提供しています。
query = from w in Weather,
where: w.prcp > 0,
where: w.temp < 20,
select: w
Elixirでは、キーワードリストは関数にオプションを渡すときの標準の仕組みとなっています。if/2
マクロのつぎの構文はその例です(「Elixir入門 05: 条件 - case/cond/if」「do/endブロック」参照)。
iex> if(false, do: :this, else: :that)
:that
:do
と:else
はひとつのキーワードリストの要素です。したがって、つぎのように書き替えられます。一般に、キーワードリストが関数の最後の引数のとき、角かっこ[]
は省けるのです。
iex> if(false, [do: :this, else: :that])
:that
もちろん、つぎのようにも書けます。
iex> if(false, [{:do, :this}, {:else, :that}])
:that
キーワードリストにもパターンマッチングは使えます。けれど、要素の数とその順序までマッチしなければなりません。そのため、実際に用いられることは少ないでしょう。
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でキーと値の組みを納めるデータ構造として、お勧めするのはマップです。マップは%{}/2
の構文でつくります。
iex> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> map[:a]
1
iex> map[2]
:b
iex> map[:c]
nil
リストと比べて、マップのキーにはつぎの特徴があります。
- どのようなデータ型でも使える
- 順序は問わない
キーワードリストとは異なり、パターンマッチングはマップではとても有用です。マップのパターンは、サブセットとマッチします。つまり、パターンの中に含まれるキーさえマッチしていればよいのです。したがって、空のマップ%{}
はすべてのマップにマッチします。
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}
変数は参照やパターンマッチング、あるいはマップに加えるキーにも使えます。
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
と似た便利な関数が備わっています。つぎのコードは、関数Map.get/3
(第3引数はデフォルトnil
)とMap.put/3
およびMap.to_list/1
を使った例です。
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}]
マップのキーの値は、|
を用いたつぎの構文で変えられます。ただし、キーはマップにすでに備わっていなければなりません。つまり、この構文で新たなキーは加えられないということです。
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
関数をお使いください。
iex> map = %{:a => 1, 2 => :b}
%{2 => :b, :a => 1}
iex> Map.put(map, :c, 3)
%{2 => :b, :a => 1, :c => 3}
キーが重複したときは、あとの値で上書きされます。
iex> %{:a => 1, 2 => :b, :a => :one}
%{2 => :b, :a => :one}
マップの中のキーがすべてアトムの場合は、つぎのように出力されます。
iex> %{:a => 1, :b => 2}
%{a: 1, b: 2}
そして、この簡略な構文は、マップをつくるときにも使えるのです。また、アトムのキーはドット.
でも参照できます。
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」参照)。
入れ子のデータ構造
マップやキーワードリストは、それぞれ入れ子にできます。また、マップとキーワードリストを互いに入れ子にすることも可能です。値はそれぞれの構文を組み合わせて得られます。
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
を用います。
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
マクロは、第1引数の値を第2引に数渡した関数で処理します。
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
put_in/3
update_in/3
get_and_update_in/3
Elixir入門もくじ
- Elixir入門 01: コードを書いて試してみる
- Elixir入門 02: 型の基本
- Elixir入門 03: 演算子の基本
- Elixir入門 04: パターンマッチング
- Elixir入門 05: 条件 - case/cond/if
- Elixir入門 06: バイナリと文字列および文字リスト
- Elixir入門 07: キーワードリストとマップ
- Elixir入門 08: モジュールと関数
- Elixir入門 09: 再帰
- Elixir入門 10: EnumとStream
- Elixir入門 11: プロセス
- Elixir入門 12: 入出力とファイルシステム
- Elixir入門 13: aliasとrequireおよびimport
- Elixir入門 14: モジュールの属性
- Elixir入門 15: 構造体
- Elixir入門 16: プロトコル
- Elixir入門 17: 内包表記
- Elixir入門 18: シギル
- Elixir入門 19: tryとcatchおよびrescue
- Elixir入門 20: 型の仕様とビヘイビア
- Elixir入門 21: デバッグ
- Elixir入門 22: Erlangライブラリ
- Elixir入門 23: つぎのステップ
Top comments (0)