17 トライ,キャッチとレスキュー - try, catch and rescue

エリクサーは3つのエラー機構を持っています: エラー,スローそしてイグジットです.この章ではそれらについて,それぞれどんな時に使われるとよいかも含めて詳しくみていきましょう.

Elixir has three error mechanisms: errors, throws and exits. In this chapter we will explore each of them and include remarks about when each should be used.

17.1 エラー - Errors

アトムへ数を足そうとすると,エラーの例を見ることができます.

A sample error can be retrieved by trying to add a number into an atom:

iex> :foo + 1
** (ArithmeticError) bad argument in arithmetic expression
     :erlang.+(:foo, 1)

ランタイムエラーはraise/1マクロを使うことでいつでも起こすことができます:

A runtime error can be raised any time by using the raise/1 macro:

iex> raise "oops"
** (RuntimeError) oops

他のエラーはraise/2にエラーの名前とキーワード引数のリストを渡すことで起こすことができます:

Other errors can be raised with raise/2 passing the error name and a list of keyword arguments:

iex> raise ArgumentError, message: "invalid argument foo"
** (ArgumentError) invalid argument foo

モジュールを作り,その中でdefexception/1マクロを使うと自分でエラーを定義することもできます.よくあるケースでは例外を定義するときにメッセージも入力できるようにします:

You can also define your own errors by creating a module and use the defexception/1 macro inside it. The most common case is to define an exception with a message field:

iex> defmodule MyError do
iex>  defexception message: "default message"
iex> end
iex> raise MyError
** (MyError) default message
iex> raise MyError, message: "custom message"
** (MyError) custom message

例外はtry/rescue構造で拾うことができます:

Exceptions can be rescued by using the try/rescue construct:

iex> try do
...>   raise "oops"
...> rescue
...>   e in RuntimeError -> e
...> end
%RuntimeError{message: "oops"}

上の例ではランタイムエラーを拾い,iexセッションでエラーが表示されるようにエラー自身を返しています.しかし実際のところElixirを使っている開発者がtry/rescue構造を使うことはほとんどないでしょう.例えば,多くの言語ではファイルを開けなかったときにエラーになるので,あなたがそれを拾わなければならないようになっています.一方Elixirでの関数File.read/1は,ファイルを開くのに成功した/失敗した情報を含んだタプルを返すようになっています:

The example above rescues the runtime error and returns the error itself which is then printed in the iex session. In practice Elixir developers rarely use the try/rescue construct though. For example, many languages would force you to rescue an error when a file cannot open successfully. Elixir instead provides a File.read/1 function which returns a tuple containing information if the file was opened with success or not:

iex> File.read "hello"
{:error, :enoent}
iex> File.write "hello", "world"
:ok
iex> File.read "hello"
{:ok, "world"}

ここにはtry/rescueが出てきません.ファイルを開くときにおこる複数の結果に対応したいときは,caseを使ってパターンマッチングするだけです:

There is no try/rescue here. In case you want to handle multiple outcomes of opening a file, you can simply use pattern matching with case:

iex> case File.read "hello" do
...>   {:ok, body} -> IO.puts "got ok"
...>   {:error, body} -> IO.puts "got error"
...> end

最終的に,ファイルを開いたときに例外にするかどうかはあなたのアプリケーションでどうするか決めることになります.なぜならElixirではFile.read/1や他の多くの関数が例外を押しつけてこないからです.どうすれば一番よくなるかは開発者へお任せしています.

At the end of the day, it is up to your application to decide if an error while opening a file is exceptional or not. That's why Elixir doesn't impose exceptions on File.read/1 and many other functions. Instead we leave it up to the developer to choose the best way to proceed.

ファイルがあることを期待している(そしてファイルがないと本当の意味でのエラー)の場合は単にFile.read!/1を使えます:

For the cases where you do expect a file to exist (and the lack of a file is truly an error) you can simply use File.read!/1:

iex> File.read! "unknown"
** (File.Error) could not read file unknown: no such file or directory
    (elixir) lib/file.ex:305: File.read!/1

言い変えると,try/rescueの利用を避けるのはフロー制御のためにエラーを利用しないためです.Elixirでは,エラーを期待していない,あるいは例外的な状況のために用意しています.フロー制御構造が必要な場合,スローが使われなければなりません.次にそれを見ていきましょう.

In other words, we avoid using try/rescue because we don't use errors for control flow. In Elixir, we take errors literally: they are reserved to unexpected and/or exceptional situations. In case you actually need flow control constructs, throws must be used. That's what we are going to see next.

17.2 スロー - Throws

Elixirでは後で捕れるように値を投げることができます.throwcatchがないと値が取れないような状況のためにthrowcatchという言葉は予約されています.

In Elixir, one can throw a value to be caught later. throw and catch are reserved for situations where it is not possible to retrieve a value unless by using throw and catch.

