hom 設計
目標
MoonBit の現在の型システムの中で構造を保つ写像を表現し、型システムが証明できない法則を開発者に委ねつつ、その義務を監査可能かつ検査可能に保つこと。
制約
- MoonBit の trait は
Selfしか引数を持たず、多引数 trait も関連型もないため、準同型A -> Bを trait として書けません。 - ℕ と ℤ からの準同型は一意(始対象)なので、
FromNatとFromIntegerは対象側の trait として置けます。それ以外の準同型は一般に一意ではなく、値として扱う必要があります。
中核となる設計判断
- LCF 流の証明書:
Hom[S, A, B]のフィールドは非公開で、このパッケージの中でのみ構築されます。 - 公開された信頼の入口は
Hom::postulateだけです。カーネルの規則はパッケージ内部のtrustを使うため、postulateを検索すると利用者側の義務だけが正確に列挙されます。(assumeは MoonBit の予約語のため使いません。)Section::postulateは切断の信頼の入口であり、標準のHom::from_integerとSection::of_integralはもう一方の葉で、その義務は trait インスタンスにあります。 - 商から被覆代数へ戻る持ち上げは
HomではなくSectionです。射影をHomとして持ち、proj(lift(q)) == qだけを約束します。代表元上で演算と一致することはこの法則から従います。両者を分けることで、Int -> BigIntのような代表元の持ち上げが準同型であるかのように合成されるのを防ぎます。 - シグネチャは証明書の幽霊型
Sとして現れ、代数は辞書値Algebra[S, A]として渡されます。証明書は何を保つかを述べ、辞書は検査に使われます。 - シグネチャ間の包含は、このパッケージだけが作れる
Reduct[S, T]の証人で表します。 - 保存の強さは検査時に選ぶ関係
relで決まり、厳密・lax・近似の準同型が一つの API を共有します。
切断
hom チュートリアルでは、この数学に触れずに Section を使います。
定義
π : A -> Q を全射準同型とします(例: 簡約 BigInt -> Int)。切断とは、すべての q について π(s(q)) = q を満たす写像 s : Q -> A で、各類 π⁻¹(q) から元を一つ選びます。第一同型定理により Q は商 A / ker π なので、切断とは商代数の代表元の選び方のことです。
切断が保存するもの
すべての演算 ω と引数 x について:
s(ω(x))とω(s(x))はker πを法として合同です。両方にπを適用すると、切断の法則によりπ(s(ω(x))) = ω(x)、πが準同型なのでπ(ω(s(x))) = ω(π(s(x))) = ω(x)です。s(ω(x)) = ω(s(x))となるのは、ω(s(x))がsの像に含まれるとき、かつそのときに限ります。ω(s(x)) = s(y)なら、同じ計算によりy = π(s(y)) = π(ω(s(x))) = ω(x)なのでω(s(x)) = s(ω(x))です。逆にs(ω(x))は常に像に含まれます。
同じ二段階を数式で書きます。 項演算 と について、 とします:
したがって に減法があれば です。ある について なら、 を適用して 、よって です。
したがって Section::check は切断の法則だけを検査し、check_ops は持ち上げた引数の上で π が準同型であることを検査します。第 2 点はそこから従います。Int では s の像は [-2^31, 2^31) であり、「結果が像に含まれる」とは「結果が回り込んでいない」ということです。
桁上がり
Int の加算では、第 1 点の差は s(a) + s(b) - s(a + b) = c(a, b)·2^32 で、c(a, b) ∈ {-1, 0, 1} は桁上がりです。s(a) + s(b) + s(e) を二通りに展開すると次を得ます
c(a, b) + c(a + b, e) = c(b, e) + c(a, b + e)
この恒等式は結合法則から来ます。、 と書き、三つの持ち上げの和を二通りにまとめます:
両辺は ℤ で等しいので、 の係数は一致します。 の範囲は代表元の範囲から従います。、 なので、 は と の間に厳密に収まり、 です。
したがって c は 2-コサイクルで、ℤ を ℤ/2^32 の 2^32ℤ による拡大として記述します。ℤ には有限位数の元がないのでこの拡大は分裂せず、どう代表元を選んでも s は準同型になりません。直接示すと、加法的な切断 があれば となり ですが、これは に矛盾します。乗法では、ずれは積の上位ワードです。
正規形
n = s ∘ π : A -> A は正規形です: n(n(a)) = n(a)、a と n(a) は合同、そして a と b が合同であることと n(a) = n(b) は同値です。商の演算は代表元の上で s(ω_Q(x)) = n(ω_A(s(x))) として計算されます。つまり A で計算してから正規化します。回り込む Int の演算は A = ℤ の場合のこの計算です。Section::normalize は s と π から構成されるので、合同な二つの値に異なる正規形を与えることはありません。
Section が Hom ではない理由
切断は単射であり、その像の上では演算と一致するため、準同型と取り違えて合成してしまいがちです。別の証明書に分けることで違いが型に現れます: Section は π を Hom として持ち、π(s(q)) = q だけを約束します。
法則はどの代表元を選ぶかを決めません。[0, 2^32) と [-2^31, 2^31) はどちらも BigInt -> Int の切断を与えますが、Int の符号付きの順序を保つのは後者だけです。このような性質は別途検査が必要です。
採用しなかった代替案
- 型が実装する準同型 trait
Hom[A, B]: 二つの型パラメータが必要ですが MoonBit の trait にはありません。また型の組ごとに準同型を一つしか許しませんが、組には普通いくつもあります。たとえば複素数上の恒等写像と共役です。 - 証明書のない普通の関数
(A) -> B: 検査済みの準同型と任意の変換を区別できず、合成しても責務の出所が記録されません。 Int -> BigIntのような代表元の持ち上げを準同型として扱うこと: 上のコサイクルの議論からそうではないとわかるので、専用の証明書Sectionを持ちます。- 検査の関係(厳密、緩い、許容誤差)を型に記録すること: 推論規則が強さごとに増えてしまいます。代わりに関係は検査時に選び、その代償は下の境界で述べます。
境界
- 単一ソートのシグネチャのみを扱います。加群(スカラーとベクトル)のような多ソート構造はこのサブシステムの範囲外です。
- 高階カインドがないため、関手的な持ち上げ(多項式、行列など)はそれぞれのパッケージが提供し、ここでは一般化しません。
- 法則は証明されず、検査されるだけです。証明書が保証するのは由来の追跡可能性であり、法則が成り立つことではありません。
thenの健全性は、台集合ごとにS-代数が一つだけであることに依存します。組み込みのタグは trait の一貫性によってこれを満たしますが、Algebra::makeの辞書は規約によるだけです。- 証明書は
check_byで使った関係を記録しないため、lax な写像や近似的な写像も厳密な準同型であるかのように合成されます。 - 固定幅の整数は ℤ や ℕ ではなく ℤ/2^k です。そこから ℤ への写像は切断なので、演算と一致するのは回り込みが起きない間だけです。
- 切断の法則はどの代表元を選ぶかを決めないため、順序の保存のような性質は別途検査する必要があります。