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 位精度计算 并读取结果:
///|
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 即 。
日常任务
其余示例用下面这个辅助函数打印结果:
///|
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 值,第二次调用会悄无声息地产生一个数(,);而包装器会报告计算是在何处离开实数的。
用 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(融合的 )只在 时可认证,因此对 会失败;这是认证上的限制,而不是数学上的定义域。
深入了解
基于 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 或无穷结果的运算(、对负数的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。
后续步骤
bin_float_checked设计:错误单子、其定律以及第一个错误定理。bin_float_checkedAPI:所有方法。bin_float教程:上下文、标志与交换格式。ball_float_checked教程:面向包络的同类流水线。