def チュートリアル

このチュートリアルでは、floating のすべての数値型で一度に動作するコードの書き方を示します。Floating トレイトを通じて値を調べ、ジェネリックな述語でそのクラスを判定し、IEEE の比較結果を PartialOrder として読み取り、def が再エクスポートする算術のコンテキスト型とエラー型を再利用します。def 自身は算術を行わず、それを行うのは具体的なパッケージ(bin_float、decimal、decimal_gda、ball_float)です。トレイトの背後にある法則は設計ページにあります。

クイックスタート

モジュールを追加し、使用する表現と並べて 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 十進、区間の値を受け付けます。MoonBit はもはやトレイトメソッドを型パラメータのドットメソッドにしないので、トレイトメソッドは修飾形式 @def.Floating::classify(x) で呼び出してください。

///|
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")
}

11 は二進で 1011 です。有効ビット 3 桁に丸めると 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 の型です。語彙だけが必要なコードは、arithmetic の代わりに def をインポートできます。

///|
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 を実装する

トレイトは開いています。ラッパー型は既存の実装に委譲できますが、その場合も設計ページに挙げた法則を守らなければなりません(例えば 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 に切り上げられます。

次のステップ