loading...
gumi TECH Blog

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

gumitech profile image gumi TECH Updated on ・4 min read

本稿は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入門もくじ

番外

Posted on by:

gumitech profile

gumi TECH

@gumitech

gumi TECH は、株式会社gumiのエンジニアによる技術記事公開やDrinkupイベントなどの技術者交流を行うアカウントです。 gumi TECH Blog: http://dev.to/gumi / gumi TECH Drinkup: http://gumitech.connpass.com

gumi TECH Blog

株式会社gumiのエンジニアによる技術記事を公開しています。

Discussion

pic
Editor guide