decimal_gda_checked 教程

本教程展示如何把一次通用十进制算术(GDA)计算作为遵守 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 basic 上下文下做除法:

///|
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 basic 上下文:精度 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 遵循 GDA 的限制,精度和指数界须在 ±999 999\pm 999\,999 以内。交换格式上下文满足这一条件;采用默认无界范围的 GdaContext::new 则不满足,这些函数会返回带 invalid_context 的 NaN(在 InvalidOperation 下会触发陷阱):

///|
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。

后续步骤