7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

gumi Inc.Advent Calendar 2018

Day 10

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

Last updated at Posted at 2018-12-11

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

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

7
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
7
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?