core 教程

本教程带你从安装 arithmetic 开始,到编写能准确声明自身需求的数值代码:基于分析能力的泛型函数、把无效输入变成可处理值的检查运算、报告结果如何被舍入的上下文运算,以及接入同一组 trait 的自定义类型。每个示例都是一个测试,可以粘贴到 _test.mbt 文件中并用 moon test 运行;预期输出写在 inspect 调用中。

快速开始

把本包添加到你的模块:

moon add Luna-Flow/arithmetic@0.5.0

在使用它的包的 moon.pkg 中导入它。Luna Flow 代码使用别名 @lf_arith:

import {
  "Luna-Flow/arithmetic" @lf_arith,
}

最小的有用程序只依赖一项能力,并在两种数值类型上使用它:

fn[T : Add + Mul + @lf_arith.Sqrt] hypot(x : T, y : T) -> T {
  @lf_arith.Sqrt::sqrt(x * x + y * y)
}

test "quick start" {
  inspect(hypot(3.0, 4.0), content="5")
  inspect(hypot((6.0 : Float), (8.0 : Float)), content="10")
}

hypot 适用于任何具有 +、* 和平方根的类型,且仅适用于这些类型。这正是整个包的理念:算法只声明它所用到的最小能力集合。

日常任务

泛型地调用初等函数

非检查 trait(Sqrt、Exponential、Logarithmic、Trigonometric 等)返回 Self,并把特殊情形交给类型自己处理。通过 trait 调用它们,即 @lf_arith.Trigonometric::sin(x),以保持代码的泛型性:

fn[T : Add + Mul + @lf_arith.Trigonometric] sin_plus_cos_squared(x : T) -> T {
  let s = @lf_arith.Trigonometric::sin(x)
  let c = @lf_arith.Trigonometric::cos(x)
  s * s + c * c
}

test "pythagorean identity" {
  let v = sin_plus_cos_squared(0.7)
  inspect((v - 1.0).abs() < 1.0e-15, content="true")
  let pi : Double = @lf_arith.Constants::pi()
  inspect(@lf_arith.Logarithmic::ln(@lf_arith.Exponential::exp(pi)) == pi, content="true")
}

常量没有参数,因此需要标注所需的类型,如上面的 pi。

把无效输入变成错误值

检查 trait 返回 Result[_, ArithmeticError]。当负数开平方或零除数是调用方必须处理的情形、而不是留待以后才发现的 NaN 时,就使用它。下面的二次方程求解器使用无抵消形式 q=−12(b+sign⁡(b)b2−4ac)q = -\tfrac12\bigl(b + \operatorname{sign}(b)\sqrt{b^2 - 4ac}\bigr)、x1=q/ax_1 = q/a、x2=c/qx_2 = c/q:

fn real_roots(
  a : Double,
  b : Double,
  c : Double,
) -> Result[(Double, Double), @lf_arith.ArithmeticError] {
  let ctx = @lf_arith.ArithmeticContext::new(53)
  let s = match @lf_arith.SqrtChecked::sqrt_checked(b * b - 4.0 * a * c, ctx) {
    Ok(s) => s
    Err(e) => return Err(e)
  }
  let q = if b >= 0.0 { -0.5 * (b + s) } else { -0.5 * (b - s) }
  let x1 = match @lf_arith.DivChecked::div_checked(q, a, ctx) {
    Ok(x) => x
    Err(e) => return Err(e)
  }
  @lf_arith.DivChecked::div_checked(c, q, ctx).map(x2 => (x1, x2))
}

test "real roots" {
  guard real_roots(1.0, -3.0, 2.0) is Ok((x1, x2)) else { fail("no roots") }
  inspect(x1, content="2")
  inspect(x2, content="1")
  guard real_roots(1.0, 0.0, 1.0) is Err(e) else { fail("expected an error") }
  inspect(e.is_domain_error(), content="true")
  guard real_roots(0.0, 2.0, -4.0) is Err(e) else { fail("expected an error") }
  inspect(e.is_division_by_zero(), content="true")
}

