プログラミング Elixir 第八章


概要

プログラミング Elixir

パターンマッチを使ったマップの利用方法、構造体や辞書型の使い方を解説している

マップとキーワードリストどちらを使うべきか

以下の条件で、キーワードリスト [a: 1, b: 2, c: 3]マップ %{a: 1, b: 2, c: 3} をどうやって使い分ける

キーワードリスト

  • 同じキーが 2 回以上現れる
  • 要素の順番を保証する必要がある

マップ

  • 内容について、パターンマッチを行う必要がある
  • その他諸々

キーワードリスト

主に関数に渡されるオプションの用途で使われる。

defmodule Database do
  @defaults [
    adapter: "sqlite3",
    encoding: "utf8",
    pool: 5,
    password: "hoge",
    socket: "/tmp/mysql.sock"
  ]

  def configures(env, options \\ []) do
    options = Keyword.merge(@defaults, options)

    IO.puts "#{env}:"
    IO.puts "  adapter:  #{options[:adapter]}"
    IO.puts "  encoding: #{options[:encoding]}"
    IO.puts "  pool:     #{Keyword.get(options, :pool)}"
    IO.puts "  username: #{Keyword.get(options, :username, "default_user")}"
    IO.puts "  password: #{Keyword.get_values(options, :password)}"
    IO.puts "  socket:   #{Keyword.fetch!(options, :socket)}"
  end
end
iex> Database.configures("development", adapter: "mysql2", encoding: "ascii", password: "new_password")
development:
  adapter:  mysql2
  encoding: ascii
  pool:     5
  username: default_user
  password: new_password
  socket:   /tmp/mysql.sock

options[:adapter] で取り出す以外にも Keyword モジュール、Enum モジュールの関数を使うこともできる。

マップ

キー・値のセットのデータを扱いたい際に、幅広く使われる型

iex> map = %{first_name: "yamada", last_name: "taro", birthday: 19991010}
%{birthday: 19991010, first_name: "yamada", last_name: "taro"}
iex> Map.keys map
[:birthday, :first_name, :last_name]
iex> Map.values map
[19991010, "yamada", "taro"]
iex> map.first_name
"yamada"
iex> Map.put map, :address, "Tokyo"
%{address: "Tokyo", birthday: 19991010, first_name: "yamada", last_name: "taro"}
iex> Map.pop map, :address
{nil, %{birthday: 19991010, first_name: "yamada", last_name: "taro"}}
iex> Map.pop map, :first_name
{"yamada", %{birthday: 19991010, last_name: "taro"}}

マップのパターンマッチ

マップは指定したキーと値があるかを判定する際によく使われる。
例えば以下の例はマッチが成功する。

iex> user = %{name: "taro", address: "Japan"}
%{address: "Japan", name: "taro"}

# name key が存在するか
iex> %{name: a} = user
%{address: "Japan", name: "taro"}
iex> a
"taro"

# name, address key が存在するか
iex> %{name: _, address: _} = user
%{address: "Japan", name: "taro"}

# name の値は taro か
iex> %{name: "taro"} = user
%{address: "Japan", name: "taro"}

次の様に、値が一致しない、key が存在しないなどの場合はマッチが失敗する。

iex> %{name: "hanako"} = user
** (MatchError) no match of right hand side value: %{address: "Japan", name: "taro"}

iex> %{age: _} = user
** (MatchError) no match of right hand side value: %{address: "Japan", name: "taro"}

パターンマッチを使って指定したキーに関連した値を取り出せることを利用して、for で以下のような実装ができる。

users = [
  %{name: "taro", address: "Japan"},
  %{name: "tom", address: "US"},
  %{name: "hanako", address: "Japan"},
  %{name: "mike", address: "US"}
]

for user = %{address: country} <- users, country == "Japan", do: user

#=> [%{address: "Japan", name: "taro"}, %{address: "Japan", name: "hanako"}]

address の値を取り出して、"Japan" の user を出力している。

ガード節を使って、複数の body を実装した例。

defmodule Sample do
  def evaluate(%{name: name, score: score})
  when score > 40 do
    IO.puts "#{name} is an excellent player"
  end

  def evaluate(%{name: name, score: score})
  when score < 10 do
    IO.puts "#{name} is a poor player"
  end

  def evaluate(player) do
    IO.puts "#{player.name} is an average player"
  end
end
iex> users = [
...>   %{name: "Taro", score: 50},
...>   %{name: "Tom", score: 40},
...>   %{name: "Hanako", score: 20},
...>   %{name: "Mike", score: 5}
...> ]

iex> users |> Enum.each(&Sample.evaluate/1)
Taro is an excellent player
Tom is an average player
Hanako is an average player
Mike is a poor player

キーへの値の束縛

キーはパターンマッチによる束縛はできない

iex> %{key1: a} = %{key1: 1, key2: 2}
%{key1: 1, key2: 2}
iex> a
1

iex> %{a: 1} = %{key1: 1, key2: 2}
** (MatchError) no match of right hand side value: %{key1: 1, key2: 2}

変数キーをパターンマッチに使う

変数キーをパターンマッチに使用する場合は、以前見たようにピン演算子を使用する。

iex> for key <- [:name, :score] do
...>   %{^key => value} = player
...>   value
...> end
["Taro", 50]

マップの更新

パイプ文字を使うと簡単に値を置き換えた、新しい Map を作ることができる

iex> m = %{a: 1, b: 2, c: 3}
%{a: 1, b: 2, c: 3}
iex> %{m | a: "update"}
%{a: "update", b: 2, c: 3}
iex> %{m | b: "update"}
%{a: 1, b: "update", c: 3}
iex> %{m | c: "update"}
%{a: 1, b: 2, c: "update"}

