技術メモ

プログラミングとか電子工作とか

Elixir入門(第七章 キーワードリストとマップ)

f:id:ysmn_deus:20190122112104p:plain

どうも、靖宗です。
今回は"Keyword lists and maps"。辞書型みたいなもん?mapはおそらくリストやタプルに対する繰り返し処理というか、アレでしょう!

Keyword lists(キーワードリスト)

キーワードリストはアトムと値のセット(タプルを用いる)をリストにしたもので表されます。

iex> list = [{:a, 1}, {:b, 2}]
[a: 1, b: 2] # ただのタプルのリストではなさそう
iex> list == [a: 1, b: 2]
true

一応蛇足だけど、{:atom, value}じゃないリストはどうなるのかチェック。

iex> list = [{2, 1}, {3, 2}]
[{2, 1}, {3, 2}]

ただのタプルのリストになった。
あと、スルーしてたけど[a: 1, b: 2]という書き方もできる。こっちの方がPythonに似てて好きだけどまぁどっちでもいいや。

なぜか言及がありませんでしたがキーワードリストの要素にアクセスするにはキーワードリスト[アトム]という表現でアクセスできるみたいです。

iex> list[:a]
1

キーワードリストはあくまでリストなのでリストの演算子++が使える。

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

ちょっとまて、keyに対して値が一意に決まらない?
[a: 0, a: 1, b: 2]
なんか、そういうもんらしい。
なので、list[:a]としたときは、一番最初の該当箇所の値が得られる。

  • キーは必ずアトム
  • キーは開発者によって整列されている(Pythonの辞書型のように順番はしらんで!なんて事にはならない?)
  • キーは1回以上使える

というのが仕様だそうです。うーん謎仕様。
the Ecto libraryってのがこの特性つかっとるんやで!とありますがそもそもこのライブラリを詳しく知らないのでスキップ!
おそらく利便性があるからそうなってるのだと今は信じて次へ。

実は5章で出てきたifがマクロでありif/2と表現されてたのは、このキーワードリストを内部で用いているからとのこと。

iex> if false, do: :this, else: :that
:that
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]

一番最後のところなのですが、普通の辞書型や連想配列ならマッチしそうな所。
おそらくキーワードリストはあくまでリストなので順番が効いてきてるのでしょう。

Maps(マップ)

maps(マップ)はElixirの中では「後藤」のデータとのこと(嘘)。
おそらくキーワードリストを通常のデータセットとして使う場合は不便だからこういうのが用意されている?
表現方法は%{}

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

存在しないキーにアクセスするとnilが返ってくる模様。

マップの特性は以下のよう。

  • マップはキーとしてどんな値でもとれる
  • マップのキーに順番はない

いわゆる辞書型っぽい!

マップのパターンマッチングは使いやすそう。

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}

キーの一致の際に値を取り出す、とか少ない行で書けそう。
(しらない人が見たら可読性落ちそうですが)

マップのキーには変数も使用可能。

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}

変数でマッチングするときはピンオペレータを忘れずに。(代入されちゃう。)

上で言及してませんでした(チュートリアルにも参照程度にしか書かれてなかった)が、Keywordモジュールと同じ様なMapモジュールがあります。

iex> Map.get(%{:a => 1, 2 => :b}, :a)
1
iex> Map.put(%{:a => 1, 2 => :b}, :c, 3)
%{2 => :b, :a => 1, :c => 3}
iex> Map.to_list(%{:a => 1, 2 => :b})
[{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}

でも追加はできない模様。素直にMapモジュールを使用した方がよさそう。

キーがアトムだけの時はキーワードリストの時のようにかける。

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

あと、アトムのキーにはプロパティへアクセスするように値へアクセスできる。

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}

あと、最後に注意書きが。
MapsはErlangの新しい機能なので昔の環境ではHashDictモジュールが要るかもな!と書いてあります。
基本昔のことは気にしなくて良いとは思いますが、もしライブラリ関連でトラブルに巻き込まれるとしたらこういう互換性のないところなので頭の片隅には置いておきたいところ。

Nested data structures(ネストデータ構造)

上記のキーワードリストやマップを使えばネストしたデータ構造も表現可能!

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"}]

辞書型とか基本ネストしますよね。
usersの:johnのageにアクセスするときは

iex> users[:john].age
27

となります。users.john.ageとか無理なんですね。とか思ったけどよく考えたら.あとむでアクセスできるのはマップのみ。
users.john.ageでアクセスしたくば

iex(38)> users = %{
...(38)> john: %{name: "John", age: 27, languages: ["Erlang", "Ruby", "Elixir"]},
...(38)> mary: %{name: "Mary", age: 29, languages: ["Elixir", "F#", "Clojure"]}
...(38)> }
%{
  john: %{age: 27, languages: ["Erlang", "Ruby", "Elixir"], name: "John"},
  mary: %{age: 29, languages: ["Elixir", "F#", "Clojure"], name: "Mary"}
}
iex(39)> 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"}]

頑張ればマップの更新機能とかで書けそうなところですが、キーワードリストの更新方法分からないし(新しく作ってそのときに変更?)、たぶんこのマクロを用いるのがシンプルで間違えにくそう。
update_in/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"}]

更新する値の所に無名関数を放り込んでます。
なんかput_in/3とかアリティが3のマクロもあるみたいなんですが、今回は勘弁して・・・