DEV Community

gumi TECH for gumi TECH Blog

Posted on • Updated on

Elixir入門 07: キーワードリストとマップ

本稿は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
Enter fullscreen mode Exit fullscreen mode

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

iex> list ++ [c: 3]
[a: 1, b: 2, c: 3]
iex> [a: 0] ++ list
[a: 0, a: 1, b: 2]
Enter fullscreen mode Exit fullscreen mode

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

iex> new_list = [a: 0] ++ list
[a: 0, a: 1, b: 2]
iex> new_list[:a]
0
Enter fullscreen mode Exit fullscreen mode

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

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

たとえば、ElixirのライブラリEctoは、キーワードリストでデータベースクエリが書ける優れたDSLを提供しています。

query = from w in Weather,
      where: w.prcp > 0,
      where: w.temp < 20,
     select: w
Enter fullscreen mode Exit fullscreen mode

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

iex> if(false, do: :this, else: :that)
:that
Enter fullscreen mode Exit fullscreen mode

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

iex> if(false, [do: :this, else: :that])
:that
Enter fullscreen mode Exit fullscreen mode

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

iex> if(false, [{:do, :this}, {:else, :that}])
:that
Enter fullscreen mode Exit fullscreen mode

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

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]
Enter fullscreen mode Exit fullscreen mode

キーワードリストを操作するために、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
Enter fullscreen mode Exit fullscreen mode

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

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

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

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}
Enter fullscreen mode Exit fullscreen mode

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

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}
Enter fullscreen mode Exit fullscreen mode

モジュール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}]
Enter fullscreen mode Exit fullscreen mode

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

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
Enter fullscreen mode Exit fullscreen mode

キーを加えるときは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}
Enter fullscreen mode Exit fullscreen mode

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

iex> %{:a => 1, 2 => :b, :a => :one}
%{2 => :b, :a => :one}
Enter fullscreen mode Exit fullscreen mode

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

iex> %{:a => 1, :b => 2}
%{a: 1, b: 2}
Enter fullscreen mode Exit fullscreen mode

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

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}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

値を書き替えるには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
Enter fullscreen mode Exit fullscreen mode

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#"]
Enter fullscreen mode Exit fullscreen mode

get_and_update_in/2
put_in/3
update_in/3
get_and_update_in/3

Elixir入門もくじ

番外

Latest comments (0)