decimal_gda_checked チュートリアル

このチュートリアルでは、GDA のトラップを尊重するパイプラインとして General Decimal Arithmetic (GDA) の計算を実行する方法を示します。アプリケーションが必要とするトラップを持つ GdaContext を選び、GdaDecimalChecked を開始し、演算を連鎖させ、トラップしたステップを検出し、スティッキーなステータスを調べ、続行するかどうかを明示的に決めます。これは Python の decimal モジュールや GDA のテストスイートのモデルです。ステータスフラグはスティッキーであり、有効なトラップは計算を停止させます。演算は decimal_gda に由来します。ステータスとトラップの法則は設計ページで証明し、すべてのメソッドは API リファレンスに一覧があります。

クイックスタート

moon add Luna-Flow/floating@0.8.0
import {
  "Luna-Flow/floating/decimal_gda",
  "Luna-Flow/floating/decimal_gda_checked",
}

ゼロ除算をトラップする GDA の基本コンテキストの下で除算します。

///|
test "quick start: a trapped division" {
  let ctx = @decimal_gda.GdaContext::default()
  let r = @decimal_gda_checked.GdaDecimalChecked::parse("1", ctx).divide(
    @decimal_gda.Decimal::zero(),
  )
  inspect(r.is_trapped(), content="true")
  inspect(
    r.trapped_signal() == Some(@decimal_gda.GdaSignal::DivisionByZero),
    content="true",
  )
}

日常的なタスク

以下の例では、次のヘルパーでフラグを列挙します。

///|
fn gda_flags(f : @decimal_gda.GdaFlags) -> String {
  let named = [
    ("inexact", f.inexact),
    ("rounded", f.rounded),
    ("invalid_operation", f.invalid_operation),
    ("division_by_zero", f.division_by_zero),
    ("overflow", f.overflow),
    ("conversion_syntax", f.conversion_syntax),
    ("invalid_context", f.invalid_context),
  ]
  [ for p in named if p.1 => p.0 ].join(",")
}

トラップを選ぶ

GdaContext はトラップ集合を持ちます。定義済みのコンテキストはそれぞれ異なります。GdaContext::default()(GDA の基本コンテキスト: 精度 9、HalfUp)は DivisionByZero、InvalidOperation、Overflow、Underflow、Clamped をトラップし、decimal32()、decimal64()、decimal128()、new(...) は何もトラップしません。トラップの追加・削除は trap で行います。

///|
test "trap inexact results" {
  let exact_only = @decimal_gda.GdaContext::decimal64().trap(
    @decimal_gda.GdaSignal::Inexact,
  )
  let ok = @decimal_gda_checked.GdaDecimalChecked::parse("10", exact_only).divide(
    @decimal_gda.Decimal::from_int(4),
  )
  inspect(ok.is_trapped(), content="false")
  inspect(ok.value().to_string(), content="2.5")
  let stopped = @decimal_gda_checked.GdaDecimalChecked::parse("10", exact_only).divide(
    @decimal_gda.Decimal::from_int(3),
  )
  inspect(stopped.is_trapped(), content="true")
  inspect(
    stopped.trapped_signal() == Some(@decimal_gda.GdaSignal::Inexact),
    content="true",
  )
}

スティッキーなステータスを読み取る

raised() は最新の演算のシグナルを保持し、status() はコンテキストが作成されて以降のすべてのシグナルを保持します。

///|
test "status is sticky" {
  let ctx = @decimal_gda.GdaContext::new(precision=5)
  let r = @decimal_gda_checked.GdaDecimalChecked::parse("1.234567", ctx)
    .add(@decimal_gda.Decimal::one())
    .multiply(@decimal_gda.Decimal::from_int(2))
  inspect(r.value().to_string(), content="4.4692")
  inspect(gda_flags(r.raised()), content="")
  inspect(gda_flags(r.status()), content="inexact,rounded")
}

トラップの後は何も実行されない

いったんトラップされると、以降のすべての演算はパイプラインを変更せずに返します。値はトラップしたステップの定義済みの結果です。