错误类别告诉调用方出了什么问题:没有实根是 DomainError,a=0a = 0 的退化方程是 DivisionByZero。

比较可能为 NaN 的值

只要涉及 NaN,Double 上的 < 就返回 false,因此排序或求最大值会悄无声息地出错。CompareChecked 把无序的情形变成错误:

fn checked_max(xs : Array[Double]) -> Result[Double, @lf_arith.ArithmeticError] {
  let mut best = xs[0]
  for x in xs {
    match @lf_arith.CompareChecked::compare_checked(x, best) {
      Ok(1) => best = x
      Ok(_) => ()
      Err(e) => return Err(e)
    }
  }
  Ok(best)
}

test "checked maximum" {
  inspect(checked_max([3.0, 7.5, -1.0]).unwrap(), content="7.5")
  let r = checked_max([3.0, 0.0 / 0.0, 7.5])
  inspect(r is Err(e) && e.is_unordered_comparison(), content="true")
}

跨多个步骤收集诊断

上下文运算返回 ArithmeticOutcome:即值加上计算过程中设置的诊断。要串联多个步骤,把值传递下去,并用 combine 合并诊断。下面这个小辅助函数可用于任何上下文运算:

fn[A, B] and_then(
  r : Result[@lf_arith.ArithmeticOutcome[A], @lf_arith.ArithmeticError],
  next : (A) -> Result[@lf_arith.ArithmeticOutcome[B], @lf_arith.ArithmeticError],
) -> Result[@lf_arith.ArithmeticOutcome[B], @lf_arith.ArithmeticError] {
  match r {
    Err(e) => Err(e)
    Ok(first) =>
      next(first.value).map(second => @lf_arith.ArithmeticOutcome::with_diagnostics(
        second.value,
        first.diagnostics.combine(second.diagnostics),
      ))
  }
}

test "diagnostics survive a chain" {
  let ctx = @lf_arith.ArithmeticContext::new(24)
  let embedded : Result[@lf_arith.ArithmeticOutcome[Float], _] = @lf_arith.IntegralContextual::from_int_contextual(
    16_777_217, ctx,
  )
  let root = and_then(embedded, x => @lf_arith.SqrtContextual::sqrt_contextual(x, ctx)).unwrap()
  inspect(root.value, content="4096")
  inspect(root.diagnostics.inexact, content="true")
}

224+12^{24} + 1 无法放入 Float,因此嵌入把它舍入为 2242^{24} 并设置 inexact。2242^{24} 的平方根是精确的,但合并后的诊断仍记得先前的舍入:标志只会累积。

遍历可表示数

AdjacentContextual 返回相邻的可表示值。一个数与其后继之间的间隔是一个末位单位(ulp),它反映了该格式在这一量级上的精细程度:

fn ulp(x : Double) -> Double {
  let ctx = @lf_arith.ArithmeticContext::new(53)
  @lf_arith.AdjacentContextual::next_plus_contextual(x, ctx).unwrap().value - x
}

test "ulp grows with magnitude" {
  inspect(ulp(1.0), content="2.220446049250313e-16")
  inspect(ulp(1024.0), content="2.2737367544323206e-13")
  inspect(ulp(9007199254740992.0), content="2")
}

在 2532^{53} 处,相邻的双精度数相距 22,因此超过它的整数不再全部可表示。

报告认证失败

基于证明的后端用 CertificationFailure 错误报告它无法认证某个结果。应把它与定义域错误分开处理:输入是有效的,更大的预算可能会成功。

fn describe(err : @lf_arith.ArithmeticError) -> String {
  match err.certification_failure_detail() {
    Some(d) =>
      "\{d.operation()}: gave up after \{d.refinements()} refinements at \{d.work_precision()} bits"
    None => err.message
  }
}

