どうも、靖宗です。
流石にこの章は例外処理でしょう。
でも、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
例外の様なケースで値を取り出したい時にはthrow
とcatch
を使えるそうです。
下記は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
でリンクしてると、リンクしてるプロセス全部がおなくなりになるようですね。
上記の例は1
をexit
シグナルとして送信しているようですが、エラーの時どうするんだとかは追々って感じでしょうか。
この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
同様、rescue
やcatch
でも省略系が使えるそうですが、後ろの二つは使わん方がええよって言われてるのでafter
でよく使うことになると思います。
Else
try
ブロックで例外を想定してcatch
やrescue
で待ち受けたとしましょう。
ここで例外が起こらなかった場合にのみ実行したい処理があるときは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
などがあまり推奨されてないことを踏まえるとあまり日の目は見ないかもしれません。