arithmetic 教程

本教程演示如何在你自己的泛型辅助函数中使用 arithmetic 的标量运算 trait:绝对值、近似比较,以及把非法输入转为可处理的值的受检除法、平方根与比较。背景知识见 arithmetic 设计。

快速上手

moon add Luna-Flow/linear-algebra@0.5.0
moon add Luna-Flow/arithmetic@0.2.2
///|
import {
  "Luna-Flow/linear-algebra/arithmetic" @la_arithmetic,
  "Luna-Flow/arithmetic" @lf_arith,
}

一个泛型的“最大模”辅助函数需要 Abs 和 Compare:

///|
fn[T : @la_arithmetic.Abs + Compare] arith_tut_max_abs(xs : Array[T]) -> T? {
  let mut best : T? = None
  for x in xs {
    let a = @la_arithmetic.Abs::abs(x)
    best = match best {
      Some(b) if b >= a => Some(b)
      _ => Some(a)
    }
  }
  best
}

///|
test "largest magnitude" {
  debug_inspect(arith_tut_max_abs([3, -7, 5]), content="Some(7)")
  debug_inspect(arith_tut_max_abs([0.5, -0.25]), content="Some(0.5)")
}

日常任务

归一化向量时避免除以零

CheckedDiv 把零除数报告为错误,而不是产生无穷大:

///|
fn arith_tut_normalize(
  xs : Array[Double],
  ctx : @lf_arith.ArithmeticContext,
) -> Result[Array[Double], @lf_arith.ArithmeticError] {
  let mut sum = 0.0
  for x in xs {
    sum = sum + x.abs()
  }
  let out = []
  for x in xs {
    match @la_arithmetic.CheckedDiv::checked_div(x, sum, ctx) {
      Ok(v) => out.push(v)
      Err(e) => return Err(e)
    }
  }
  Ok(out)
}

///|
test "normalize by the 1-norm" {
  let ctx = @lf_arith.ArithmeticContext::new(53)
  debug_inspect(
    arith_tut_normalize([1.0, 3.0], ctx).unwrap(),
    content="[0.25, 0.75]",
  )
  inspect(arith_tut_normalize([0.0, 0.0], ctx) is Err(_), content="true")
}

对全零输入,第一次除法是 0/00/0,会以 kind DomainError 报告。

只在定义域内开平方

///|
fn arith_tut_std_from_variance(
  variance : Double,
) -> Result[Double, @lf_arith.ArithmeticError] {
  @la_arithmetic.CheckedSqrt::checked_sqrt(
    variance,
    @lf_arith.ArithmeticContext::new(53),
  )
}

///|
test "square root of a variance" {
  inspect(arith_tut_std_from_variance(2.25).unwrap(), content="1.5")
  match arith_tut_std_from_variance(-1.0e-18) {
    Err(e) => inspect(e.is_domain_error(), content="true")
    Ok(_) => fail("negative variance must be rejected")
  }
}

略为负的方差是单遍公式中典型的舍入产物;受检调用让你自己决定如何处理它。

对可能含 NaN 的数据排序

checked_compare 在遇到 NaN 时失败,因此可以在排序之前检测出无序数据:

///|
fn arith_tut_all_ordered(xs : Array[Double]) -> Bool {
  for x in xs {
    if @la_arithmetic.CheckedCompare::checked_compare(x, x) is Err(_) {
      return false
    }
  }
  true
}

///|
test "detect NaN before sorting" {
  inspect(arith_tut_all_ordered([2.0, 1.0]), content="true")
  inspect(arith_tut_all_ordered([2.0, 0.0 / 0.0]), content="false")
}

近似比较结果

ApproxEq 适用于量级为一的值,例如归一化向量的元素:

///|
fn[T : @la_arithmetic.ApproxEq] arith_tut_all_close(
  xs : Array[T],
  ys : Array[T],
) -> Bool {
  if xs.length() != ys.length() {
    return false
  }
  for i in 0..<xs.length() {
    if !@la_arithmetic.ApproxEq::approx_eq(xs[i], ys[i]) {
      return false
    }
  }
  true
}

///|
test "approximate comparison of computed values" {
  inspect(arith_tut_all_close([0.1 + 0.2, 1.0], [0.3, 1.0]), content="true")
  inspect(0.1 + 0.2 == 0.3, content="false")
}

进一步了解

你自己的标量类型。 所有 trait 都是 pub(open)。十进制或有理数类型可以实现 Abs、ApproxEq(对精确类型直接用 == 即可)以及受检 trait;之后它就能配合所有针对这些 trait 编写的辅助函数使用。

矩阵代码中的错误。 矩阵包用 LinearAlgebraError::arithmetic_failure 报告标量失败,它包装一个 ArithmeticError。混合了矩阵与标量步骤的辅助函数可以用该构造函数进行转换;参见错误教程。

上游 trait。 对超越函数和带上下文的算术,请直接使用 Luna-Flow/arithmetic;本包只重新导出 API 页面上列出的名称。

常见陷阱

  • 绝对容差用错了量级。 对 102010^{20} 附近的值,approx_eq 实际上就是严格相等;对 10−2010^{-20} 附近的值,它接受一切。请对数据做缩放,或使用自己的相对规则。
  • 串联近似比较。 a≈ba \approx b 与 b≈cb \approx c 并不蕴含 a≈ca \approx c。
  • 期望上下文改变 Double 结果。 对二进制浮点数,ArithmeticContext 的精度会被忽略。
  • 对 Int.MIN_VALUE 使用 Abs。 结果会回绕,仍为负数。

后续步骤