///|
test "short circuit after a trap" {
  let ctx = @decimal_gda.GdaContext::default()
  let r = @decimal_gda_checked.GdaDecimalChecked::parse("1", ctx)
    .divide(@decimal_gda.Decimal::zero())
    .add(@decimal_gda.Decimal::one())
    .multiply(@decimal_gda.Decimal::from_int(5))
  inspect(r.is_trapped(), content="true")
  inspect(r.value().to_string(), content="inf")
}

意図的に再開する

resume_defined() は定義済みの結果を受け入れて続行します。ステータスはトラップされたシグナルを記録したままなので、その判断は後からでも確認できます。

///|
test "resume with the defined result" {
  let ctx = @decimal_gda.GdaContext::default()
  let trapped = @decimal_gda_checked.GdaDecimalChecked::parse("1", ctx).divide(
    @decimal_gda.Decimal::zero(),
  )
  let resumed = trapped.resume_defined().minus()
  inspect(resumed.is_trapped(), content="false")
  inspect(resumed.value().to_string(), content="-inf")
  inspect(gda_flags(resumed.status()), content="division_by_zero")
}

再開した後もトラップは有効なままです。再びゼロで割れば、再びトラップされます。

さらに進んで

構文エラーは無効演算である

GDA は変換構文、不可能な除算と未定義の除算、無効なコンテキストを無効演算の条件に分類します。したがって InvalidOperation をトラップするコンテキストは不正な文字列で停止し、ステータスには個別の条件に加えて invalid_operation が記録されます。

///|
test "a malformed literal" {
  let r = @decimal_gda_checked.GdaDecimalChecked::parse(
    "12,5",
    @decimal_gda.GdaContext::default(),
  )
  inspect(r.is_trapped(), content="true")
  inspect(
    r.trapped_signal() == Some(@decimal_gda.GdaSignal::InvalidOperation),
    content="true",
  )
  inspect(gda_flags(r.status()), content="invalid_operation,conversion_syntax")
}

数学関数には有界なコンテキストが必要

exp、ln、log10、power は、精度と指数の限界を ±999 999\pm 999\,999 以内とする GDA の制約に従います。交換形式のコンテキストはこれを満たしますが、デフォルトの非有界な範囲を持つ GdaContext::new は満たさず、関数は invalid_context(InvalidOperation の下でトラップされる)付きの NaN を返します。

///|
test "exp needs a bounded context" {
  let good = @decimal_gda_checked.GdaDecimalChecked::parse(
    "2",
    @decimal_gda.GdaContext::decimal64(),
  ).exp()
  inspect(good.value().to_string(), content="7.389056098930650")
  let bad = @decimal_gda_checked.GdaDecimalChecked::parse(
    "2",
    @decimal_gda.GdaContext::new(precision=16),
  ).exp()
  inspect(bad.value().to_string(), content="nan")
  inspect(gda_flags(bad.raised()), content="invalid_context")
}

素の GDA 関数と組み合わせる

decimal_gda の演算はどれも GdaOutcome を返します。パイプラインに対応するメソッドがない場合は、value() と context() に対してその演算を実行し、結果を包みます。

///|
test "use an operation without a pipeline method" {
  let ctx = @decimal_gda.GdaContext::decimal64()
  let start = @decimal_gda_checked.GdaDecimalChecked::parse("7.5", ctx)
  let rounded = @decimal_gda_checked.GdaDecimalChecked::from_outcome(
    @decimal_gda.to_integral_value(start.value(), start.context()),
  )
  inspect(rounded.value().to_string(), content="8")
}

これはトラップされていないパイプラインに対してだけ行ってください。from_outcome は直前の状態をチェックしません。

よくある落とし穴

  • default() はトラップし、new() はトラップしない。 コンテキストは意図して選んでください。
  • raised() と status() の違い。 前者は最新のステップ、後者はスティッキーな履歴です。
  • resume_defined はステータスとトラップを保持する。 クリアするのはトラップの印と raised() だけです。必要なら、コンテキストに対して GdaContext::clear_status でステータスをクリアし、新しいパイプラインを開始してください。
  • エラーはない。 トラップは ArithmeticError に変換されません。is_trapped() を確認してください。
  • 二つの十進パッケージ。 decimal_gda.Decimal は decimal.Decimal ではありません。トラップなしの IEEE 流のフラグ蓄積は decimal_checked です。

次のステップ