エラーハンドリング#

エラーハンドリングは、私たちの言語設計の中核です。以下では、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
}

エラー多相性#

これは、高階関数が別の関数を引数として受け取るときに起こります。引数として渡される関数がエラーを送出する場合もしない場合もあり、それによってこの関数自身の振る舞いが変わります。

代表的な例は Arraymap です:

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#

trycatch を使ってエラーを捕捉し、処理できます。例えば:

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
}