プログラマーはふつう、完璧なソフトウェアを書こうとします。そして、たいていは失敗します。
物事は思いがけないときにうまくいかなくなるもので、それに対処できる必要があります。
言語の設計者の中には、できるだけ早くエラーを検出し、デバッグの助けになる情報を含むメッセージとともに実行を終了させることを優先すべきだと考える人もいます。
データサイエンス向けの言語は、もっと細やかなアプローチを取る傾向があります。即座に終了させなければならないほど深刻なエラーもありますが、多くの場合、問題を後で対処すべきものとして記録しておき、実行を続けたほうがよいのです。
Nothingnessコンセプトで見たように、Juliaには問題のある値を表すさまざまなプレースホルダーが用意されています。missing、NaN、Infです。こうしたものが、特定の状況でプログラムを終了させるよりよいアプローチかどうかは、プログラマーが判断することです。
_用語についての注意_を先に述べておきます。細かな話に入る前に。Juliaのドキュメントでは、「error」と「exception」という言葉をほぼ同じ意味で扱っています。以下の内容も同じくらい一貫していないかもしれません。
シラバスのここまで進めば、Juliaからたくさんのエラーメッセージを見てきたはずです。たとえば、次のようなものです。
julia> Int(3.14)
ERROR: InexactError: Int64(3.14)
小数を整数にキャストしようとすると精度が失われるので、InexactErrorが発生します。
InexactErrorは型のひとつで、Juliaに標準で組み込まれているいくつかの型(現在は25種類)のうちの1つです。これらはすべてExceptionのサブタイプです。
julia> supertype(InexactError)
Exception
throw()標準のエラー型の中には、自分のコードで発生させると役に立つものもあります。
すべての具象型と同じように、エラーにもコンストラクターがあります。引数はさまざまなので、使いたいものについてはドキュメントを確認してください。
julia> DomainError(42, "out of range")
DomainError(42, "out of range")
エラーを使うには、コンストラクターをthrow()関数で包みます。
julia> throw(DomainError(42, "out of range"))
ERROR: DomainError with 42:
out of range
error()手っ取り早く済ませたいときは、error()関数が便利です。引数として文字列(または文字列を構成する部品)を取ります。
julia> happy = false;
julia> happy || error("😞 something went wrong")
ERROR: 😞 something went wrong
新しいエラー型を作るのは、原理的にはとても簡単です。Exceptionのサブタイプをもう1つ追加するだけです。
julia> struct MyError <: Exception end
julia> throw(MyError)
ERROR: MyError
アサーションの基本的な考え方は、「この文は真であるはずなので、偽なら大声で警告する」というものです。これが役に立つのは主にデバッグ中で、本番のコードではアサーションが失敗してはいけません。
Typesコンセプトで見たように、型アサーションを追加できます。たとえば、関数の戻り値を確認するのに使います。
julia> 42::Number
42
julia> "two"::Number
ERROR: TypeError: in typeassert, expected Number, got a value of type String
より一般的には、@assertマクロを使うと、真偽値に評価される任意の式を検査できます。
julia> n = 22;
julia> @assert isodd(n) "n must be odd"
ERROR: AssertionError: n must be odd
try...catch
どうしても致命的になるエラーもありますが、多くの場合、プログラムがうまく復帰してくれることを期待します。
デフォルトでは、エラーが起きると現在の関数は即座に終了し、そのエラー(情報を含むメッセージがあればそれも)は呼び出し元の関数に渡されます。
これはコールスタックをさかのぼって続き、最終的にトップレベルのコードがエラーメッセージとともに終了します。
どの段階でも、エラーを処理しようとするtry...catchブロックでそのエラーを捕まえることができます。
julia> n = -1;
julia> try
log_n = log(n)
catch problem
if problem isa DomainError # number out of range
# See next section for more on @warn and @info
@warn "you may have supplied a negative real number: $n"
@info "trying with complex argument"
log_n = log(Complex(n)) # fallback calculation
elseif problem isa MethodError # no idea what n is
@error "please supply a valid argument"
else
rethrow() # the error could be anything else
end
end
┌ Warning: you may have supplied a negative real number: -1
└ @ Main REPL[3]:5
[ Info: trying with complex argument
0.0 + 3.141592653589793im # success
上の例では、log(n)はnが正の実数値か、任意の複素数値であることを必要とします。try ... catchは負の実数値による問題を捕まえ、数学的表記でいう正しい複素数解iπを返します。
たとえば文字列を引数として渡した場合、ユーザーに修正してもらう以外に復帰する方法はありません。
最後の受け皿として、DomainErrorでもMethodErrorでもないものすべてに対してrethrow()を追加しました。
注意: try...catchが必要なこともありますが、使いすぎは避けてください。代わりにif...elseブロックを使えるなら、例外を捕まえるよりずっと性能がよくなります。
上で説明したerror()関数と、@errorマクロを混同しないようにしましょう。
関数のほうは例外を発生させ、捕まえられない限りコールスタックをさかのぼって渡されていきます。
@errorマクロは、その仲間である@debug、@info、@warnとともにLoggingモジュールの一部で、プログラムの流れを変えずに情報を伝えるメッセージを生成するためのものです。
出力はデフォルトでターミナルに送られます(重要度に応じて色分けされます)が、実際のアプリケーションではほかにも多くの選択肢があります。
julia> @warn "Something looks not quite right"
┌ Warning: Something looks not quite right
└ @ Main REPL[55]:1
julia> @error "Panic!"
┌ Error: Panic!
└ @ Main REPL[56]:1
try...catchのところにある前の例も参照してください。
エレナは新聞工場の新しい品質管理責任者です。 ちょうど入社したばかりなので、工場のいくつかの工程を見直して、改善できるところがないか確かめることにしました。 調べてみると、技術者たちが多くの品質チェックを手作業で行っていることがわかります。自動化の好機だと感じたエレナは、フリーランスのエンジニアであるあなたに、いくつかの機械を監視するソフトウェアの開発を頼みます。
最初のミッションは、生産室の湿度レベルを監視するソフトウェアを書くことです。すでにセンサーが会社のソフトウェアに接続されていて、部屋の湿度のパーセントを定期的に返してくれます。
湿度が高すぎる場合にエラーを投げる関数を、ソフトウェアに実装する必要があります。
湿度が許容範囲内であれば、Infoログが追加されます。
関数の名前はhumiditycheckとし、湿度のパーセントを引数として受け取ります。
パーセントが70%を超える場合は、ErrorExceptionで停止してください(メッセージの内容そのものは重要ではありませんが、測定された湿度を含める必要があります)。
そうでなければ、メッセージ"humidity level check passed: h%"のInfoログを追加してください。ここでhは湿度のパーセントです。
julia> humiditycheck(60)
[ Info: humidity level check passed: 60%
julia> humiditycheck(100)
ERROR: humidity check failed: 100%
エレナは最初の仕事にとても満足していて、次は機械の温度の監視を任せたいと考えています。 技術者のグレッグと雑談していると、機械の温度が500°Cを超えると技術者たちは過熱を心配し始める、と教えてくれます。
機械には、内部温度を測るセンサーが付いています。 このセンサーはとても繊細で、よく壊れてしまうことを知っておいてください。 壊れた場合は、技術者が交換する必要があります。
あなたの仕事は、温度を引数に取る関数temperaturecheckを実装することです。この関数は、問題がなければログを追加し、センサーが壊れている場合や機械が過熱し始めた場合にはエラーを投げます。
あとでエラーの種類に応じて違う対応をする必要があるので、2種類のエラーを区別する仕組みが要ります。
nothingになります。
この場合は、ArgumentErrorで停止してください(メッセージは重要ではありません)。DomainErrorを投げてください。"temperature check passed: t °C"のInfoログを追加してください。ここでtは温度です。julia> temperaturecheck(nothing)
ERROR: ArgumentError: sensor is broken
julia> temperaturecheck(800)
ERROR: DomainError with 800:
"overheating detected"
julia> temperaturecheck(500)
[ Info: temperature check passed: 500 °C
次のタスクでは、より一般的な、どんなエラーも受け止めるエラーを定義する必要があります。
エラーであることと、名前がMachineErrorであること以外に、実装の詳細は重要ではありません。
フィールドやメッセージは、役に立つと思うものを自由に含めてかまいません。
機械がエラーを検知できるようになり、独自のマシンエラーも定義できたので、全体がどう動いているかを報告するラッパー関数を追加します。 このラッパーは、これまでの関数からのログを返すだけでなく、発生した不具合の種類に応じてログを追加する必要もあります。
ErrorExceptionを投げた場合は、メッセージ"humidity level check failed: h%"のErrorログを追加します。ここでhは湿度のパーセントです。ArgumentErrorを投げた場合は、メッセージ"sensor is broken"のWarnログを追加します。DomainErrorを投げた場合は、メッセージ"overheating detected: t °C"のErrorログを追加します。ここでtは温度です。MachineErrorを1つ投げます。humiditycheckとtemperaturecheckからのログだけが追加されます。湿度と温度を引数に取る関数machinemonitor()を実装してください。
julia> machinemonitor(42, 450)
[ Info: humidity level check passed: 42%
[ Info: temperature check passed: 450 °C
julia> machinemonitor(42, 550)
[ Info: humidity level check passed: 42%
┌ Error: overheating detected: 550 °C
└ @ Main # output truncated
Error: MachineError
julia> machinemonitor(82, 521)
┌ Error: humidity level check failed: 82%
└ @ Main # output truncated
┌ Error: overheating detected: 521 °C
└ @ Main # output truncated
Error: MachineError
julia> machinemonitor(42, nothing)
[ Info: humidity level check passed: 42%
┌ Warning: sensor is broken
└ @ Main # output truncated
Error: MachineError