Edited at
gumi Inc.Day 10

Elixir: パターンマッチングを使う

パターンマッチングは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
_ -> :others
end
end
end

iex> MyCase.get_result({:ok, "success"})

"success"
iex> MyCase.get_result({:error, "something wrong"})
"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"]

パターンマッチングには、取り出すデータを絞り込んだり、条件や場合分け、データの確認など、さまざまな使い方があります。ぜひ活用してみてください。