新しい key => value を追加したい場合は、Map.put_new/3 を使うと良い

iex> Map.put_new(m, :d, "update")
%{a: 1, b: 2, c: 3, d: "update"}

構造体

%{} とあれば Map である、ということはわかるが、どんなキーを持つのか?キーはどんなデフォルト値を持つのか?型はあるのか?などの情報はわからない。それらを明確に宣言することができるのが構造体である。

構造体は制限のついた Map になっていて、キーはアトムになっており、Map の一部の機能が使えない。
モジュールになっていて、defstruct でどんなキー・バリューをもっているかを宣言する。

defmodule Player do
  defstruct name: "", team: "default_team", score: 0
end
iex> %Player{}
%Player{name: "", score: 0, team: "default_team"}
iex> %Player{name: "tarokichi"}
%Player{name: "tarokichi", score: 0, team: "default_team"}
iex> %Player{name: "tarokichi", score: 100}
%Player{name: "tarokichi", score: 100, team: "default_team"}

iex> player_1 = %Player{name: "tarokichi", score: 100, team: "yellow"}
%Player{name: "tarokichi", score: 100, team: "yellow"}
iex> player_1.name
"tarokichi"
iex> player_1.team
"yellow"

パターンマッチで値を取得することもできる

iex> %Player{name: player_1_name} = player_1
%Player{name: "tarokichi", score: 100, team: "yellow"}
iex> player_1_name
"tarokichi"

Map のようにパイプで値を更新できる

iex> player_2 = %Player{player_1 | name: "jiro"}
%Player{name: "jiro", score: 100, team: "yellow"}
iex> player_2
%Player{name: "jiro", score: 100, team: "yellow"}

構造体を使った関数を定義した例

defmodule Player do
  defstruct name: "", team: "default_team", score: 0

  def print_info(player = %Player{name: name}) when name != "" do
    IO.puts "name: #{player.name}, team: #{player.team}, score: #{player.score}"
  end

  def print_info(%Player{}) do
    raise "name is empty!!"
  end
end
iex> Player.print_info(%Player{name: "taro"})
name: taro, team: default_team, score: 0
:ok
iex> Player.print_info(%Player{})
** (RuntimeError) name is empty!!
    iex:184: Player.print_info/1
iex> Player.print_info(%{})
** (FunctionClauseError) no function clause matching in Player.print_info/1
    iex:179: Player.print_info(%{})

Player 以外を渡すとエラーになっていることがわかる。

入れ子になった辞書構造体

以下の様に構造体を入れ子にすることもできる。

defmodule Player do
  defstruct name: "", team: "default_team", score: 0
end

defmodule Competition do
  defstruct winner: %Player{}, name: "", prize: ""
end

Competition の winner が Player 構造体になっている。

iex> taro = %Player{name: "Yamada Taro"}
%Player{name: "Yamada Taro", score: 0, team: "default_team"}
iex> c1 = %Competition{winner: taro, name: "Zenkoku-Taikai", prize: "$10,000"}
%Competition{name: "Zenkoku-Taikai", prize: "$10,000",
 winner: %Player{name: "Yamada Taro", score: 0, team: "default_team"}}

値には . をつなげる事でアクセスできる。

iex> c1.winner.name
"Yamada Taro"
iex> c1.winner.team
"default_team"

通常の構造体と同様にパイプ文字で値を更新することができる。
例えば、winner の team を "yellow team" に更新したい場合は以下のようになる。

iex> c1
%Competition{name: "Zenkoku-Taikai", prize: "$10,000",
 winner: %Player{name: "Yamada Taro", score: 0, team: "default_team"}}
iex> taro
%Player{name: "Yamada Taro", score: 0, team: "default_team"}

iex> %Competition{c1 | winner: %Player{taro | team: "yellow team"}}
%Competition{name: "Zenkoku-Taikai", prize: "$10,000",
 winner: %Player{name: "Yamada Taro", score: 0, team: "yellow team"}}

このように Player の team を更新したいだけなのに、Competition 構造体を介して更新させなければならず、とても冗長になってしまう。
そこで、こういう場合は put_in を使って更新する。

iex> put_in(c1.winner.team, "red team")
%Competition{name: "Zenkoku-Taikai", prize: "$10,000",
 winner: %Player{name: "Yamada Taro", score: 0, team: "red team"}}

似ている関数で update_in というのもある。
これは、与えた関数を適用する。

update_in(c1.winner.score, &(&1 + 100))
%Competition{name: "Zenkoku-Taikai", prize: "$10,000",
 winner: %Player{name: "Yamada Taro", score: 100, team: "default_team"}}

上記の put_in などの例はマクロで値を渡しているが、put_in には関数が存在し、リストと key を渡して値を更新することもできる。

iex> player = %{name: %{first: "Taro", last: "Yamada"}}
%{name: %{first: "Taro", last: "Yamada"}}
iex> put_in(player, [:name, :first], "Hanako")
%{name: %{first: "Hanako", last: "Yamada"}}
iex> put_in(player, [:name, :last], "Tanaka")
%{name: %{first: "Taro", last: "Tanaka"}}

セット

セットの実装として MapSet が用意されている

iex> set = MapSet.new([1, 2, 3])
#MapSet<[1, 2, 3]>
iex> MapSet.delete(set, 2)
#MapSet<[1, 3]>
iex> MapSet.put(set, 4)
#MapSet<[1, 2, 3, 4]>
iex> set2 = MapSet.new([1, 2, 3, 5, 6])
#MapSet<[1, 2, 3, 5, 6]>
iex> MapSet.difference(set2, set)
#MapSet<[5, 6]>
iex> MapSet.union(set2, set)