Elixir

Elixir のパターンマッチを攻略しよう

More than 3 years have passed since last update.

Elixir にあって Ruby や JavaScript のような言語にない特徴といえば


  • 軽量プロセス (+ OTP周り)

  • パターンマッチ

の2点が大きく、その他の部分というのはだいたい「あの言語のこれだな」という風に対応させて理解できる(パターンマッチを実装した他の関数型言語になれてる人にとっては別かもしれないが)。 特に後者のパターンマッチの方は Elixir の文法の多くの部分の基礎になっている。従って、主観的にはパターンマッチさえ理解できれば Elixir の半分以上は理解できたと思っていいんじゃないかと思っていたりする。

というわけでカッとなってパターンマッチについて書いてみる。


パターンマッチとは

パターンマッチの例で、いきなり {x, y} = {1, 5} とかいう例を見せられても「変数扱うのに便利な記法か何かですかね? (ES6 の Destructuring assignment みたいなもん?)」 と思ってしまう。

そうではない。

パターンマッチは本来的には、その言葉の通り値とかに対する「パターン」の照合をするもので、例えば {:status_code, 200} というパターンがあったとき、このパターンには「ステータスコードが 200 のタプル」という値がマッチする、そしてマッチするならそれに伴い何がしかの処理が実行される、というものである。

例えば Elixir の case 文の本文はパターンマッチで記述する。Elixirだ 第1回強化版 前半 に載ってる例を見てみよう。

res = case File.read("/etc/sudoers") do

{:ok, res} -> res
{:error, :enoent} -> "oh it isn't here"
{:error, :eacces} -> "you can't read it"
_ -> "?"
end

IO.inspect res

File.read/1 の戻りの値は、{:ok, res}{:error, :enoent}{:error, :eacces} などのパターンに照合されて、マッチした式が実行される。

この場合 /etc/sudoers は root 権限でしか読めないこともあり、File.read/1{:error, :eacces} という を返す。それが {:error, :eacces} という パターン にマッチする。ので you can't read it が出力される。

・・・というように、パターンマッチの理解は、変数束縛ではない例から入った方がわかりやすい。


パターンマッチのユースケース

また別のユースケースをみてみよう。Qiita API v2 を使って Elixir に関する投稿の JSON を持ってきてタイトルを出力する例である。

defmodule MyQiita do

def fetch_titles(tag) do
HTTPotion.start
HTTPotion.get("https://qiita.com/api/v2/items?query=tag:" <> tag)
|> process_response
end

def process_response(%{status_code: 200, body: body}) do
body
|> Poison.decode!
|> extract_title
end

def extract_title(items), do: extract_title(items, [])

defp extract_title([], res), do: res

defp extract_title([%{"title" => title}|tail], res) do
extract_title(tail, [title|res])
end
end

MyQiita.fetch_titles("elixir") |> Enum.each fn(title) ->
IO.inspect title
end

実行すると以下のように出力される。

$ mix run

"すごいE本をElixirでやる(6章)"
"Elixirで進捗どうですか?"
"ElixirのGETTING STARTED(6.Binaries, strings and char lists)をやってみた"
"Elixir のリスト内包表記でクイックソート"
"ElixirのGETTING STARTED(7.Keywords, maps and dicts)をやってみた"
"[翻訳] Elixirのプロセスを探検する"
"ElixirのGETTING STARTED(8.Modules)をやってみた"
"Mac OSXにanyenv/exenvでElixirをインストールする"
...


関数定義とパターンマッチ

さて、コードの内容だが上から順に見てみよう。

  def fetch_titles(tag) do

HTTPotion.start
HTTPotion.get("https://qiita.com/api/v2/items?query=tag:" <> tag)
|> process_response
end

HTTP クライアントのライブラリには HTTPoison を使っている。HTTPoison で API を叩いて、受け取ったレスポンスを process_response/1 に渡している。なお、|> はパイプライン演算子で、左辺の値を右辺の第一引数に渡す演算子で、関数の結果を次の関数に渡して繋ぐ場合に便利。

次に process_response/1

  def process_response(%{status_code: 200, body: body}) do

body
|> Poison.decode!
|> extract_title
end

ここでパターンマッチが使われている。

関数の引数の定義に %{status_code: 200, body: body} とパターンが記述されている。これは、単に引数の変数名を定義してるのではなくて、パターンマッチのパターンを書いてるというところがミソ。

