読者です 読者をやめる 読者になる 読者になる

Elixirのパターンマッチ

Elixir

JSONを取得するサンプル

適当なAPIサーバーを立ててJSONを取得してみます。

APIサーバー

http://localhost:3000/capitals.jsonにアクセスすると首都一覧を返します。

const express = require('express')
const app = express()

app.get('/capitals.json', (request, response) => {
  response.contentType('application/json')

  const capitals = [
    { country: 'China', city: 'Beijing' },
    { country: 'Japan', city: 'Tokyo' },
    { country: 'Mongolia', city: 'Ulan Bator' },
    { country: 'North Korea', city: 'Pyhongyang' },
    { country: 'South Korea', city: 'Seoul' },
  ]

  response.send(JSON.stringify(capitals))
})

app.listen(3000)
クライアント

HTTPoisonのサンプル通りに記述してJSONを取得します。

defmodule JsonHandler do
  def main(args) do
    fetch_capitals
  end

  def fetch_capitals do
    url = "http://localhost:3000/capitals.json"

    case HTTPoison.get(url) do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
        IO.puts body
      {:ok, %HTTPoison.Response{status_code: 404}} ->
        IO.puts "Not found :("
      {:error, %HTTPoison.Error{reason: reason}} ->
        IO.puts "Unknown error"
    end
  end
end
実行
$ ./json_handler
[{"country":"China","city":"Beijing"},{"country":"Japan","city":"Tokyo"},{"country":"Mongolia","city":"Ulan Bator"},{"country":"North Korea","city":"Pyhongyang"},{"country":"South Korea","city":"Seoul"}]

最初のcaseにマッチするのでレスポンスが出力されました。

次にサーバーを起動しない状態で実行すると...

$ ./json_handler
Unknown error

最後のcaseにマッチするのでメッセージが出力されます。

ではマッチしないケースはどうなるでしょうか。
503を返すようにサーバーを書き換えて実行してみます。

response.status(503)
response.send()
$ ./json_handler
** (CaseClauseError) no case clause matching: {:ok, %HTTPoison.Response{body: "", headers: [{"X-Powered-By", "Express"}, {"Content-Type", "application/json"}, {"Date", "Thu, 18 Aug 2016 04:39:06 GMT"}, {"Connection", "keep-alive"}, {"Content-Length", "0"}], status_code: 503}}
    (error_handling_sample) lib/error_handling_sample.ex:9: ErrorHandlingSample.fetch_capitals/0
    (elixir) lib/kernel/cli.ex:76: anonymous fn/3 in Kernel.CLI.exec_fun/2

マッチするケースがないよ、ってことでクラッシュしてしまいます。
というわけで200, 404以外にマッチするパターンを追加します。

    case HTTPoison.get(url) do
      {:ok, %HTTPoison.Response{status_code: 200, body: body}} ->
        IO.puts body
      {:ok, %HTTPoison.Response{status_code: 404}} ->
        IO.puts "Not found :("
      {:ok, %HTTPoison.Response{status_code: status_code}} ->
        IO.puts "Status code #{status_code}"
      {:error, %HTTPoison.Error{reason: reason}} ->
        IO.puts "Unknown error"
    end

これで503も拾えました。

$ ./json_handler
Status code 503

case文を関数化する

もう少しElixirらしく?、各パターンを関数に書き換えます。
要注意なのがcase文と同じく先にある関数からマッチがされていくという点です。
つまり関数の記述順序も気にしないといけません。(まぁこの場合はパターンが悪いと思いますが)

defmodule JsonHandler do
  def main(args) do
    fetch_capitals
  end

  def fetch_capitals do
    url = "http://localhost:3000/capitals.json"

    HTTPoison.get(url)
    |> process_response
  end

  defp process_response({:ok, %HTTPoison.Response{status_code: 200, body: body}}) do
    IO.puts body
  end

  defp process_response({:ok, %HTTPoison.Response{status_code: 404}}) do
    IO.puts "Not found :("
  end

  defp process_response({:ok, %HTTPoison.Response{status_code: status_code}}) do
    IO.puts "Status code #{status_code}"
  end

  defp process_response({:error, %HTTPoison.Error{reason: reason}}) do
    IO.puts "Unknown error"
  end
end

クラッシュ怖い

マッチするパターンがないとクラッシュするって言われると、あらゆるパターン(特にエラー処理)を想定しなければならず、正常系よりも異常系の処理を書くことがメインのお仕事になってきます。

そんなの辛い。

"Let it crash."

qiita.com

Elixir(とそのベースになっているErlang)のプロセスは生成のためのコストが小さいため「下手にエラー処理するコードを書いてプロセスを維持するよりはさっさとクラッシュさせて、それに続く処理の中で対策して再起動したほうがよい」という思想があります。

クラッシュ上等というわけですね。

というわけで今度はクラッシュしまくるスタイルに書き換えてみる予定です。