def 教程

本教程展示如何编写能同时适用于 floating 所有数值类型的代码:通过 Floating trait 检视值,用泛型谓词检验其类别,把 IEEE 比较结果读作 PartialOrder,并复用 def 重新导出的算术上下文与错误类型。def 本身不做任何算术;算术由具体的包(bin_float、decimal、decimal_gda、ball_float)完成。该 trait 背后的定律见设计页面。

快速入门

添加模块,并在你所用的表示之外再导入 def:

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

最小的实用程序询问一个值是什么:

///|
test "quick start: observe a value" {
  let x = @bin_float.BinFloat::from_double(-2.5)
  inspect(@def.is_finite(x), content="true")
  inspect(@def.Floating::sign(x) == @def.Sign::Negative, content="true")
  inspect(@def.Floating::precision(x), content="53")
}

日常任务

为所有表示编写同一个函数

以 F : @def.Floating 为约束的函数可以接受二进制、IEEE 十进制、GDA 十进制和区间值。请以限定形式 @def.Floating::classify(x) 调用 trait 方法,因为 MoonBit 不再把 trait 方法变成类型参数的点方法。

///|
fn[F : @def.Floating] summary(x : F) -> String {
  if @def.is_nan(x) {
    return "nan"
  }
  let sign = match @def.Floating::sign(x) {
    Negative => "negative"
    Zero => "zero"
    Positive => "positive"
  }
  let kind = if @def.is_infinite(x) { "infinite" } else { "finite" }
  "\{kind} \{sign} at precision \{@def.Floating::precision(x)}"
}

///|
test "one summary for four representations" {
  inspect(
    summary(@bin_float.BinFloat::from_int(7)),
    content="finite positive at precision 53",
  )
  inspect(
    summary(@decimal.Decimal::from_string("-0.00").unwrap()),
    content="finite zero at precision 34",
  )
  inspect(
    summary(@decimal_gda.Decimal::from_string("-Infinity").unwrap()),
    content="infinite negative at precision 34",
  )
  inspect(summary(@ball_float.BallFloat::empty()), content="nan")
}

区间的情形说明了为什么要先检验 NaN:空区间被分类为 NaN,而 BallFloat::sign 在其上会中止。

泛型地改变精度

with_precision 舍入到所要求的有效数字位数,位数以值自身的基数计:二进制为位,十进制为十进制数字:

///|
fn[F : @def.Floating] three_digits(x : F) -> F {
  @def.Floating::with_precision(x, 3, @lf_arith.RoundingMode::ToNearestEven)
}

///|
test "round to three significant digits of the radix" {
  let d = three_digits(@decimal.Decimal::from_string("3.14159").unwrap())
  inspect(d.to_string(), content="3.14")
  let b = three_digits(@bin_float.BinFloat::from_int(11))
  inspect(b.to_string(), content="3p2")
}

十一的二进制是 1011;三位有效位把它舍入为 1100,to_string 将其打印为 3⋅223 \cdot 2^2。

读取 IEEE 比较结果

遵循 IEEE 754 的比较返回一个 PartialOrder,其第四个值 Unordered 表示有 NaN 操作数:

///|
fn relation(a : @bin_float.BinFloat, b : @bin_float.BinFloat) -> String {
  match a.compare_quiet(b).0 {
    Less => "less"
    Equal => "equal"
    Greater => "greater"
    Unordered => "unordered"
  }
}

///|
test "four-way comparison" {
  let one = @bin_float.BinFloat::from_int(1)
  let two = @bin_float.BinFloat::from_int(2)
  inspect(relation(one, two), content="less")
  inspect(relation(@bin_float.BinFloat::nan(), @bin_float.BinFloat::nan()), content="unordered")
  inspect(
    relation(@bin_float.BinFloat::from_double(-0.0), @bin_float.BinFloat::zero()),
    content="equal",
  )
}

比较表示之前先规范化

值相同的十进制数,其存储的指数可能不同(1.5 和 1.500 属于同一个同值类)。normalized 选取规范成员,从而使打印形式一致:

///|
test "normalize a decimal cohort" {
  let a = @decimal.Decimal::from_string("1.500").unwrap()
  let b = @decimal.Decimal::from_string("1.5").unwrap()
  inspect(a.to_string(), content="1.500")
  inspect(
    @def.Floating::normalized(a).to_string() ==
    @def.Floating::normalized(b).to_string(),
    content="true",
  )
}

深入了解

使用重新导出的算术类型

@def.ArithmeticContext、@def.ArithmeticError、@def.RoundingMode 以及其他别名都是 Luna-Flow/arithmetic 的类型。只需要这套词汇的代码可以导入 def 而不是 arithmetic:

///|
fn describe_error(e : @def.ArithmeticError) -> String {
  if e.is_division_by_zero() {
    "division by zero: " + e.message
  } else if e.is_domain_error() {
    "domain error: " + e.message
  } else {
    "other: " + e.message
  }
}

///|
test "handle a checked result through def" {
  let result = @bin_float.BinFloat::from_int(1).div_checked(
    @bin_float.BinFloat::zero(),
  )
  match result {
    Ok(_) => fail("expected an error")
    Err(e) => inspect(describe_error(e), content="division by zero: division by zero")
  }
}

为自己的类型实现 Floating

该 trait 是开放的。包装类型可以委托给已有的实现;但它必须遵守设计页面列出的定律(例如,normalized 不得改变值):

///|
struct Measured {
  value : @bin_float.BinFloat
  unit : String
}

///|
impl @def.Floating for Measured with classify(self) {
  @def.Floating::classify(self.value)
}

///|
impl @def.Floating for Measured with sign(self) {
  @def.Floating::sign(self.value)
}

///|
impl @def.Floating for Measured with precision(self) {
  @def.Floating::precision(self.value)
}

///|
impl @def.Floating for Measured with with_precision(self, precision, mode) {
  { ..self, value: @def.Floating::with_precision(self.value, precision, mode) }
}

///|
impl @def.Floating for Measured with normalized(self) {
  { ..self, value: @def.Floating::normalized(self.value) }
}

///|
test "a user type joins the generic code" {
  let m = { value: @bin_float.BinFloat::from_int(-4), unit: "m" }
  inspect(summary(m), content="finite negative at precision 53")
  inspect(@def.is_zero(m), content="false")
  let coarse = @def.Floating::with_precision(m, 1, @lf_arith.RoundingMode::TowardZero)
  inspect(coarse.unit, content="m")
}

常见陷阱

  • Sign::Zero 并不意味着“值为零”。标量类型对 NaN 也返回它,任何包含零的区间也返回它。请先检验 @def.is_nan;对区间请使用 BallFloat::contains_zero 或其界。
  • 对 BallFloat 而言,@def.is_zero 在 [−1,1][-1, 1] 上为真:该谓词通过 sign 定义,而它对区间的含义是“包含零”。
  • Sign 无法区分 −0-0 和 +0+0。当符号位重要时,请使用具体的包(BinFloat::is_negative_zero、Decimal::is_signed)。
  • PartialOrder 不是可以用来排序的序:Unordered 破坏了三分律。排序请使用具体包提供的全序(BinFloat::total_order、Decimal::compare_total)。
  • with_precision 不报告标志,也忽略指数界限。当舍入必须可观测时,请使用具体包的 *_ctx 运算。
  • with_precision(x, 0, mode) 不是错误:精度会被钳制为 1。

后续步骤