bin_float_checked 教程

本教程展示如何编写二进制浮点计算,使每个可能失败的步骤都得到检查,而无需在每一步之后解包 Result:你把输入包装进 BinFloatResult,串联运算和运算符,用 bind 加入自己的验证,并在最后一次性检查第一个错误。该包装器把所有算术委托给 bin_float。形式化模型见设计页面;所有方法都列在 API 参考中。

快速入门

moon add Luna-Flow/floating@0.8.0
import {
  "Luna-Flow/floating/bin_float",
  "Luna-Flow/floating/bin_float_checked",
}

以 48 位精度计算 81/3\sqrt{81} / 3 并读取结果:

///|
test "quick start: a checked pipeline" {
  let value = @bin_float_checked.BinFloatResult::from_int(81, precision=48)
    .sqrt()
    .div(@bin_float_checked.BinFloatResult::from_int(3, precision=48))
  match value.result() {
    Ok(x) => inspect(x.to_string(), content="3p0")
    Err(e) => fail(e.message)
  }
}

to_string 打印精确的二进制值:3p0 即 3⋅203 \cdot 2^0。

日常任务

其余示例用下面这个辅助函数打印结果:

///|
fn show(r : @bin_float_checked.BinFloatResult) -> String {
  match r.result() {
    Ok(v) => v.to_string()
    Err(e) => "error: " + e.message
  }
}

用运算符编写公式

+、-、*、/ 和一元 - 都可作用于包装器。第一个失败的步骤决定结果;出错之后,后续步骤不再执行:

///|
fn harmonic_mean(
  a : @bin_float_checked.BinFloatResult,
  b : @bin_float_checked.BinFloatResult,
) -> @bin_float_checked.BinFloatResult {
  let one = @bin_float_checked.BinFloatResult::from_int(1)
  let two = @bin_float_checked.BinFloatResult::from_int(2)
  two / (one / a + one / b)
}

///|
test "harmonic mean with a checked division" {
  let r = fn(n : Int) { @bin_float_checked.BinFloatResult::from_int(n) }
  inspect(show(harmonic_mean(r(2), r(6))), content="3p0")
  inspect(show(harmonic_mean(r(2), r(0))), content="error: division by zero")
}

对普通的 BinFloat 值,第二次调用会悄无声息地产生一个数(1/0=∞1/0 = \infty,2/∞=02/\infty = 0);而包装器会报告计算是在何处离开实数的。

用 bind 加入自定义检查

map 应用一个不会失败的函数;bind 应用一个返回 BinFloatResult、可能失败的函数。用 bind 把应用规则放进流水线:

///|
fn require_probability(x : @bin_float.BinFloat) -> @bin_float_checked.BinFloatResult {
  let in_range = x.compare(@bin_float.BinFloat::zero()) >= 0 &&
    x.compare(@bin_float.BinFloat::from_int(1)) <= 0
  if in_range {
    @bin_float_checked.BinFloatResult::ok(x)
  } else {
    @bin_float_checked.BinFloatResult::err(
      @lf_arith.ArithmeticError::domain_error("not a probability"),
    )
  }
}

///|
test "entropy term with a domain check" {
  let term = fn(p : Double) {
    let x = @bin_float_checked.BinFloatResult::from_double(p).bind(require_probability)
    -(x * x.log2())
  }
  inspect(show(term(0.5)), content="1p-1")
  inspect(show(term(1.5)), content="error: not a probability")
}

用上下文控制精度与舍入

每个初等函数都有一个接收 BinaryContext 的 _ctx 形式。普通形式在操作数自身的精度下就近舍入;上下文形式则按上下文舍入:

///|
test "the same logarithm at two precisions" {
  let two = @bin_float_checked.BinFloatResult::from_int(2)
  inspect(show(two.ln()), content="6243314768165359p-53")
  let single = @bin_float.BinaryContext::binary32()
  inspect(show(two.ln_ctx(single)), content="1453635p-21")
  let down = @bin_float.BinaryContext::unbounded(
    24,
    rounding=@bin_float.BinaryRoundingMode::RoundTowardNegative,
  )
  inspect(show(two.ln_ctx(down)), content="11629079p-24")
}

区分定义域错误与认证失败

错误是来自 Luna-Flow/arithmetic 的 ArithmeticError 值。其种类说明是输入超出了函数的定义域,还是库无法认证一个正确舍入的结果:

///|
fn classify_error(r : @bin_float_checked.BinFloatResult) -> String {
  match r.result() {
    Ok(_) => "ok"
    Err(e) if e.is_domain_error() => "domain"
    Err(e) if e.is_certification_failure() => "certification"
    Err(e) if e.is_division_by_zero() => "division by zero"
    Err(_) => "other"
  }
}

///|
test "error kinds" {
  let r = fn(n : Int) { @bin_float_checked.BinFloatResult::from_int(n) }
  inspect(classify_error(r(-1).sqrt()), content="domain")
  inspect(classify_error(r(3).exp_ln()), content="certification")
  inspect(classify_error(r(1) / r(0)), content="division by zero")
  inspect(classify_error(r(2).sqrt()), content="ok")
}

exp_ln(融合的 ln⁡(exp⁡x)\ln(\exp x))只在 ∣x∣≤1/8|x| \le 1/8 时可认证,因此对 33 会失败;这是认证上的限制,而不是数学上的定义域。

深入了解

基于 Luna-Flow/arithmetic trait 的泛型代码

BinFloat 实现了 Luna-Flow/arithmetic 的 checked trait(SqrtChecked、DivChecked、PowIntChecked、PowNatChecked)。针对这些 trait 编写的函数返回 Result[T, ArithmeticError],from_result 可把它接入流水线:

///|
fn[T : @lf_arith.SqrtChecked + @lf_arith.DivChecked] ratio_root(
  a : T,
  b : T,
  ctx : @lf_arith.ArithmeticContext,
) -> Result[T, @lf_arith.ArithmeticError] {
  match @lf_arith.DivChecked::div_checked(a, b, ctx) {
    Ok(q) => @lf_arith.SqrtChecked::sqrt_checked(q, ctx)
    Err(e) => Err(e)
  }
}

///|
test "a generic checked function feeds the pipeline" {
  let ctx = @lf_arith.ArithmeticContext::new(53)
  let r = @bin_float_checked.BinFloatResult::from_result(
    ratio_root(@bin_float.BinFloat::from_int(18), @bin_float.BinFloat::from_int(2), ctx),
  )
  inspect(show(r + @bin_float_checked.BinFloatResult::from_int(1)), content="1p2")
  let bad = @bin_float_checked.BinFloatResult::from_result(
    ratio_root(@bin_float.BinFloat::from_int(1), @bin_float.BinFloat::zero(), ctx),
  )
  inspect(show(bad), content="error: division by zero")
}

在需要时保留 IEEE 标志

包装器只存储一个值或一个错误,别无其他。如果不精确、上溢或下溢标志是你契约的一部分,请调用返回 (value, BinaryFlags) 的 BinFloat::*_ctx 运算,并自行合并标志:

///|
test "carry flags next to the wrapper" {
  let ctx = @bin_float.BinaryContext::binary64()
  let (third, f1) = @bin_float.BinFloat::from_int(1).div_ctx(
    @bin_float.BinFloat::from_int(3),
    ctx,
  )
  let (sum, f2) = third.add_ctx(third, ctx)
  let flags = f1.combine(f2)
  inspect(flags.inexact(), content="true")
  inspect(show(@bin_float_checked.BinFloatResult::ok(sum)), content="6004799503160661p-53")
}

常见陷阱

  • NaN 算成功。 BinFloatResult 只报告 bin_float 作为错误报告的情形。IEEE 定义为 NaN 或无穷结果的运算(∞−∞\infty - \infty、对负数的 sqrt_ctx、除以零的 div_ctx)都是成功的值。如有需要,请对最终值检验 @def.is_nan。
  • div 与 div_ctx 不同。 div(以及 /)在除数为零时失败;div_ctx 返回无穷。pow_int 与 pow_int_ctx、sqrt 与 sqrt_ctx 之间也是如此。
  • 只有第一个错误会保留下来。 在 a + b 中,若两个操作数都失败,报告的是 a 的错误。错误不会被收集。
  • _ctx 方法会丢弃标志。 需要标志时请直接使用 bin_float。
  • 混合操作数的精度。 二元运算采用较大的操作数精度;from_float 默认 24 位,因此把它与 from_double 值混用会得到 53 位的结果。
  • flat_map 已弃用。 请使用 bind。

后续步骤