Elixir、関数の複数の句とガード

Tsukasa OISHI

Elixirの勉強の続き( 前回の記事 )。今日は関数の複数の句とガードについてです。

関数の複数の句

Elixirでは関数の引数でもパターンマッチを活かすことができます。

defmodule Tsuka do
  def sum([head|tail]) do
    head + sum(tail)
  end

  def sum([]) do
    0
  end
end
iex(1)> Tsuka.sum([1,2,3,4,5,6,7,8,9])
45
iex(2)> Tsuka.sum([])
0
iex(3)> Tsuka.sum(1)
** (FunctionClauseError) no function clause matching in Tsuka.sum/1
    iex:2: Tsuka.sum(1)

Tsuka.sum/1 は配列を受け取ってその要素の総和を求めます。Enum.sum/1と同じものです。

Tsukaモジュール内には、sum関数がふたつ定義されています。このように関数は複数の句を持つことができます。関数に渡した引数とパターンがマッチした句が実行されます。

上の例では、[1,2,3,4,5,6,7,8,9] が引数として渡されています。これが最初の句である、sum([head|tail]) にマッチします。headが 1 になり、tailが[2,3,4,5,6,7,8,9] となります。ここでtailはふたたびsum関数に投げることで再帰的にsumが実行され、1 + 2 + 3 +... が処理されていきます。最終的に配列は空になるので二つ目の句にマッチして再帰処理は終了します。
パターンにマッチしなかった場合は例外が発生します。

これでようやく、Phoenixのアクション関数のパターンマッチがわかるようになりました。もうずいぶん前の記事になりますが、Phoenixのアクションでは関数でパターンマッチが使われていました。

  def show(conn, %{"id" => id}) do
    article = Repo.get!(Article, id)
    render(conn, "show.html", article: article)
  end

これによって、paramsに%{"id" => id}が存在しているときにだけ、show関数にマッチするようになります。

パターンマッチの優先度は最初に定義された句のほうが高いようです。

defmodule Tsuka do
  def hoge(%{name: name}) do
    "one:" <> name
  end

  def hoge(%{name: name, bad: bad}) do
    "two:" <> name <> ", " <> bad
  end
end
iex(1)> Tsuka.hoge(%{name: "tsuka"})
"one:tsuka"
iex(2)> Tsuka.hoge(%{name: "tsuka", bad: "oh"})
"one:tsuka"

%{name: "tsuka", bad: "oh"}%{name: name} にマッチしてしまうので二つ目の句は実行されません。この場合は、ふたつの句の定義順番を入れ替えれば問題が解決します。

ガード

複数の句のパターンマッチだけだと実現できないことがあります。たとえば、引数の型によって処理をわけたいときなどです。
こういうときにガードを使います。ガードはパターンマッチに加えて条件式を追加することができます。

defmodule Tsuka do
  def hello(name) when is_binary(name) do
    "hello, " <> name
  end

  def hello(name) when is_integer(name) do
    "hello, number:" <> Integer.to_string(name)
  end
end
iex(1)> Tsuka.hello("tsuka")
"hello, tsuka"
iex(2)> Tsuka.hello(3)      
"hello, number:3"

関数定義で引数のあとのwhen の後ろに条件を書きます。ガードに使える式には制限があります。詳細は本家サイトを参照してください。