で、パターンの詳細はともかく、ここで何の意図をもってここにパターンを記述してるかというと、「API のレスポンスのステータスコード 200 の場合だけ、この関数を実行しろ」ということがしたいからだ。403 とか 404 とか、あるいは 500 とかが返ってきた場合はこの関数は実行されない。

つまり、関数の引数にパターンを書くと、その関数を実行したとき、引数に渡された値とそのパターンが照合されて、パターンにマッチした関数が実行されるのである。

それの何が良いのか? ─「もしステータスコード 200 の場合はこうして」みたいな命令的なコードを記述する必要なく、正常系の関数を宣言的に定義できる。エラー処理をする関数が書きたければ、エラーのパターンを定義したものを追加すればよい。


Let it crash!

ところで、ステータスコードが 200 でない値が返ってきた場合、上記のコードではそのパターンを処理するための関数がないため、FunctionClauseError でクラッシュする。あとステータスコードが 200 でも、実はレスポンスが JSON じゃなくて HTML だったりすると Posion.decode! のところでパースエラーか何かでクラッシュするはず。「ダメじゃん!」と思った方、いいえ、ダメじゃない。

詳しくは [翻訳] Elixirのプロセスアーキテクチャ または私は如何にして心配するのを止めてクラッシュを愛するようになったか を読むと良いのだけど、Erlang/Elixir では入力値に意図しない値が入ってきたときとか、そういうエラーは頑張ってエラー回避のコードを書くんじゃなくて、もうプロセスをクラッシュさせちまえ、というのが基本的な考え方になっている。そしてそのプロセスをまた別のプロセスで監視しておいて、クラッシュに対してどうアプリケーションを運用するかをそちら側でハンドリングする。リカバリするのか、そのまま終わらせるのかを考えるのはクラッシュしたプロセス自身ではなくて他のプロセスに任せてしまう。結果的にロジックとエラー処理を分離できる。

関数のパターンマッチで、入力値に合わせて宣言的に関数を書いといて、そのパターンに照合しないものはエラーとしてクラッシュさせてしまう。パターンマッチはこういう風に使う。


(飛ばしてOK) リストの分解と再帰

次、extract_title/1 であるが、こちらは JSON をパースして得られたデータ構造から記事タイトルに相当するものだけを抜き出す記述である。Enum.map とかを使って書いてもいいのだけど、ここでもやはりパターンマッチを使っている。

  def extract_title(items), do: extract_title(items, [])

defp extract_title([], res), do: res

defp extract_title([%{"title" => title}|tail], res) do
extract_title(tail, [title|res])
end

パターンマッチに慣れていないとちょっと複雑だけれども、これは Elixir で頻出するイディオムである。この段階で詳細がわからなくても、まあよく見るので、いずれすんなりわかるようになる。

ポイントは [head|tail] というパターンと再帰処理の組み合わせ。Elixir では [head|tail] という記述は「リストの先頭要素 (head) とそれ以外 (tail)」というパターンに相当し

[h|t] = [1, 2, 3, 4, 5]

と書くと h は 1 に、t[2, 3, 4, 5] というリストに束縛される。従って

def loop([h|t]) do

... h に対して何かする ...
loop(t)
end

という再帰処理はリスト一個一個に対して何かする、というリストに対する繰り返し処理を記述してることになる。

ただし [h|t] は空のリストにはマッチしない。空のリストは [] というパターンで記述される。従って

def loop([h|t]) do

... h に対して何かする ...
loop(t)
end

def loop([]), do: nil

とかして、空のリストにマッチする関数定義も一緒に用意してやらないとリストが空になたっところで FunctionClauseError になってしまう。

ここが分かれば、extract_title/1 の大枠は読み解けるはず。

もう一つポイントは [{"title" => title}|tail] という引数のパターンで、これは [h|t]h の部分に更にパターンを持ってきているところ。これで、リストの先頭要素の title 属性は、title という変数を束縛する、ということが一発で書けている。

このようにパターンマッチはデータの分解にも使われて、再帰処理と組み合わせることで繰り返し処理の基礎になったりもする。


ほか、パターンマッチが使われる場所

Qiita API の例から離れて、ほかにもパターンマッチがよく出てくる箇所を少しみてみよう。

Elixir の OTP (GenServer 編) に書いた軽量プロセス。軽量プロセスで外のプロセスからメッセージを受け取るには receive/0 を使うが、receive/0 の本文も case に同じくパターンマッチになっている。

