typestateで壊れた状態を書けなくする
Carinaの処理系は「処理が進むと値の型が変わる」typestateパターンを系統的に使っている。狙いは一貫していて、静的段階の内部不変条件を、実装言語の型でコンパイル時に固定することである。
適用例は複数の層に及ぶ。
- 未解決の列挙識別子と解決済み正規形は別の型であり、後者はリゾルバしか生成できない(carina#3438)。DSL綴りのまま比較に持ち込むことが型で書けなくなる。
- μ型の参照を展開した後の型ビューには参照変種が存在しない(carina#3349)。未展開のまま照合する状態がそもそも書けない。
- 効果の部分集合しか受理しない実行器には、絞り込み済みの型を渡す(carina#3164)。
- パーサの段階(パース直後 / 推論後)は型パラメータで区別される。
「覚えている」に依存しないこと
いずれの例も、対応する不変条件は「各消費地点が処理を忘れないこと」でも守れる。列挙値は比較の前に正規化する、μ型の参照は照合の前に展開する、と規約に書くこともできる。
しかし規約に依存する限り、同じバグクラスに未来の呼び出し側が再び到達できる。正規化を経ない比較経路が残っていたために列挙値が永遠に変更扱いされ続けた事故は、まさにその形で起きた。一箇所の消費地点が忘れたことが、差分という中心機能を静かに壊した。
typestateはこれを型の形に変える。壊れた状態を表す値が構成できないなら、忘れることが可能な地点がそもそも存在しない。「呼び出し側が覚えていること」に依存する不変条件は、型の形に作り直すまで根が残っている、という原則の実装である。
適用先の見当
この原則は未解決の問題にも指し示す先を持つ。代入で解決された値がスキーマで再検証されない問題(carina#3448)に対しては、deferredの穴に期待型を持たせ、穴を埋める唯一のAPIが「その型の検証器だけが生成できる検証済み値」を要求するようにすれば、検査を飛ばした代入が実装言語の型レベルで書けなくなる。検査を置くこと(信頼境界での再検証)と、検査を外せなくすること(typestate)は独立の手当てであり、併用が完成形である。
ここへリンクしているノート
- 同値関係を方向付き判定に流用すると抽象から具体への代入が漏れる
- 高階関数の境界条件は「関数値がgraph形状決定位置に到達しない」ことである
- 循環スキーマは名前付きμ型で明示展開する
- 属性の出所は型に昇格させるべきモダリティである
- 構造体を封印すると打ち間違いは捕まるがスキーマ進化に弱くなる
- 検査器が二系統あるのは設計負債である
- 型名の文字列比較で代入可能性を決めるのは構造を捨てている
- 部分型関係が公理化されていないと性質の破れを検出できない
- 等価判定は正規形へ簡約してから型ごとに行う
- 信頼境界を越えるデータは境界で再検証する
- 差分計算は命令選択である
- 「未確定」を表す表現が四つあるのは漸進的型付けの境界が引けていない徴候である
- 変性は書かれていないだけで、既に決まっている
- 変性は選ぶものではなく、位置が決めるものである
- 黙って捨てるのは、落とすことより悪い
- 契約を別ファイルに切り出すと必ずずれる
- 内容から導いた識別子は、内容を直した瞬間に静かに外れる
- 競合は観測では閉じない。順序づけでしか閉じない
- 遷移を実行する場所を一箇所に閉じ込める