error 設計

設計目標

多くの行列演算は部分的です。積には合成可能な形状が、行列式には正方行列が、逆行列には正則行列が必要です。error パッケージはこれらの失敗に共通の構造化された表現を 1 つ与えます。これにより、リポジトリのすべての検査付き 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 が少なくとも 1 つの要素を持つ

全域化する 2 つの方法

プログラムはすべての入力に対して 何か をしなければなりません。誠実な選択肢は 2 つあります。

検査付き 形式は、エラー値を使って 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 では中断します(あるいは、そうとドキュメント化された一部のルーチンでは、規定されない値を返します)。

2 つの形式は、リポジトリがすべての検査付き・検査なしの組について維持している次の法則で結び付いています。

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 です。リポジトリ全体で単一のエラー型を使っているからこそ、変換なしでこの合成が可能になります。

設計上の判断

suberror ではなく、種類を持つ構造体

選択肢。 (a) raise で送出する MoonBit の suberror。(b) 素朴な enum。(c) 種類とメッセージを保持し、Result で返す構造体。

決定。 Luna-Flow の慣例に従い (c)。pub enum LinearAlgebraErrorKind を伴う pub struct LinearAlgebraError { kind, message } です。

理由。 Result は普通の値です。保存したり、写像したり、組み合わせたりでき、その存在はシグネチャに現れます。種類は制御フローのための契約であり、メッセージは自由に変更でき、どのインデックスが誤っていたかといった詳細を伝えます。enum をパッケージ外から読み取り専用にし、スネークケースのコンストラクタ(LinearAlgebraError::singular_matrix)を公開しておくことで、パッケージは後から呼び出し側を壊さずにフィールドを追加できます。

enum と並ぶ述語

各種類には is_* 述語があります。呼び出し側の大半は yes/no の問い(「特異だったか?」)しか必要とせず、述語は将来のバージョンで種類にペイロードが加わっても機能し続けます。網羅的な match ではそうはいきません。

検査なし形式は従来の振る舞いを保つ

0.4.0 より前は、行列メソッドは中断するか Option を返していました。それらの振る舞いは明示的な unchecked_* という名前のもとで保たれ(unchecked_inverse は今も Option を返します)、短い名前は検査付き形式になりました。そのため m.inverse() を読む人は既定で安全な形式を目にし、安全でない形式はその名前で自らを示します。

予約された種類

InvalidLength、RaggedRows、NonConvergence、ArithmeticFailure は、本リリースのどの API からも生成されません。対応する操作(from_array、from_2d_array、eigen)は今も中断します。これらの種類は、下流の検査付きラッパーや将来の検査付きコンストラクタが、enum に破壊的変更を加えずに共通の語彙でこれらの失敗を報告できるように存在しています。

Show も Debug もない

このエラーは Eq だけを実装します。書式化とローカライズは表示の方針であり、このパッケージはアプリケーションに任せます。message が診断用のテキストです。

正しさと不変条件

  • 種類が述語を決める。 すべてのエラー e について、ちょうど 1 つの is_* 述語、すなわち e.kind にちなんだ名前のものが true を返します。
  • 検査付き・検査なしの法則。 immut と mutable のすべての組について、背景の節で述べた法則が成り立ちます。consistency とパッケージのテストが両方の経路を検査します。
  • 決定的なエラーの選択。 入力が複数の前提条件に違反する場合、検査は固定の順序で実行されるので、報告される種類は入力の関数になります。たとえば pow は指数の符号より先に正方性を検査するため、負の指数を持つ非正方行列は常に NonSquareMatrix を報告します。
  • 部分的な副作用がない。 不変パッケージの検査付き演算は引数を決して変更しません。可変パッケージの検査付き演算は書き込む前に検証するので、Err の結果は引数を変更しないまま残します。

却下した代替案

  • 文字列としてのエラーコード(コントリビューター向けメモで言及されている、E_ 接頭辞付きの古い慣例)。文字列は網羅的にマッチできず、解析を招きます。
  • すべての失敗に Option を使う。 理由が失われます。逆行列の None では、行列が非正方だったのか特異だったのかがわかりません。
  • パッケージごとに別々のエラー型。 immut、mutable、container をまたぐ合成で、境界ごとに変換が必要になります。

境界

error は値だけを定義します。行列アルゴリズム、特異な入力や悪条件の入力からの回復戦略、ログ出力、書式化は実装しません。どの操作を検査付きにするかも決めません。それは LinearAlgebraError を返す各パッケージの責任です。