技術メモ

プログラミングとか電子工作とか

Elixir入門(第十九章 try, catch, and rescue)

f:id:ysmn_deus:20190122112104p:plain

どうも、靖宗です。
流石にこの章は例外処理でしょう。
でも、rescueってなんぞや( ´゚д゚`)

Elixirには3つのエラー処理errors``throws``exitsがあるそうです。
順に追っていきます。

Errors

Errors(or exceptions)と書かれてるのでコレが例外?
どこかでraiseとかも出てきてましたがおそらくコレ。

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

アトムと数値を足し合わせようとしたのでエラーが出てます。
普通のエラー=例外という認識でよさそうです。

先ほど言及したraise/1を使用すればいつだってエラーを起こせる!

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

上記ではただのRuntimeErrorでしたが、raise/2で特定の例外も送出できるようです。

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

defexceptionを使用すれば例外を定義することもできるそうです。
既に定義済みの例外なども上書きできるそうで。実際にやってみる。

iex(77)> defmodule ArgumentError do
...(77)>   defexception message: "default message"
...(77)> end
warning: redefining module ArgumentError (current version loaded from c:/Program Files (x86)/Elixir/lib/elixir/ebin/Elixir.ArgumentError.beam)
  iex:77

{:module, ArgumentError,
 <<70, 79, 82, 49, 0, 0, 12, 52, 66, 69, 65, 77, 65, 116, 85, 56, 0, 0, 1, 78,
   0, 0, 0, 33, 20, 69, 108, 105, 120, 105, 114, 46, 65, 114, 103, 117, 109,
   101, 110, 116, 69, 114, 114, 111, 114, 8, 95, ...>>, :ok}
iex(78)> raise ArgumentError
** (ArgumentError) default message

warningがでてますができました。
ここでの定義でもやはりdefmoduleを利用しているので、基本的にこういうものはモジュールとして動作しているっぽいです。
ちなみに、例外送出時に出てくるメッセージをdefault messageにしてましたが、raiseの時に与えるとメッセージを書き換えれるようです。

iex(78)> raise ArgumentError, message: "custom message"
** (ArgumentError) custom message

エラーはtry/rescue構文で回収可能っぽい。いわゆる例外処理っぽい?

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

rescue以下は条件分岐?
また、eは%RuntimeError{message: "oops"}というところを鑑みるに構造体になってるみたいです。
ただ例外を拾いたいだけで、エラー内容を触らないなら下記の書き方でも良いそうで。

iex> try do
...>   raise "oops"
...> rescue
...>   RuntimeError -> "Error!"
...> end
"Error!"

ただし、Elixirではあまりtry/rescue構文が使われないようです。
というのも、おそらく大体の処理は例外としてエラーがでてくるのではなく、アトム入りタプルなどで返ってくるからっぽい。
良い例がFile.read/1で読み出し先のファイルが無かった場合。

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

エラーが出るのではなく、エラーのタプルが返ってきてます。
なので例外処理として書くのではなく、返ってくる値で条件分岐を書く。

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

こういう実装になってるのは開発者に裁量が委ねられてるから?
{:ok, body}で返ってくること前提で書いておけば、パターンマッチなどでbodyを抽出する際に例外が送出される筈なので、そこでエラーを回収する方針でコードを書くこともできそう。
ただElixirでは推奨されていないので素直に{:ok, hoge}などで条件分岐を書くのが良さそう。

今までにも数回出てきてる気がしますが、関数名に!を付けるとエラーを送出するバージョンで実装されてることが多いんだとか。

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

先ほどの理由であまり使わない方がよさそう。

Throws

例外の様なケースで値を取り出したい時にはthrowcatchを使えるそうです。
下記はEnumモジュールでリスト(-50..50)の最初から捜索して最初に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"

raiseでもよくね?とも思いますが、特定の値を取り出したい時にはこちらを使うのが良いのでしょう。
ただしEnumモジュールに上記のような関数Enum.find/2が実装されてますし、あまり使用されないようなので頭の片隅に置いとく程度で次へ。

Exits

Elixirのプロセスは自然に終了するときexitシグナルを送信するそうです。プロセス作成元に?
明示的にexitシグナルを送信することもできるそうで

iex> spawn_link fn -> exit(1) end
** (EXIT from #PID<0.56.0>) evaluator process exited with reason: 1

実際にやるとiexが再起動してるっぽいです。spawn_linkでリンクしてると、リンクしてるプロセス全部がおなくなりになるようですね。
上記の例は1exitシグナルとして送信しているようですが、エラーの時どうするんだとかは追々って感じでしょうか。
このexitシグナルはtry/catch構文でも受けることができて

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

とも書けるそうです。
ただ、めったにやらないって書いてるので無視でよさそう。

Erlang VMに置いてexitシグナルは重要だよ!って書いてますが、どう効いてくるのかはいろんなライブラリを使ってみて慣れるって感じでしょうか。

After

プリズムヲー

try/after構文は今までの例外処理同様なんですが、例外があってもafter以降を実行する構文のようです。

iex(1)> try do
...(1)>   IO.puts "hello"
...(1)> after
...(1)>   IO.puts "world"
...(1)> end
hello
world
:ok
iex(2)> try do
...(2)>   IO.puts "hello"
...(2)>   raise "oops"
...(2)> after
...(2)>   IO.puts "world"
...(2)> end
hello
world
** (RuntimeError) oops

例外の有無に限らず、afterの箇所が実行されています。ファイルのCloseなどに利用できるでしょうか。
ただし、このafterはプロセスが生きてる場合のみなのでspawn_linkなどでリンクしてるプロセスがexitシグナルを送ってきた場合は実行されないようです。
試しにやってみたんですが、おそらくexitコード送ってくる前にafterが実行されてるみたいでわざと処理を入れてます。

iex(1)> try do
...(1)>   IO.puts "hello"
...(1)>   spawn_link fn -> exit(1) end
...(1)>   total_sum = 1..100_000 |> Enum.map(&(&1 * 3)) |> Enum.filter(&(rem(&1, 2) != 0)) |> Enum.sum
...(1)>   raise "oops"
...(1)> after
...(1)>   IO.puts "world"
...(1)> end
warning: variable "total_sum" is unused (if the variable is not meant to be used, prefix it with an underscore)
  iex:4

hello
** (EXIT from #PID<0.339.0>) shell process exited with reason: 1

"world"が印字されてないのでafter以降は実行されてません。
ただFileは別プロセス中でクラッシュしてもきちんとcloseするようにできているようで、他のソケットなども同様にきちんとクローズするようになっているそうです。
これも独立プロセスとして稼働させる恩恵でしょうか。

また、afterの特性上、関数全体とかをtry/after構文で包みたい欲が湧くと思います。
そういう開発者のためか、ブロック全体をtry/hoge構文にするときにはtryを省略できるようです。

iex> defmodule RunAfter do
...>   def without_even_trying do
...>     raise "oops"
...>   after
...>     IO.puts "cleaning up!"
...>   end
...> end
iex> RunAfter.without_even_trying
cleaning up!
** (RuntimeError) oops

after同様、rescuecatchでも省略系が使えるそうですが、後ろの二つは使わん方がええよって言われてるのでafterでよく使うことになると思います。

Else

tryブロックで例外を想定してcatchrescueで待ち受けたとしましょう。
ここで例外が起こらなかった場合にのみ実行したい処理があるときはelseブロックを作成できるそうです。

iex> x = 2
2
iex> try do
...>   1 / x
...> rescue
...>   ArithmeticError ->
...>     :infinity
...> else
...>   y when y < 1 and y > -1 ->
...>     :small
...>   _ ->
...>     :large
...> end
:small

これも使用頻度希じゃね?とは思いますが、念のため。
ちなみにelseブロックの中で例外が出た場合はもちろん既存の例外処理には引っかからないので注意。

Variables scope

ここでも変数のスコープがかかわってきます。
tryの構文中の変数はtryブロック外部には影響を及ぼしません。(tryブロックの中のローカルスコープ)
なので下記はエラーになります。

iex> try do
...>   raise "fail"
...>   what_happened = :did_not_raise
...> rescue
...>   _ -> what_happened = :rescued
...> end
iex> what_happened
** (RuntimeError) undefined function: what_happened/0

スコープの内外はどの言語でも気にすると思うのであまり気にしなくてよさそう。
一応上記の意図を汲んで実装するなら下記のようになるそうです。

iex> what_happened =
...>   try do
...>     raise "fail"
...>     :did_not_raise
...>   rescue
...>     _ -> :rescued
...>   end
iex> what_happened
:rescued

どのみちrescueなどがあまり推奨されてないことを踏まえるとあまり日の目は見ないかもしれません。