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",
}

81/3\sqrt{81} / 3 を 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 は 3⋅203 \cdot 2^0 です。

日常的なタスク

残りの例では次のヘルパーで結果を出力します。

///|
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 値では、2 回目の呼び出しは黙って数を生成します(1/0=∞1/0 = \infty かつ 2/∞=02/\infty = 0)。ラッパーは、計算がどこで実数の範囲を離れたかを報告します。

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(融合された ln⁡(exp⁡x)\ln(\exp x))は ∣x∣≤1/8|x| \le 1/8 に対してのみ認証されるので、33 では失敗します。これは数学的な定義域ではなく、認証の限界です。

さらに進んで

Luna-Flow/arithmetic のトレイト上のジェネリックなコード

BinFloat は Luna-Flow/arithmetic の checked トレイト(SqrtChecked、DivChecked、PowIntChecked、PowNatChecked)を実装しています。それらのトレイトに対して書かれた関数は 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 フラグを保持する

ラッパーは値かエラーを保持するだけで、それ以外は何も保持しません。inexact、オーバーフロー、アンダーフローのフラグが契約の一部である場合は、(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 または無限大の結果を定義している演算(∞−∞\infty - \infty、負の数に対する 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 を使ってください。

次のステップ