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 时,就使用它。下面的二次方程求解器使用无抵消形式 、、:
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, 的退化方程是 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")
}
无法放入 Float,因此嵌入把它舍入为 并设置 inexact。 的平方根是精确的,但合并后的诊断仍记得先前的舍入:标志只会累积。
遍历可表示数
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")
}
在 处,相邻的双精度数相距 ,因此超过它的整数不再全部可表示。
报告认证失败
基于证明的后端用 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")
}
和 都恰好处于中点;就近偶数舍入把它们分别变为 和 ,即末位为偶数的那个相邻值。
以三种结果比较包络
对于区间或球类型,“ 吗?”有三种答案:对每个容许值都是,对每个容许值都不是,或者未知。两个关系 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 定义。当算法同时需要两者时,在约束中把它们组合起来。例如,求 的 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,因为在取倒数之前 已经下溢为零。 epsilon_contextual是 ,而不是单位舍入误差。 就近舍入的误差以 为界。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。