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。

从自己的辅助函数中报告失败

使用构造函数返回共享 kind 的错误:

///|
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,使该辅助函数只有单一的错误类型:

///|
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")
  }
}

进一步了解

非受检形式。 每个受检方法都有一个 unchecked_* 对应版本,它中止而不是返回 Err(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。
  • 以为保留的 kind 会出现。 本版本不会产生 NonConvergence、RaggedRows 和 InvalidLength。

后续步骤