error 设计

设计目标

许多矩阵运算都是部分运算:乘积需要可复合的形状,行列式需要方阵,逆矩阵需要非奇异矩阵。error 包为这些失败提供一种共享的结构化表示,使仓库中的每个受检 API 都能把部分运算变为全运算,调用者也无需解析文本即可决定在失败路径上如何处理。

数学背景

部分函数

部分函数 f:X⇀Yf : X \rightharpoonup Y 是定义在子集 dom⁡f⊆X\operatorname{dom} f \subseteq X 上的函数。矩阵运算在形状上或数值上是部分的:

运算定义域
(A,B)↦AB(A, B) \mapsto ABcols⁡(A)=rows⁡(B)\operatorname{cols}(A) = \operatorname{rows}(B)
A↦tr⁡AA \mapsto \operatorname{tr} A, A↦det⁡AA \mapsto \det AAA 为方阵
(A,k)↦Ak(A, k) \mapsto A^{k}AA 为方阵,k≥0k \ge 0
A↦A−1A \mapsto A^{-1}AA 为方阵且 det⁡A≠0\det A \ne 0
A↦mean⁡(A)A \mapsto \operatorname{mean}(A)AA 至少有一个元素

使其成为全函数的两种方式

程序必须对每个输入都有所处理。有两种诚实的选择。

受检形式用一个错误值把 ff 扩展到整个 XX:

f^:X→Y+E,f^(x)={Ok (f(x))x∈dom⁡f,Err (ε(x))otherwise.\hat f : X \to Y + E, \qquad \hat f(x) = \begin{cases} \mathrm{Ok}\,(f(x)) & x \in \operatorname{dom} f, \\ \mathrm{Err}\,(\varepsilon(x)) & \text{otherwise.} \end{cases}

非受检形式保持类型 X→YX \to Y,并把属于定义域作为前置条件:对 x∉dom⁡fx \notin \operatorname{dom} f,它中止(或者,对于某些有文档说明的例程,返回未定义的值)。

两种形式之间由仓库对每一对受检/非受检运算所维护的定律联系起来:

x∈dom⁡f  ⟹  checked(x)=Ok (unchecked(x)),x∉dom⁡f  ⟹  checked(x)=Err (_).x \in \operatorname{dom} f \;\Longrightarrow\; \mathtt{checked}(x) = \mathrm{Ok}\,(\mathtt{unchecked}(x)), \qquad x \notin \operatorname{dom} f \;\Longrightarrow\; \mathtt{checked}(x) = \mathrm{Err}\,(\_).

在代码中,几乎每个受检方法字面上都是“先校验,再调用非受检方法”,因此第一个蕴含关系由构造保证成立。

复合

受检运算在错误单子的 Kleisli 范畴中复合:给定 f^:X→Y+E\hat f : X \to Y + E 和 g^:Y→Z+E\hat g : Y \to Z + E,

(g^∘Kf^)(x)={g^(y)f^(x)=Ok (y),Err (e)f^(x)=Err (e),(\hat g \circ_K \hat f)(x) = \begin{cases} \hat g(y) & \hat f(x) = \mathrm{Ok}\,(y), \\ \mathrm{Err}\,(e) & \hat f(x) = \mathrm{Err}\,(e), \end{cases}

复合的定义域是 {x∈dom⁡f:f(x)∈dom⁡g}\{x \in \operatorname{dom} f : f(x) \in \operatorname{dom} g\}。在 MoonBit 中,这就是一个遇到 Err 即提前返回的 match,或者 Result::bind。正是因为整个仓库使用同一种错误类型,这种复合才无需转换。

设计决策

带 kind 的结构体,而不是 suberror

选项。 (a) 用 raise 抛出的 MoonBit suberror。(b) 普通枚举。(c) 包含 kind 与消息、通过 Result 返回的结构体。

决定。 (c),遵循 Luna-Flow 约定:pub struct LinearAlgebraError { kind, message } 配合 pub enum LinearAlgebraErrorKind。

理由。 Result 是普通的值:它可以存储、映射和组合,而且它的存在在签名中可见。kind 是控制流的契约;消息可以自由变化,并携带诸如哪个索引出错之类的细节。让枚举在包外只读,并暴露 snake_case 构造函数(LinearAlgebraError::singular_matrix),使该包日后可以增加字段而不破坏调用者。

与枚举并列的谓词

每种 kind 都有一个 is_* 谓词。大多数调用者只需要一个是/否问题(“是否奇异?”),而当某个 kind 在未来版本中增加载荷时,谓词仍然有效,穷尽式 match 则不然。

非受检形式保留原有行为

在 0.4.0 之前,矩阵方法要么中止,要么返回 Option。这些行为以显式的 unchecked_* 名称保留下来(unchecked_inverse 仍返回 Option),而短名称成为受检形式。因此读到 m.inverse() 时,默认看到的是安全形式,不安全的形式则会自报家门。

保留的 kind

InvalidLength、RaggedRows、NonConvergence 和 ArithmeticFailure 在本版本中不由任何 API 产生;对应的操作(from_array、from_2d_array、eigen)仍会中止。这些 kind 之所以存在,是为了让下游的受检包装和未来的受检构造函数能用共享词汇报告这些失败,而无需对枚举做破坏性修改。

没有 Show 或 Debug

该错误只实现 Eq。格式化与本地化属于展示策略,本包将其留给应用程序;message 是诊断文本。

正确性与不变量

  • kind 决定谓词。 对每个错误 e,恰有一个 is_* 谓词返回 true,即以 e.kind 命名的那个。
  • 受检/非受检定律。 对 immut 和 mutable 中的每一对,背景一节中的定律都成立;consistency 以及各包的测试会覆盖两条路径。
  • 确定性的错误选择。 当输入违反多个前置条件时,检查按固定顺序进行,因此报告的 kind 是输入的函数。例如 pow 先检查是否为方阵,再检查指数的符号:带负指数的非方阵总是报告 NonSquareMatrix。
  • 没有部分副作用。 不可变包的受检运算从不修改其参数;可变包的受检运算在写入之前先校验,因此 Err 结果不会改变参数。

被否决的方案

  • 用字符串作为错误码(贡献者笔记中提到的一种较早的 E_ 前缀约定)。字符串无法被穷尽匹配,而且会诱使人去解析。
  • 对每种失败都返回 Option。 这会丢失原因;逆矩阵返回的 None 无法说明矩阵是非方阵还是奇异矩阵。
  • 每个包使用各自的错误类型。 这样在 immut、mutable 和 container 之间复合时,每个边界都需要转换。

边界

error 只定义值。它不实现任何矩阵算法,不提供针对奇异或病态输入的恢复策略,也不做日志或格式化。它不决定哪些运算是受检的;那是每个返回 LinearAlgebraError 的包自己的职责。