LoginSignup
2
1

More than 5 years have passed since last update.

プログラミング Elixir 第八章

Posted at

概要

プログラミング 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)
2
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
2
1