ロバストネス原則(型のためのPostelの法則)
規範的(Normative)。
この文書は、Rigorが著作するすべての型 — 組み込みカタログエントリー、推論されたユーザーメソッドシグネチャ、またはRBS::Extendedペイロード — が守らなければならないロバストネス(堅牢性、頑健性、Robustness)原則を定義します。設計の根拠と未解決事項はdocs/adr/5-robustness-principle.mdにあります。
戻り値は健全性(soundness)を損なわずに証明できる限り厳密であるべきです(SHOULD)。パラメータは本体の正しい動作が許す限り寛容であるべきです(SHOULD)。
これは型システムにおけるPostelの法則(「ロバストネス原則」)の解釈です: 生み出すものについては保守的に、受け入れるものについては寛大に。Rigorに限定すると:
- 厳密な戻り値は推論エンジンが下流に伝播できる事実の精度を最大化します。
non-negative-intの戻り値は値に依存するすべての後続ナローイング(narrowing)チェーンを締め付けます;それをIntegerに広げることはすべての消費者にとって情報を捨てることになります。 - 寛容なパラメータは過度に厳格なシグネチャが呼び出し元に防衛的な型強制(
x.to_s、x || default、Array(x))をすべての呼び出し元に貼り付けることを強いるのを防ぎます。メソッド本体内部のナローイング層が実装が実際に必要とする精密な型を回復します。
両句はMUSTではなくSHOULDです: 原則は、複数の正確性を保持するキャリア(carrier)が利用可能なときのデフォルトの選択を指示します。正確性は常にどちらの句よりも優先します。
原則が適用される場所
Section titled “原則が適用される場所”原則はRigorが型を著作するすべての場所に拘束力を持ちます。すでに存在するRBSの著作をオーバーライドしません。
| 著作サーフェス(surface) | 原則の適用 | 注記 |
|---|---|---|
組み込みカタログ(data/builtins/ruby_core/*.yml) | はい — 戻り値層のみ | カタログはRBSの戻り値プロジェクションをオーバーライドします; RBS::Extendedアノテーションがない限り、パラメータプロジェクションはRBS駆動のままです。 |
| 推論されたユーザーメソッドシグネチャ | はい — 両方の層 | 呼び出し元で再型付けされます;推論された戻り値は本体の精密な格子値であり、推論されたパラメータは呼び出し元の型のユニオンです。 |
ユーザーが著作するRBS::Extendedアノテーション | はい — 両方の層 | %a{rigor:v1:return-refinement: …}は戻り値を締め付けられます(MAY);ケイパビリティ(capability)ロール / _ToFooパラメータアノテーションはパラメータを広げられます(MAY)。 |
手書きのRBSシグネチャ(ユーザー自身の.rbsファイル、rbs-inlineアノテーション) | いいえ | RBSの著作が拘束します。原則はユーザーが提供したシグネチャを暗黙に書き換えてはなりません(MUST NOT)。 |
rbs gemにバンドルされたベンダー済み / stdlib RBS | いいえ | 同様: それらのシグネチャは宣言された形状に拘束します。カタログ層はより厳密な戻り値をその上に重ねられますが、パラメータ型をオーバーライドしてはなりません(MUST NOT)。 |
厳密な戻り値: 第1句
Section titled “厳密な戻り値: 第1句”Rigorの既存のキャリアは、原則が解析器に使うよう指示する精度のツールです。精度の高い順:
| キャリア | 優先するとき | 例 |
|---|---|---|
Constant[v] | 結果が静的に決定される | 1 + 2 → Constant[3] |
Tuple[T1, …, Tn] | 固定アリティの異種コンテナ | 5.divmod(3) → Tuple[Constant[1], Constant[2]] |
HashShape{k: T, …} | 既知キーのシンボルキードレコード | {name: "A", age: 30} |
IntegerRange[a, b] | 有界整数 | Array#size → non-negative-int |
Union[T1, …, Tn] | 異なる可能性の有限で小さなセット | n.even? ? :even : :odd → `Constant[:even] |
Nominal[Class[args]] | 適用されたジェネリック引数を持つクラス | Array#map { String } → Array[String] |
Nominal[Class] | 生の名前的型(nominal type、公称型とも) | Object#dup → self(依然として名前的型) |
Dynamic[T] | 最終手段の漸進的(gradual)フォールバック | より具体的なものを証明できないとき |
第1句の候補は、実装が実際に返す値を誤って除外する場合には採用してはなりません(MUST NOT)。原則は「証明済みの限り厳密」であり、「想像できる限り厳密」ではありません。
具体的なパターン
Section titled “具体的なパターン”- コンテナクエリからの有界整数。
Array#size、String#length、Hash#size、Range#size、Set#sizeはNominal[Integer]ではなくnon-negative-intを返すべきです(SHOULD)。境界は構造的な真実(負のサイズはない)であり、すべての後続比較を通じて伝播します。 - イテレータブロックパラメータ。
Integer#times、Integer#upto、Integer#downto、Range#eachなどは、コンテナの要素型だけではなく、反復ドメインの精密なIntegerRangeにブロックのインデックスパラメータをバインドすべきです(SHOULD)。 - タプル形状の戻り値。 固定アリティの異種配列を返すメソッド(例:
Integer#divmod)は、多重代入先で各スロットの型がローカル変数に流れるようにTuple[…]として公開すべきです(SHOULD)。 - カタログ下の定数たたみ込み。 レシーバーと引数が具体的な定数である
:leaf/:trivial/:leaf_when_numericとして分類されたすべてのメソッドはConstantにたたみ込まれるべきです(SHOULD)。MethodCatalog層は最も広いメソッドサーフェスにわたって第1句を観察するツールチェーンです。
プラットフォーム移植性
Section titled “プラットフォーム移植性”厳密な証明済みの包絡線は、ユーザーのデプロイターゲットが異なる可能性があるとき解析器ホストのプラットフォームに依存する定数を除外します。File.basename、File.dirname、File.extname、File.join、File.split、File.absolute_path?はすべてFile::SEPARATOR / File::ALT_SEPARATORを読み、WindowsとPOSIXホストで異なる答えを生成します。デフォルトのRigorモードは推論された型が移植可能なままになるようにそれらをたたみ込むことを拒否します。
設定によるオプトイン(.rigor.ymlのfold_platform_specific_paths: true)は精度の利得のために移植性を犠牲にします。単一プラットフォームプロジェクト(ほとんどの内部ツール、サーバーオンリーデプロイメント)は有効にできます(MAY);デフォルトはオフで、macOSの開発者マシンで実行する解析器がWindowsのCIでも実行するプロジェクトにPOSIX固有のパスセマンティクスを暗黙に焼き込まないようにします。
予約済みのnon-empty-stringリファインメント(refinement、篩型とも)(imported-built-in-types.md参照)はパスメソッドの戻り値に対して計画されたプラットフォームに依存しない締め付けです。「結果は非空のString」を特定のセパレータにコミットせずに浮上させます。リファインメントインフラが出荷されるまで、パスメソッドはデフォルトモードでNominal[String]に着地します。
第1句が譲歩するとき
Section titled “第1句が譲歩するとき”- 契約(contract)はすべての呼び出し元のためであり、現在の呼び出しサイトのためではありません。シグネチャはすべての呼び出し元へのメソッドの約束を記述します;第1句は特定の呼び出しのためにメソッドのシグネチャを締め付けることを許可しません。その作業は
ConstantFoldingと呼び出しサイトごとのディスパッチャー層に属します。 - 本体が実際により広い型を返す。
Array#eachはselfを返し、ブロックの戻り値型を返しません。Object#tapはselfを返します。第1句は本体が一度も生成しない、より厳密な戻り値を宣伝することを許可しません。 - ユーザーがRBSシグネチャを提供した。 手書きまたはrbs-inlineで提供された戻り値型が拘束します;第1句はRigorがシグネチャを著作するときのみ動作します。
寛容なパラメータ: 第2句
Section titled “寛容なパラメータ: 第2句”パラメータの著作は最も広い正確性を保持するキャリアに手を伸ばします。寛容度の高い順:
| キャリア | 優先するとき | 例 |
|---|---|---|
ケイパビリティロール(_ReadableStream、_ToS …) | 本体がロールに挙げられたメソッドのみを使う | def write(stream) = stream.write(...) → _Writable |
構造的インターフェース(RBS interface _Foo) | 本体が小さな固定サーフェスを使う | def each(enum) → _Each[T] |
Union[T1, T2] | 本体が各ケースを明示的に処理する | def render(content) = content.nil? ? "" : content.to_s → `String |
Nominal[Superclass] | 本体がスーパークラスのメソッドのみを使う | def add(numeric) → Numeric、Integerではない |
Nominal[ExactClass] | 本体が本当にこのクラスを必要とする | def freeze_string(s) → Stringのみ |
第2句の候補は、本体が正しく処理できない値が本体に到達することを許す場合には採用してはなりません(MUST NOT)。原則は「本体が許す限り寛容」であり、「想像できる限り寛容」ではありません。
具体的なパターン
Section titled “具体的なパターン”- 名前的クラスよりもケイパビリティロール。 メソッドが引数に対して
:writeのみを使う場合、パラメータ型はIOではなく_Writable(構造的ロール)であるべきです(SHOULD)。これによりStringIO、Tempfile、カスタムモックオブジェクト、および将来のすべてのクラスが契約を満たせるようになります。 RBS::Extendedによる構造的インターフェース。 メソッドの引数が少数のメソッドセットを公開する必要がある場合、RBSのinterface _Foo宣言にパラメータへの%a{rigor:v1:conforms-to: _Foo}アノテーションを加えることが、具体的なクラスのユニオンを列挙するよりも好まれます。- 本体がチェックするときのnilableパラメータ。 本体に
return if x.nil?ガードまたは同等のナローイングがある場合、パラメータはTではなくT | nilであるべきです(SHOULD)。ナローイング層は本体内で精密な非nil型を回復します。 - 数値スーパータイプ。 引数に対して
+、-、*、<=>のみを使うメソッドは、テストスイートがたまたま整数のみを渡す場合でもIntegerではなくNumericでパラメータ型付けすべきです(SHOULD)。
回避策の増殖アンチパターン
Section titled “回避策の増殖アンチパターン”メソッドのパラメータが過度に厳格な場合、呼び出し元はすべての呼び出し元で回避策を貼り付けます:
# 過度に厳格: renderがStringのみを期待するrender(content || "")render(other.to_s)render(nullable_field.to_s)回避策は荷重を担うようになります — 後で置き換えることが難しくなるのは意図を隠すからです。第2句はデザイン時に寛容性に傾けることでこれを防ぎます。メソッド境界でわずかに広いパラメータ型のコストは、コードベース全体に渡る回避策の複合コストよりもはるかに小さいです。
第2句が譲歩するとき
Section titled “第2句が譲歩するとき”- 本体がより広い型を処理できない。
arg.bit_lengthを呼ぶメソッドはNumericを受け入れられません。なぜならFloat#bit_lengthは存在しないからです。パラメータはIntegerのままでなければなりません(MUST)。 - より広い型がプログラマーエラーをマスクする。 本体が
to_sのみを使うためにObjectを受け入れることは、nil.to_sが本体に静かに到達することを許してしまいます;契約が「非nilの文字列的な」ものなら、_ToS(nilを除外するロール)がObjectではなく正しい広げ方です。 - パラメータがユーザーが名前的として文書化した公開境界にある。 ユーザーのRBSシグネチャがパラメータを特定のクラスに拘束している場合、原則はオーバーライドしません。
他のルールとの相互作用
Section titled “他のルールとの相互作用”- RBSラウンドトリップ(ADR-1): RBS → Rigorは無損失のままです。Rigor → RBSは保守的な消去のままです。原則はRigorの著作の選択を指示しますが、境界契約を変更しません。
- サブタイピング(subtyping)対漸進的一貫性(relations-and-certainty.md): 第2句の広げ方はサブタイピングの動き(より寛容なパラメータはより厳格なものの上位型)です;
Dynamic[T]をtopに折り畳みません。 - 3値の確実性(relations-and-certainty.md): 第1句の厳密な戻り値型は、精度がちょうど境界にある場合でも
maybeの答えを生成できます。原則はmaybeが正しい場所でyes/noの答えを強制しません。 - ナローイング(control-flow-analysis.md): 第2句の広げ方はナローイング層と意図的にペアになります。広いパラメータはナローイングで回復された精密な本体を供給します — これら2つは別々ではなく一緒に設計されています。
- 消去(rbs-erasure.md): 厳密な戻り値はエクスポート時により広いRBS形式に消去される場合があります(MAY)。第1句は内部的に厳密なキャリアを生成します;消去はエクスポートルールが要求するものを提示します。
rigor type-ofによるユーザーのビューは厳密な形式を示します。 - 推論バジェット(inference-budgets.md): 第1句はエンジンの残りと同じバジェットで制限されます。無限計算を必要とする厳密な戻り値はバジェットに譲歩しなければなりません(MUST);原則は無制限の推論を認可しません。
- オーバーライドシグネチャチェック(ADR-35、v0.1.15で出荷): この原則が推論されたシグネチャを置換可能性へ偏らせるのに対し、
def.override-*ルールファミリーはプロジェクト定義の階層をまたいで著作されたものを検証します —def.override-return-widenedは第1句の戻り値共変性の対応物、def.override-param-narrowedは第2句のパラメータ反変性の対応物です。これらのルールはオーバーライドと影にされた祖先の両方が著者提供のシグネチャを持つときにのみ発火し(どちらかの側が推論のみなら沈黙を保つので、原則の著作上の選択そのものが決してフラグされることはありません)、重大度はseverity_profile:を通じてマップされます。
仕様レベルのまとめ
Section titled “仕様レベルのまとめ”| 位置 | 方向 | キャリアを選ぶ |
|---|---|---|
| 戻り値 | より厳密(第1句) | 最小の正確性を保持する値セット |
| パラメータ | より広い(第2句) | 最大の正確性を保持する値セット |
選択が曖昧な場合、解析器は候補を記録してカタログ著者またはルール著者がキュレーションできるようにすべきです(SHOULD)。選択が正確性を損なう場合、解析器は安全な(精度が低い / 寛容度が低い)オプションに譲歩しなければなりません(MUST)。
© 2026 TypedDuck. Licensed under CC BY-SA 4.0.