receive do

{from, {:store, key, value}} ->
...
{from, {:lookup, key}} ->
...
end

外のプロセスからメッセージを待ち、メッセージが届いたらパターンと照合して、マッチする式を実行する。これによりプロセスは、任意のメッセージを受け取って、そのメッセージのパターンによって振る舞いを変えることができる、というわけだ。

Phoenix でもパターンマッチはよく使われている。

Rails ではルーターに /user/:id とか書いてやると URL の :id の箇所をパラメータとして受け取ることができるが、Phoenix にも同様の機能がある。その値の受け取りはパターンマッチで受け取ると良い。

ルーターに


web/router.ex

scope "/", Sandbox do

...
get "/hello/:name", PageController, :hello
end

と書いて、その設定に対応するアクションを


web/controllers/page_controller.ex

defmodule Sandbox.PageController do

...
def hello(conn, %{"name" => name}) do
render conn, "hello.html", name: name
end
end

と書き、関数定義でパターンマッチで受け取る。こうしておけば、意図しない値が入ってきたら例によってエラーになって安全である。


変数束縛とパターンマッチ : = は「パターンマッチ演算子」

・・・という、case 文やら関数定義におけるパターンマッチの役割を理解してから、改めて変数束縛の件をみると、それもパターンマッチのひとつのユースケースとして捉えることができ、結果的に理解しやすいと思う。

で、実は Elixir における = は代入演算子ではなく、また変数束縛にだけ使われる演算子でもなくパターンマッチ演算子なんである。左辺のパターンと、右辺の値をパターンマッチする、というのが = の本来的な説明だ。

だから

:ok = :ok

というのは、右辺を評価した値 :ok と左辺のパターン :ok を照合して、結果成立する。

:ok = :ng

は同じように考えて、成立しない。

そして左辺に変数が置かれている場合は

x = 1

右辺を評価した結果、左辺をどうすれば式が成立するかを Elixir は考え、結果的に x は 1 に束縛されるのである。

だから

{:ok, res} = File.read("/etc/hosts")

というパターンマッチは、右辺の結果として {:ok, ファイルの内容} という値が返ってきた場合に限りマッチし、変数 res がファイルの内容に束縛されればこの式が成立すると考え res は値に束縛される。

このように パターンの中に変数があったとき、その変数を値に束縛することで照合が成立するなら、Elixir はその変数を値に束縛しようとする (そして成立しないなら変数束縛はしない) のである。

スッキリ!


Getting Start を読む

さて、ここまで来てから改めて公式のドキュメント パターンマッチング - Pattern matching (日本語) を読んでみるといい。

中に出てくる例は以下のものだがスラスラ理解できるはず。

iex(1)> x = 1

1
iex(2)> 1 = x
1
iex(3)> 2 = x
** (MatchError) no match of right hand side value: 1

最初の行では x を 1 に束縛すればパターンマッチが成立するから束縛、2行目は 1 というパターンに右辺の値を照合して成立、3行目は 2 というパターンに右辺の値を照合してマッチエラー、である。

{a, b, c} = {:hello, "world", 42}

これも先に説明した通り。

{:ok, result} = {:ok, 13}

同じく。

[head|tail] = [1, 2, 3, 4, 5]

途中でこれのユースケースを見た通り。


if 文

ところで、ここまで条件分岐的なこととか再帰とかやったけども、if 文が一切出てこなかった。なぜか。

実は if 文の条件式というのは Elixir においてはパターンマッチなんである。パターンマッチをして、結果 true なら式を実行せよ、というのが if 文なんですな。そしてここまで見たとおりその「パターンマッチの結果ああしろこうしろ」というのは関数定義で宣言的に書いたり、case 文で書いたりで表現できてしまうので、if 文の出番がほとんどないんである。


まとめ

パターンマッチは Elixir のあれやこれ ・・・ 変数束縛、データ型の分解、繰り返し処理、関数の選択的実行、条件分岐などの基礎をなす重要な機能のひとつである。のでこれを理解すると Elixir のいろんな文法が氷解してアハ体験!() みたいな感じになる・・・と思う。

そしてパターンマッチを巧く使うと、いろんな記述を宣言的に書くことができ、それと Erlang/Elixir の Let it crash! なポリシーを組み合わせることで、強力なエラー処理基盤を手に入れることになる、というわけである。

おしまい。