error チュートリアル

このチュートリアルでは、検査付きの線形代数呼び出しの失敗を扱う方法を示します。何がうまくいかなかったのかを識別し、意味のある場合は回復し、複数の検査付きステップを連鎖させ、自作のヘルパーからの失敗も同じ語彙で報告します。エラー型の背後にある考え方は エラーの設計 にあります。

クイックスタート

moon add Luna-Flow/linear-algebra@0.5.0
///|
import {
  "Luna-Flow/linear-algebra/error" @la_error,
  "Luna-Flow/linear-algebra/mutable",
}

検査付きの呼び出しは Result[_, LinearAlgebraError] を返します。結果で分岐し、述語を使って失敗を分類します。

///|
test "classify a failed inverse" {
  let m = @mutable.Matrix::from_2d_array([[1.0, 2.0], [2.0, 4.0]])
  let status = match m.inverse() {
    Ok(_) => "ok"
    Err(e) => if e.is_singular_matrix() { "singular" } else { e.message }
  }
  inspect(status, content="singular")
}

出力は singular です。

日常的なタスク

失敗ごとに異なる回復をする

///|
fn err_tut_inverse_report(m : @mutable.Matrix[Double]) -> String {
  match m.inverse() {
    Ok(inv) => "inverse has \{inv.row()} rows"
    Err(e) =>
      if e.is_non_square_matrix() {
        "not square: use a least-squares method instead"
      } else if e.is_singular_matrix() {
        "singular: regularize the matrix"
      } else {
        e.message
      }
  }
}

///|
test "different recovery paths" {
  let rect = @mutable.Matrix::from_2d_array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
  inspect(
    err_tut_inverse_report(rect),
    content="not square: use a least-squares method instead",
  )
  let ok = @mutable.Matrix::from_2d_array([[2.0, 0.0], [0.0, 4.0]])
  inspect(err_tut_inverse_report(ok), content="inverse has 2 rows")
}

検査付きステップを連鎖させる

両方のステップが成功したときだけ tr⁡(Ak)\operatorname{tr}(A^{k}) を計算します。早期の return Err(e) は、最初の失敗をそのまま受け渡します。

///|
fn err_tut_trace_of_power(
  a : @immut.Matrix[Int],
  k : Int,
) -> Result[Int, @la_error.LinearAlgebraError] {
  let p = match a.pow(k) {
    Ok(p) => p
    Err(e) => return Err(e)
  }
  p.trace()
}

///|
test "trace of a matrix power" {
  let a = @immut.Matrix::from_2d_array([[1, 1], [1, 0]])
  inspect(err_tut_trace_of_power(a, 5).unwrap(), content="11")
  match err_tut_trace_of_power(a, -1) {
    Err(e) => inspect(e.is_negative_exponent(), content="true")
    Ok(_) => fail("negative powers are not defined")
  }
}

Fibonacci 行列の A5A^5 は (8553)\begin{pmatrix} 8 & 5 \\ 5 & 3 \end{pmatrix} なので、トレースは 1111 です。

自作のヘルパーから失敗を報告する

コンストラクタを使って、共通の種類のエラーを返します。

///|
fn err_tut_column_sum(
  m : @mutable.Matrix[Int],
  col : Int,
) -> Result[Int, @la_error.LinearAlgebraError] {
  guard col >= 0 && col < m.col() else {
    return Err(
      @la_error.LinearAlgebraError::index_out_of_bounds(
        "column \{col} is outside 0..<\{m.col()}",
      ),
    )
  }
  let mut sum = 0
  m.each_col(col, x => sum = sum + x)
  Ok(sum)
}

///|
test "a custom checked helper" {
  let m = @mutable.Matrix::from_2d_array([[1, 2], [3, 4]])
  inspect(err_tut_column_sum(m, 1).unwrap(), content="6")
  match err_tut_column_sum(m, 5) {
    Err(e) => {
      inspect(e.is_index_out_of_bounds(), content="true")
      inspect(e.message, content="column 5 is outside 0..<2")
    }
    Ok(_) => fail("column 5 does not exist")
  }
}

スカラーの失敗をラップする

ヘルパーがスカラーの検査付き演算と行列演算を混在させる場合は、ArithmeticError をラップして、ヘルパーのエラー型を 1 つにまとめます。

///|
fn err_tut_rms(
  m : @mutable.Matrix[Double],
) -> Result[Double, @la_error.LinearAlgebraError] {
  let n = (m.row() * m.col()).to_double()
  let mut sum = 0.0
  m.each(x => sum = sum + x * x)
  let ctx = @lf_arith.ArithmeticContext::new(53)
  match @la_arithmetic.CheckedDiv::checked_div(sum, n, ctx) {
    Ok(mean_sq) => Ok(mean_sq.sqrt())
    Err(e) => Err(@la_error.LinearAlgebraError::arithmetic_failure(e))
  }
}

///|
test "wrapping a scalar error" {
  let m = @mutable.Matrix::from_2d_array([[3.0, 4.0]])
  inspect(err_tut_rms(m).unwrap(), content="3.5355339059327378")
  let empty : @mutable.Matrix[Double] = @mutable.Matrix::new(0, 0, 0.0)
  match err_tut_rms(empty) {
    Err(e) => inspect(e.is_arithmetic_failure(), content="true")
    Ok(_) => fail("0 / 0 must fail")
  }
}

さらに進んで

検査なし形式。 すべての検査付きメソッドには、Err を返す代わりに中断する unchecked_* 版が対としてあります(unchecked_inverse は Option を返します)。これを使うのは、コードが前提条件を確立した後だけにしてください。たとえば、正方として構築した行列に対する内側のループなどです。x が定義域にあるときは常に checked(x) == Ok(unchecked(x)) という法則が成り立ちます。

今も中断するメソッド。 本リリースでは、すべての部分演算に検査付き形式があるわけではありません。from_2d_array のようなコンストラクタは行の長さが揃っていない入力で、演算子 +、-、* は形状の不一致で、eigen は非対称な入力で中断します。immut と mutable の API ページにはそれぞれ明記してあります。入力がプログラムの外から来る場合は、先に検証してください。

container 層からのエラー。 container の辞書とアルゴリズムは同じエラー型を返すので、ストレージを変換してから計算するパイプラインでも、エラー型の間の変換は不要です。

よくある落とし穴

  • message でマッチする。 メッセージは診断用であり、リリース間で変わる可能性があります。述語か kind で分岐してください。
  • エラーを直接出力する。 LinearAlgebraError には Show も Debug も実装されていません。e.message を出力してください。
  • エラー全体を比較する。 == はメッセージも比較します。分類だけが重要な場合は a.kind == b.kind を比較してください。
  • 予約された種類が発生すると想定する。 NonConvergence、RaggedRows、InvalidLength は本リリースでは生成されません。

次のステップ