test "describe errors" {
  let detail = @lf_arith.CertificationFailureDetail::new(
    "sinh",
    @lf_arith.CertificationStage::TargetRounding,
    @lf_arith.CertificationFailureReason::RefinementBudgetExhausted,
    53,
    1024,
    5,
  )
  inspect(
    describe(@lf_arith.ArithmeticError::certification_failure(detail)),
    content="sinh: gave up after 5 refinements at 1024 bits",
  )
  inspect(
    describe(@lf_arith.ArithmeticError::domain_error("negative input")),
    content="negative input",
  )
}

进阶

为自定义类型实现上下文 trait

只有在能够遵守时才实现某项能力。下面的定点类型以百分之一为单位存储;两个值相乘得到万分之一,必须舍入回来。该实现遵循上下文的舍入模式,在诊断中报告舍入,并拒绝它不支持的模式,而不是忽略它们:

struct Cents(Int) derive(Eq, Debug)

impl @lf_arith.MulContextual for Cents with mul_contextual(x, y, ctx) {
  let raw = x.0 * y.0
  let q = raw / 100
  let r = raw % 100
  if r == 0 {
    return Ok(@lf_arith.ArithmeticOutcome::exact(Cents(q)))
  }
  let away = if raw < 0 { q - 1 } else { q + 1 }
  let rounded = match ctx.rounding {
    TowardZero => q
    AwayFromZero => away
    ToNearestEven => {
      let twice = r.abs() * 2
      if twice > 100 || (twice == 100 && q % 2 != 0) { away } else { q }
    }
    _ =>
      return Err(
        @lf_arith.ArithmeticError::unsupported("Cents rounds only toward or away from zero, or to nearest"),
      )
  }
  let flags = @lf_arith.ArithmeticDiagnostics::new(inexact=true, rounded=true)
  Ok(@lf_arith.ArithmeticOutcome::with_diagnostics(Cents(rounded), flags))
}

test "fixed-point multiplication" {
  let nearest = @lf_arith.ArithmeticContext::new(2)
  let exact = @lf_arith.MulContextual::mul_contextual(Cents(150), Cents(150), nearest).unwrap()
  debug_inspect(exact.value, content="Cents(225)")
  inspect(exact.diagnostics.inexact, content="false")
  let tie_down = @lf_arith.MulContextual::mul_contextual(Cents(5), Cents(10), nearest).unwrap()
  debug_inspect(tie_down.value, content="Cents(0)")
  let tie_up = @lf_arith.MulContextual::mul_contextual(Cents(15), Cents(10), nearest).unwrap()
  debug_inspect(tie_up.value, content="Cents(2)")
  inspect(tie_up.diagnostics.rounded, content="true")
  let floor = @lf_arith.ArithmeticContext::new(2, rounding=@lf_arith.RoundingMode::TowardNegative)
  inspect(@lf_arith.MulContextual::mul_contextual(Cents(5), Cents(10), floor) is Err(_), content="true")
}

0.05×0.10=0.0050.05 \times 0.10 = 0.005 和 0.15×0.10=0.0150.15 \times 0.10 = 0.015 都恰好处于中点;就近偶数舍入把它们分别变为 0.000.00 和 0.020.02,即末位为偶数的那个相邻值。

以三种结果比较包络

对于区间或球类型,“x<yx < y 吗?”有三种答案:对每个容许值都是,对每个容许值都不是,或者未知。两个关系 trait 就足以计算它:

struct Interval {
  lo : Double
  hi : Double
}

impl @lf_arith.DefinitelyLt for Interval with definitely_lt(x, y) { x.hi < y.lo }

impl @lf_arith.DefinitelyLe for Interval with definitely_le(x, y) { x.hi <= y.lo }

enum Truth {
  Yes
  No
  Unknown
} derive(Debug)

fn[X : @lf_arith.DefinitelyLt + @lf_arith.DefinitelyLe] less(x : X, y : X) -> Truth {
  if @lf_arith.DefinitelyLt::definitely_lt(x, y) {
    Yes
  } else if @lf_arith.DefinitelyLe::definitely_le(y, x) {
    No
  } else {
    Unknown
  }
}

