Elixir: パターンマッチングを使う
gumi TECH
Posted on May 9, 2019
パターンマッチングはElixirの強力な構文です。基本的な使い方については、gumi TECH Blog「Elixir入門 04: パターンマッチング」に説明されています。本稿では、この記事の中で扱われていないコード例をいくつかご紹介します。
キーワードリストとマップ
前出「Elixir入門 04」には、リストにパターンマッチングを用いるコードは説明されています。もちろん、キーワードリストでも使えます。けれど、要素の数とその順序までマッチしなければなりません。そのため、実際に用いられることは少ないでしょう。
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]
キーワードリストとは異なり、パターンマッチングはマップではとても役立ちます。リストと比べて、マップのキーにはつぎの特徴があるからです。
- どのようなデータ型でも使える
- 順序は問わない
マップのパターンは、サブセットとマッチします。つまり、パターンの中に含まれるキーさえマッチしていればよいのです。したがって、空のマップ%{}
はすべてのマップにマッチします。
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}
条件
case/2
で条件に合うかどうか決めるのは、パターンマッチングです。残るすべての場合を引き受けるには_
を用います。
defmodule MyCase do
def get_result(tuple) do
case tuple do
{:ok, value} -> value
{:error, error} -> "error: " <> error
_ -> :others
end
end
end
iex> MyCase.get_result({:ok, "success"})
"success"
iex> MyCase.get_result({:error, "something wrong"})
"error: something wrong"
iex> MyCase.get_result({:oops})
:others
関数
関数の定めには、ガードと複数の句が加えられます。複数の句は、Elixirが上から順に試し、マッチした句を実行するのです。引数がいずれにもマッチしなければ、エラーになります。
defmodule Math do
def zero?(0) do
true
end
def zero?(x) when is_integer(x) do
false
end
end
IO.puts Math.zero?(0) #=> true
IO.puts Math.zero?(1) #=> false
IO.puts Math.zero?([1]) #=> ** (FunctionClauseError)
IO.puts Math.zero?(0.0) #=> ** (FunctionClauseError)
つぎのコードは、関数のデフォルト値とパターンマッチングを使った例です。Enum.join/2
は、リスト(Enumerable
)要素の間に第2引数の文字列を挟んで、バイナリ(文字列)につなげます。
defmodule Greeter do
def hello(names, language \\ "en")
def hello(names, language) when is_list(names) do
hello(Enum.join(names, ", "), language)
end
def hello(name, language) when is_binary(name) do
phrase(language) <> name
end
defp phrase("en"), do: "hello, "
defp phrase("ja"), do: "こんにちは"
end
iex> Greeter.hello("alice")
"hello, alice"
iex> Greeter.hello(["alice", "carroll"])
"hello, alice, carroll"
iex> Greeter.hello(["桃太郎", "金太郎", "浦島太郎"], "ja")
"こんにちは桃太郎, 金太郎, 浦島太郎"
再帰
つぎの関数はリスト要素の数値を2乗して、それらが要素に納められた新たなリストとして返します。このようにリスト要素を取り出して、新たなリスト要素に納めて返す処理はmapアルゴリズムと呼ばれ、関数型プログラミングの重要な考え方のひとつです。
defmodule Sum do
def square([]), do: []
def square([head | tail]), do:
[head * head | square(tail)]
end
iex> Sum.square([1, 2, 3])
[1, 4, 9]
つぎの関数は、リストが空になったら引数の合計値を返して、再帰呼び出しは終わります。空でなかったらふたつ目の関数が、テイルと合計値を引数に再帰呼び出しして、ヘッドの値を加えます。つまり、再帰のたびにヘッドの値を合計値に加えていくことになるのです。リストから要素を順に取り出して、ひとつの値にまとめる処理はreduceアルゴリズムと呼ばれます。
defmodule Sum do
def up(list, accumulator \\ 0)
def up([], accumulator), do: accumulator
def up([head | tail], accumulator),
do: up(tail, head + accumulator)
end
iex> Sum.up([1, 2, 3])
6
iex> Sum.up([4, 5], 6)
15
適切でない引数にエラーを出す
クエリ文字列から特定のキーの値を得たいとします。そこで書いてみたのが、つぎの関数です。パターンマッチングは使っていません。なお、使われている関数は、つぎのとおりです。
- String.split/3: 第1引数の文字列を第2引数の区切り文字列で分け、分けられた文字列を要素とするリストにして返します。第3引数はオプションです。
- Enum.find_value/3: 第1引数の列挙型データを順に取り出し、関数の条件に合った要素を処理して返します。第2引数がオプションです。
- Enum.at/3: 第1引数の列挙型データから、第2引数のインデックスの要素を取り出して返します。第3引数はオプションです。
defmodule Token do
def get(string, token) do
parts = String.split(string, "&")
Enum.find_value(parts, fn pair ->
key_value = String.split(pair, "=")
Enum.at(key_value, 0) == token && Enum.at(key_value, 1)
end)
end
end
関数に文字列とキーを渡せば、その値が取り出されて返されます。けれど、クエリ文字列のかたちを正しくキーと値の組みにしなくても、値は返されてしまうことがあります。
iex> Token.get("name=fumio&city=tokyo&lang=elixir", "name")
"fumio"
iex> Token.get("name=fumio=nonaka&city=tokyo&lang=elixir", "name")
"fumio"
iex> Token.get("name&city=tokyo&lang=elixir", "name")
nil
そこで、クエリ文字列の中にキーと値のふたつの組みでないものが含まれていたら、エラーを返したいと思います。このときは、つぎのようにリストでパターンマッチングさせればよいのです。要素数がマッチしなければ、エラーが返されます。コードも上の関数よりすっきりしました。
defmodule Token do
def get(string, token) do
parts = String.split(string, "&")
Enum.find_value(parts, fn pair ->
[key, value] = String.split(pair, "=")
key == token && value
end)
end
end
iex> Token.get("name=fumio&city=tokyo&lang=elixir", "name")
"fumio"
iex> Token.get("name=fumio=nonaka&city=tokyo&lang=elixir", "name")
** (MatchError) no match of right hand side value: ["name", "fumio", "nonaka"]
iex> Token.get("name&city=tokyo&lang=elixir", "name")
** (MatchError) no match of right hand side value: ["name"]
パターンマッチングには、取り出すデータを絞り込んだり、条件や場合分け、データの確認など、さまざまな使い方があります。ぜひ活用してみてください。
Posted on May 9, 2019
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.