ライブラリでふさわしいAPIが提供されていないないような場合はあまりありません.例えばEnumモジュールで,13の倍数となるような最初の数を見つけたいときに適切なAPIが用意されていない場合を考えてみましょう:

Those situations are quite uncommon in practice unless when interfacing with a library that does not provide the proper APIs. For example, let's imagine the Enum module did not provide any API for finding a value and we need to find the first number that is a multiple of 13:

iex> try do
...>   Enum.each -50..50, fn(x) ->
...>     if rem(x, 13) == 0, do: throw(x)
...>   end
...>   "Got nothing"
...> catch
...>   x -> "Got #{x}"
...> end
"Got -39"

しかしながら実際にはEnum.find/2を使った方がシンプルです:

However, in practice one can simply use Enum.find/2:

iex> Enum.find -50..50, &(rem(&1, 13) == 0)
-39

17.3 イグジット - Exits

全てのElixirコードは複数のプロセスの中で動いていて,お互いにやりとりしています.プロセスが死んだとき,exitという信号を送ります.プロセスは明示的にイグジット信号を送ることで死ぬことができます:

Every Elixir code runs inside processes that communicates with each other. When a process dies, it sends an exit signal. A process can also die by explicitly sending an exit signal:

iex> spawn_link fn -> exit(1) end
#PID<0.56.0>
** (EXIT from #PID<0.56.0>) 1

上の例では,リンクしていたプロセスはexit信号と1という値を送って死にました.Elixirのシェルはそのメッセージを自動的に受けとって端末に表示しています.

In the example above, the linked process died by sending an exit signal with value of 1. The Elixir shell automatically handles those messages and prints them to the terminal.

exittry/catchを使うことで"キャッチされ"ます:

exit can also be "caught" using try/catch:

iex> try do
...>   exit "I am exiting"
...> catch
...>   :exit, _ -> "not really"
...> end
"not really"

try/catchを使うことが既に珍しいですが,イグジットをキャッチするのはもっと珍しいことです.

Using try/catch is already uncommon and using it to catch exits is even more rare.

exitシグナルはErlang VMで提供されている耐障害性において重要な部分を担っています.プロセスは通常,単にプロセスのexit信号を待つだけの監視プロセスを備えた監視ツリーにくみこまれて動作しています.監視プロセスがイグジット信号を受けとると,監視作戦が開始され,監視対象のプロセスは再起動させられます.

exit signals are an important part of the fault tolerant system provided by the Erlang VM. Processes usually run under supervision trees which are themselves processes that just wait for exit signals of the supervised processes. Once an exit signal is received, the supervision strategy kicks in and the supervised process is restarted.

監視システムの構造自体がtry/catchtry/rescueに似ているため,Elixirではそれらの構文があまり用いられていないのです.確実にエラーを拾うかわりに,エラーのあとに"早く失敗する"ことで監視ツリーがアプリケーションを既知の初期状態へと戻すことを保証しています.

It is exactly this supervision system that makes constructs like try/catch and try/rescue so uncommon in Elixir. Instead of rescuing a certain error, we'd rather "fail fast" since the supervision tree will guarantee our application will go back to a known initial state after the error.

17.4 アフター - After

特定の動作をした後,リソースをきれいに片付けられることを保証しなければならないことがあり,そういった場合にtry/afterが利用されます.例えばファイルを開くとき,必ず閉じることをtry/afterで保証できます:

Sometimes it is necessary to use try/after to guarantee a resource is cleaned up after some particular action. For example, we can open a file and guarantee it is closed with try/after block:

iex> {:ok, file} = File.open "sample", [:utf8, :write]
iex> try do
...>   IO.write file, "olá"
...>   raise "oops, something went wrong"
...> after
...>   File.close(file)
...> end
** (RuntimeError) oops, something went wrong

17.5 変数のスコープ - Variables scope

try/catch/rescue/afterブロックの中で定義した変数は外では見えないことを心に留めておいてください.大事なことです.これはtryブロックが失敗するかもしれず,そのとき変数が最初の場所で絶対に束縛されないことがありうるためです.言い変えると,このコードは無効です:

It is important to bear in mind that variables defined inside try/catch/rescue/after blocks do not leak to the outer context. This is because the try block may fail and as such the variables may never be bound in the first place. In other words, this code is invalid:

iex> try do
...>   from_try = true
...> after
...>   from_after = true
...> end
iex> from_try
** (RuntimeError) undefined function: from_try/0
iex> from_after
** (RuntimeError) undefined function: from_after/0

trycatchそしてrescueの導入はこれで終わりです.他の言語での利用頻度よりは低いですが,目的があって"規則にのっとった"コードにならないようなライブラリや状況で便利に使えることがあるかもしれません.

This finishes our introduction to try, catch and rescue. You will find they are used less frequently in Elixir than in other languages although they may be handy in some situations where a library or some particular code is not playing "by the rules".

次はElixirを構成している内包表記やシギルといったものについて話しましょう.

It is time to talk about some Elixir constructs like comprehensions and sigils.