test "three-valued comparison" {
  let x = Interval::{ lo: 1.0, hi: 2.0 }
  debug_inspect(less(x, Interval::{ lo: 3.0, hi: 4.0 }), content="Yes")
  debug_inspect(less(x, Interval::{ lo: 0.0, hi: 1.0 }), content="No")
  debug_inspect(less(x, Interval::{ lo: 1.5, hi: 2.5 }), content="Unknown")
}

Unknown 答案并不是失败:收窄包络(以更高精度计算)后再问一次。Yes 或 No 在包络缩小时绝不会改变;设计页证明了原因。区间后端和球后端实现同样的 trait,因此 less 无需修改即可用于它们。

与代数结构结合

arithmetic 不定义环或域;它们由 luna-generic 定义。当算法同时需要两者时,在约束中把它们组合起来。例如,求 a\sqrt{a} 的 Newton 迭代只需要域运算,并与 Sqrt 交叉校验,误差在一个 ulp 以内:

fn[T : @lf_alg.Field] newton_sqrt(a : T, x0 : T, steps : Int) -> T {
  let one : T = @lf_alg.One::one()
  let two = one + one
  let mut x = x0
  for _ in 0..<steps {
    x = (x + a / x) / two
  }
  x
}

test "newton agrees with sqrt" {
  let a = 2.0
  let diff = newton_sqrt(a, 1.0, 6) - @lf_arith.Sqrt::sqrt(a)
  inspect(diff.abs() <= 2.220446049250313e-16, content="true")
}

为此,在 @lf_arith 旁边导入 "Luna-Flow/luna-generic" @lf_alg。

按性能选择层级

非检查 trait 编译为一次直接调用,没有内存分配。检查 trait 把结果包装在 Result 中,上下文 trait 还要构造 ArithmeticOutcome。在针对原生标量的内层循环中,先用一次检查调用验证输入,再在循环内使用非检查 trait。泛型代码会按类型特化,因此 trait 约束在运行时没有开销。

常见陷阱

  • Float 和 Double 返回空诊断并不意味着精确。 它们的上下文算术忽略上下文,也不检测舍入:add_contextual(0.1, 0.2, ctx) 返回 0.30000000000000004,诊断却为空。只有 Float 的整数嵌入会检测精度损失。当标志很重要时,请使用忠实遵循上下文的后端。
  • 上下文是一种请求。 ArithmeticContext::new(16) 并不会让 Double 算术变成十进制;它只是告诉后端在能做到时该怎么做。ArithmeticContext::new(0) 会静默地变为精度 1,而 e_min 大于 e_max 会导致中止。
  • 非检查的 Power 遇到负整数指数会中止。 对 Int、Int16、Int64 和 BigInt,@lf_arith.Power::pow(2, -1) 会中止,而固定宽度整数的幂在上溢时会回绕。求倒数请在浮点类型上使用 PowIntChecked。
  • 极小的底数配负指数。 pow_int_checked(1.0e-200, -2, ctx) 返回 DivisionByZero,因为在取倒数之前 x2x^2 已经下溢为零。
  • epsilon_contextual 是 ε\varepsilon,而不是单位舍入误差。 就近舍入的误差以 u=ε/2u = \varepsilon/2 为界。
  • definitely_lt 为假并不意味着“大于或等于”。 对于相互重叠的包络,两个方向都为假;还要问相反方向的问题,就像上面的 less 那样。
  • maybe_eq 不是相等。 它表示两个包络有公共点。两个不同的值可以有相互重叠的包络。
  • Double 上的 x.sqrt() 是 core 的方法,而不是 trait。 在泛型代码中应调用 trait,即 @lf_arith.Sqrt::sqrt(x);对 Double 两者结果一致,但只有 trait 调用对每个 T : Sqrt 都适用。

后续步骤

  • core API 列出了每个 trait、类型和实例及其确切语义。
  • core 设计解释了三个层级、显式上下文和包络逻辑,并给出舍入误差和正确性的推导。
  • luna-generic 提供可与这些能力组合使用的代数 trait。