decimal_checked 设计
设计目标
decimal_checked 把一系列 IEEE 754 十进制运算变为一个值,它在最后回答两个问题:结果是什么,以及途中发生了什么。IEEE 十进制算术把异常条件作为状态标志,与一个有定义的结果一同报告;单个运算返回 (value, flags),但多步计算需要每一步的标志。DecimalChecked 让同一个上下文贯穿各步并累积标志,同时保持 IEEE 的规则:异常结果仍是值。API 页面列出了各项运算;教程展示了审计式的流水线。
数学背景
标志幺半群
DecimalFlags 有十三个布尔字段(inexact、rounded、overflow、…),因此一个标志集合是 的元素,或等价地,是十三种信号构成的集合 的子集。DecimalFlags::combine 是逐字段的 OR:
由于布尔 OR 满足结合律、交换律和幂等律,单位元为 ,且运算逐字段进行,
因此 是交换幂等幺半群,即有界并半格;按子集解释, 就是并集。DecimalFlags::contains(s) 读取坐标 ,它是到 的幺半群同态:。
运算作为 writer 箭头
固定上下文 下的 IEEE 运算是十进制值 上的函数 ,返回舍入后的结果及其引发的标志(Decimal::add_ctx、div_ctx、sqrt_ctx、…)。这样的函数是 上 writer 单子的 Kleisli 箭头:
单子律归结为幺半群律。左单位律:。右单位律:。结合律:取 和 , 与 都等于 。11 P. Wadler, “Monads for functional programming”, 1995(输出单子);任何幺半群都给出一个 writer 单子,其单子律恰好就是幺半群律。
流水线状态
DecimalChecked 存储 :值、上下文、最近一步的标志、累积的标志,以及一个可选的错误。私有的 record 步骤就是 writer 绑定,扩展了最近标志,并受错误守护:
record_result 处理可能失败的运算 :在 上它就是 record,在 上它返回 。因此完整的步骤是叠加在异常单子之上的 writer 单子,错误具有吸收性。
设计决策
标志被累积,异常结果仍是值
问题。 一个长的十进制计算必须报告是否有任何一步发生了舍入、上溢或除以零,而 IEEE 754 为每个这样的步骤都定义了结果。
方案。 (a) 把异常条件变为错误,就像 Luna-Flow/arithmetic 的上下文 trait 对除以零和无效运算所做的那样。(b) 只返回最后一步的标志。(c) 保留 IEEE 值并累积标志。
选择:(c)。 IEEE 的模型是:计算以有定义的结果继续进行,状态标志说明发生了什么;审计在最后检查这些标志。22 IEEE 754-2019,第 7 条(默认异常处理)和第 8 条(替代异常处理):状态标志一经引发便保持引发状态,直到被显式清除。 方案 (a) 会在 处停止,尽管 IEEE 定义了 ;方案 (b) 会丢失较早的条件。错误只保留给本实现中不存在有定义 IEEE 结果的唯一情形:经认证的初等函数无法认证其舍入。同时保留 (raised)和 (flags),调用者既能看到最近一步,也能看到全部历史。
每条流水线一个上下文,操作数为普通值
问题。 二元运算本可以把另一条流水线作为操作数。
选择。 操作数是普通的 Decimal 值。两条流水线携带两个上下文和两段标志历史;合并它们需要规定哪个上下文胜出,而历史的并集会掩盖是哪一方引发了标志。使用普通操作数时,流水线是同一上下文下的单一线性历史,上下文只能通过 with_context 改变,它会记录把当前值舍入到新上下文时的标志。出于同样的原因,该类型不实现任何运算符。
映射 Luna-Flow/arithmetic 上下文
流水线接受一个 DecimalContext。持有 ArithmeticContext 的调用者用 DecimalContext::from_arithmetic_context 将其转换,映射如下
ArithmeticContext | DecimalContext |
|---|---|
precision | precision |
rounding | rounding,以及 decimal_rounding = DecimalRoundingMode::from_arithmetic(rounding) |
e_min, e_max | 相同;缺省时为 |
clamp | clamp |
并采用 extended = true 以及舍入后的微小性检测。因此预定义的 ArithmeticContext::decimal64() 恰好映射为 DecimalContext::decimal64()。各构造函数让上下文经过 DecimalContext::ieee754(),这是选择 IEEE 配置的钩子;在当前分支上它原样返回上下文。
数学函数像 General Decimal Arithmetic 规范对 exp、ln、log10 和 power 所做的那样限制其上下文:精度和两个指数界的绝对值都必须在 以内,否则结果为带 invalid_context 的 NaN(src/decimal 中的私有检查 math_context_is_restricted)。33 M. Cowlishaw, General Decimal Arithmetic Specification, version 1.70, “Arithmetic operations: exp, ln, log10, power”(对上下文的限制)。 因此默认的无界指数范围会禁用它们,API 页面和教程都指出了这一点。
从标志到算术诊断
Luna-Flow/arithmetic 中 Decimal 的上下文 trait 实现报告 ArithmeticDiagnostics,即由 ArithmeticDiagnostics::combine 合并的六个布尔值,同样是逐字段 OR。src/decimal/traits.mbt 中的私有辅助函数 contextual_diagnostics 通过保留六个共有坐标把标志映射为诊断;记此投影为 。坐标投影与逐字段 OR 可交换:
因此 是幺半群同态,由归纳可得
在最后对流水线的累积标志转换一次,与逐步转换再合并得到的诊断相同。trait 实现转换为 ArithmeticError 的那些条件(除以零以及由 DecimalFlags::has_error 检测的无效运算族)不在 保留的坐标之中;流水线改为把它们作为标志保留。
正确性 / 不变式
累积标志是各步标志的并
定理。 设一条流水线以标志 构造(来自 from_outcome、from_decimal、parse 或数值构造函数),随后经历成功的步骤 ,其引发的标志为 (包括 with_context 和 apply),其间没有 clear_flags。则
证明。 每个构造函数都设置 ,这就是 的情形。若 ,成功的步骤 经过 record(或 with_context 中完全相同的更新),由结合律设置 ,并且 。
由 的交换律和幂等律, 与步骤的顺序以及某信号被引发的次数无关;再由 contains 的同态性质,
下面的测试在一条三步流水线上检验该定理:累积标志等于各步引发标志的 OR,无论以何种顺序合并。
///|
test "accumulated flags are the union of the raised flags" {
let ctx = @decimal.DecimalContext::decimal64()
let s0 = @decimal_checked.DecimalChecked::from_int(1, ctx)
let s1 = s0.div(@decimal.Decimal::from_int(3))
let s2 = s1.add(@decimal.Decimal::from_int(10))
let s3 = s2.div(@decimal.Decimal::zero())
let forward = s0.raised().combine(s1.raised()).combine(s2.raised()).combine(s3.raised())
let backward = s3.raised().combine(s2.raised()).combine(s1.raised()).combine(s0.raised())
inspect(s3.flags() == forward, content="true")
inspect(forward == backward, content="true")
inspect(s3.raised().inexact, content="false")
inspect(s3.flags().inexact && s3.flags().division_by_zero, content="true")
}
在 clear_flags 之后,定理从该点起以 重新成立。 沿流水线单调:。
错误具有吸收性
定理。 若一个状态有 ,则除 clear_flags 外的每个运算都原样返回它,而 clear_flags 只改变 和 。
证明。 record、record_result 和 with_context 都以守卫 self.error_ is None 开头,否则返回 self;每个算术方法都经过 record 或 record_result。clear_flags 只更新两个标志字段。
因此被报告的是第一个错误,值和标志保持为失败步骤之前的状态,且 result() 返回 。在没有错误的流水线上,result() 就是 ,即 writer 单子的输出。
开销
每一步的开销是被委托的十进制运算,加上一次 13 字段的 OR 和一次常数大小的状态复制。
被否决的替代方案
- 把 IEEE 异常变为错误。 这与 IEEE 默认异常处理以及本包的目的相矛盾;上下文 trait 已经为单个运算提供了那种模型。
- 合并流水线。 对两个上下文没有规范的合并规则;见上文。
- 可变标志。 Luna-Flow 把上下文和状态都保持为值;不可变状态可以分叉和比较。
- 存储逐步历史。 并集以常数大小回答了审计问题;需要历史的调用者可以自行保留关心的状态。
边界
- 自身不含算术;每个结果都来自
decimal。 - 没有陷阱或替代异常处理:IEEE 标志从不中止流水线。由陷阱驱动的控制流属于
decimal_gda_checked的契约。 - 没有运算符,没有流水线与流水线之间的运算,也没有隐式的上下文改变。
- 只有初等函数的认证失败才会变成错误。
- 上下文是
decimal的 IEEE 十进制上下文;带粘滞状态的 GDA 上下文属于decimal_gda。