loading...
gumi TECH Blog

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

gumitech profile image gumi TECH ・3 min read

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

gumitech profile

gumi TECH

@gumitech

gumi TECH は、株式会社gumiのエンジニアによる技術記事公開やDrinkupイベントなどの技術者交流を行うアカウントです。 gumi TECH Blog: http://dev.to/gumi / gumi TECH Drinkup: http://gumitech.connpass.com

gumi TECH Blog

株式会社gumiのエンジニアによる技術記事を公開しています。

Discussion

pic
Editor guide