エラーハンドリング#
エラーハンドリングは、私たちの言語設計の中核です。以下では、MoonBit でのエラーハンドリングの方法を説明します。MoonBit の基本的な知識があることを前提としています。まだの場合は A tour of MoonBit を参照してください。
エラー型#
MoonBit では、すべてのエラー値を汎用エラー型である Error 型で表現できます。
ただし、Error は直接構築できません。次の形式で具体的なエラー型を定義する必要があります:
suberror E1 { E1(Int) } // error type E1 has one constructor E1 with an Int payload
suberror E2 // error type E2 has one constructor E2 with no payload
suberror E3 { // error type E3 has three constructors like a normal enum type
A
B(Int, x~ : String)
C(mut x~ : String, Char, y~ : Bool)
}
警告
古い suberror A B 構文は非推奨です。代わりに suberror A { A(B) } を使ってください。
これらのエラー型は自動的に Error 型へ昇格でき、パターンマッチで取り出せます:
suberror CustomError { CustomError(UInt) }
test {
let e : Error = CustomError(42)
guard e is CustomError(m)
assert_eq(m, 42)
}
Error 型には複数のエラー型を含められるため、Error 型に対するパターンマッチでは、すべてのエラー型に対応するためにワイルドカード _ を使う必要があります。例えば:
fn f(e : Error) -> Unit {
match e {
E2 => println("E2")
A => println("A")
B(i, x~) => println("B(\{i}, \{x})")
_ => println("unknown error")
}
}
Error は、具体的なエラー型が不要な場面や、あらゆる種類のサブエラーを一括で扱いたい場面で使うことを想定しています。
Failure#
組み込みエラー型として Failure があります。
便利な fail 関数もあります。これはエラーとソース位置の両方を表示するための定義済み出力テンプレートを持つ、実質的にはコンストラクタです。実際には Failure より fail を常に優先して使います。
#callsite(autofill(loc))
pub fn[T] fail(msg : String, loc~ : SourceLoc) -> T raise Failure {
raise Failure("FAILED: \{loc} \{msg}")
}
エラーの送出#
raise キーワードは、関数の実行を中断してエラーを返すために使います。
関数の型宣言では、raise とエラー型を組み合わせて、その関数が実行中にエラーを送出する可能性があることを示せます。例えば次の div 関数は DivError 型のエラーを返す可能性があります:
suberror DivError { DivError(String) } derive(Debug)
fn div(x : Int, y : Int) -> Int raise DivError {
if y == 0 {
raise DivError("division by zero")
}
x / y
}
具体的なエラー型が重要でない場合は Error を使えます。便宜上、raise の後のエラー型は省略でき、その場合は Error 型を使うことを意味します。例えば次の関数シグネチャは等価です:
fn f() -> Unit raise {
...
}
fn g() -> Unit raise Error {
let h : () -> Unit raise = fn() raise { fail("fail") }
...
}
エラー型に対してジェネリックな関数では、Error 制約を使えます。例えば:
// Result::unwrap_or_error
fn[T, E : Error] unwrap_or_error(result : Result[T, E]) -> T raise E {
match result {
Ok(x) => x
Err(e) => raise e
}
}
エラーを送出しない関数には、シグネチャに noraise を追加できます。例えば:
fn add(a : Int, b : Int) -> Int noraise {
a + b
}
エラー多相性#
これは、高階関数が別の関数を引数として受け取るときに起こります。引数として渡される関数がエラーを送出する場合もしない場合もあり、それによってこの関数自身の振る舞いが変わります。
代表的な例は Array の map です:
fn[T] map(array : Array[T], f : (T) -> T raise) -> Array[T] raise {
let mut res = []
for x in array {
res.push(f(x))
}
res
}
ただしこのように書くと、map 関数が常にエラーを送出する可能性を持つことになり、実態に合いません。
そこでエラー多相性が導入されています。raise? を使うと、エラーを送出する場合もしない場合もあることを表せます。
fn[T] map_with_polymorphism(
array : Array[T],
f : (T) -> T raise?
) -> Array[T] raise? {
let mut res = []
for x in array {
res.push(f(x))
}
res
}
fn[T] map_without_error(
array : Array[T],
f : (T) -> T noraise,
) -> Array[T] noraise {
map_with_polymorphism(array, f)
}
fn[T] map_with_error(array : Array[T], f : (T) -> T raise) -> Array[T] raise {
map_with_polymorphism(array, f)
}
map_with_polymorphism のシグネチャは、実際に渡される引数によって決定されます。
エラー処理#
関数を通常どおり呼び出すと、エラーが発生した場合はそのまま再送出されます。例えば:
fn div_reraise(x : Int, y : Int) -> Int raise DivError {
div(x, y) // Rethrow the error if `div` raised an error
}
ただし、エラーをその場で処理したい場合もあります。
Try ... Catch#
try と catch を使ってエラーを捕捉し、処理できます。例えば:
fn main {
try div(42, 0) catch {
DivError(s) => println(s)
} noraise {
v => println(v)
}
}
division by zero
ここでは、try でエラーを送出する可能性がある関数を呼び出し、catch で捕捉したエラーをパターンマッチして処理しています。エラーが発生しなかった場合、catch ブロックは実行されず、代わりに noraise ブロックが実行されます。
エラーが発生しなかったときに何もしないなら、noraise ブロックは省略できます。例えば:
println(div(42, 0)) catch {
_ => println("Error")
}
try の本体が単純な式である場合は、中括弧、さらには try キーワード自体も省略できます。例えば:
let a = div(42, 0) catch { _ => 0 }
println(a)
Result への変換#
潜在的なエラーを捕捉し、Result 型の first-class な値へ変換することもできます:
test {
let res : Result[Int, DivError] = Ok(div(6, 0) * div(6, 3)) catch {
error => Err(error)
}
match res {
Err(DivError(message)) => @test.assert_eq(message, "division by zero")
Ok(_) => fail("expected division to fail")
}
}
エラー時にパニックする#
予期しないエラーが起きたときに直接パニックさせることもできます:
fn remainder(a : Int, b : Int) -> Int raise DivError {
if b == 0 {
raise DivError("division by zero")
}
let div = try! div(a, b)
a - b * div
}
エラー推論#
try ブロック内では、複数種類のエラーが送出されることがあります。その場合、コンパイラは共通エラー型として Error 型を使います。したがってハンドラ側では、すべてのエラーを捕捉するためにワイルドカード _ を使い、e => raise e で他のエラーを再送出する必要があります。例えば:
fn f1() -> Unit raise E1 {
...
}
fn f2() -> Unit raise E2 {
...
}
try {
f1()
f2()
} catch {
E1(_) => ...
E2 => ...
e => raise e
}