ADR-5: Rigor型のロバストネス原則
ステータス: Accepted;実装・出荷済み。
ロバストネス原則(robustness principle、戻り値に厳密・パラメータに寛容)はRigorが著作するシグネチャと推論を支配する;規範的なコンパニオンはdocs/type-specification/robustness-principle.mdである。
ADR-5は、ポステルの法則——出力には厳格に、入力には寛大に——をRigorの型カタログと推論シグネチャの指針原則として採用した設計根拠を記録する。対応する規範的ドキュメントはdocs/type-specification/robustness-principle.mdである。両ドキュメントが観測可能な挙動で乖離した場合は、仕様が優先され、ADR-5を修正すべきである。
ADR-5はADR-1(型モデルとRBSスーパーセット戦略)とADR-3(内部型表現)の改訂である。新しいキャリア(carrier)は導入せず、あらゆるRigor型が交差する2つの境界(返り値とパラメータ)における既存キャリアの選択方法を調整する。
コンテキスト
Section titled “コンテキスト”ユーザーへのRigorの価値は、計算される型の精度と呼び出しサイトに導入される摩擦という2つの要素の積である。この2つは相反する。
- 返り値位置における厳格なシグネチャは、より有用な事実を下流に伝播させる。
Integer#absがnon-negative-intとして型付けされると、次のif abs > 0が正確にナローイング(narrowing)できる。同じメソッドがIntegerとして型付けされると、下流のすべての比較がfalse | trueに落ちてしまう。 - パラメータ位置における寛大なシグネチャは、型チェッカーが要求する正確な形状を満たすためだけの変換、防衛的コピー、ラッパーコアーションを呼び出し側に強いることを避ける。実際には
_ToStrを受け入れるのにStringを要求するメソッドは、呼び出し側が.to_sをあちこちに貼り付けなくて済むよう_ToStrを宣言すべきである。
RigorはすでにRBSで表現できる以上の精度で型を推論している(例:IntegerRange[1, 10]、Tuple[Constant<Integer>, ...]、HashShape{name: "Alice", age: 30})。推論された返り値が厳格であるほど、推論エンジンがユーザーのためにより有用な仕事をする。逆に、パラメータ位置はユーザーが制御する呼び出しサイトにあり、解析器は呼び出し側を書き換えられない——過度に厳格なパラメータ型は、呼び出し側が冗長な変換でごまかすノイズを生む。
このADRは非対称性を明示することで、将来のカタログ作業(数値、文字列、配列、ハッシュ、…)と将来の推論ルールが常にあらゆるシグネチャ境界で同じ方向に偏るようにする。
- 非対称性をケースバイケースの判断ではなく、規範的な原則として文書化する。
- カタログ作成者と推論ルール作成者を同じ標準に従わせ、カタログが成長しても一貫性を保つ。
- 「Rigorの返り値型がRBSの宣言よりも具体的に見えるのはなぜか」「Rigorが渡したばかりの形式にパラメータをナローイングしなかったのはなぜか」についてユーザーに単一のメンタルモデルを提供する。
- 既存のRBS相互運用性ルールをすべて保持する。RigorはRBSラウンドトリップを壊すような形で返り値を広げたり、パラメータを狭めたりしてはならない(MUST NOT)。
- パラメータチェックを無関係な型を受け入れるという意味で寛大にすること。この原則は安全性を弱めない。名前的のみの契約(contract)より構造的・ケイパビリティ(capability)に基づくパラメータ契約を優先し、RBSで広げた返り値よりも精度の高い返り値ファセットを優先する。
- すべての推論返り値型を改良形にすることを義務付けること。Rigorの既存キャリア(
Constant、IntegerRange、Tuple、HashShape、Union)が精度ツールであり、この原則は新しいものを導入しない。 - 格子演算(サブタイピング(subtyping)、漸進的一貫性、ナローイング)を変えること。それらのルールはADR-1と
docs/type-specification/以下の仕様にある。
作業上の決定
Section titled “作業上の決定”ポステルの法則をRigor型のデフォルト非対称設計ルールとして採用する。
- 返り値は解析器が証明できる最も精度の高いキャリアであるべきである(SHOULD)。組み込みカタログエントリー、推論されたユーザーメソッドボディ、または
RBS::Extendedアノテーションが偽陽性なしにより厳格な返り値を表現できる場合、RBS宣言された形状よりもそちらを優先する。 - パラメータは解析器が正確性を正当化できる最も寛大なキャリアであるべきである(SHOULD)。パラメータ契約が構造的インターフェース、ケイパビリティロール、またはより広いユニオン(union、合併型とも)で呼び出しサイトの安全性を弱めることなく満たされる場合、名前的のみの記法よりもそちらを優先する。
両節はSHOULDであり、MUSTではない。正確性は常にこの原則よりも優先される(ALWAYS)。健全性(soundness)を損なわずに証明できない返り値型は、節1を満たすためだけに厳しくしてはならない(MUST NOT)。特定のクラスを要求することが真に必要なパラメータ型は、節2を満たすためだけに広げてはならない(MUST NOT)。
この原則は3箇所で観察される。
- 組み込みカタログ生成(
tool/extract_builtin_catalog.rb): メソッドごとのpurityと効果ファセットが折り畳みに参加するメソッドを制御する。結果の返り値型は折り畳みが証明できる最も精度の高い格子値である。カタログパラメータの広げ(節2)は基礎となるRBSシグネチャに委譲される。カタログエントリーは返り値ティアのみをオーバーライドし、パラメータ型付けはRBS::Extendedアノテーションがオーバーライドしない限りRBSの責任である。 - ユーザーメソッド推論: 解析器は、呼び出しの引数型をパラメータにバインドして、呼び出しサイトでユーザーメソッドのボディを再型付けする。推論された返り値はボディの精確な格子値であり、推論されたパラメータ契約は呼び出し側が実際に渡すものだ(呼び出しサイト型のユニオン)。ユーザーがRBSシグネチャを提供している場合、そのシグネチャが両方向で拘束する——節1と節2は既存のシグネチャをオーバーライドするためではなく、新しいシグネチャに適用される。
RBS::Extendedアノテーション作成: アノテーションがより厳密な返り値ファセット(%a{rigor:v1:return-refinement: …})を持つ場合、RBS宣言の返り値よりも優先される。アノテーションがより緩いパラメータファセット(名前的なStringの代わりにケイパビリティロールや_ToSインターフェース)を持つ場合、RBS宣言のパラメータよりも優先される。
厳格な返り値が精度を伝播させる
Section titled “厳格な返り値が精度を伝播させる”Rigorの推論エンジンはキャリアを呼び出しチェーン全体にスレッドする。エンジンがIntegerをIntegerRange[1, 10]に置換できるところはどこでも、次の述語、比較、または算術ノードがより有用な答えを見る。
n = items.size # non-negative-int (not Integer)if n > 0 middle = n / 2 # non-negative-int / Constant[2] -> non-negative-int ...endArray#sizeがIntegerとして宣言されていたなら、n > 0もn / 2もバウンドの恩恵を受けられない。下流の損失は複合する。すべての比較、すべてのループバウンド、受信側の事実に依存するすべてのif size.zero?が精度を失う。単一の厳格な返り値が、下流の何十もの冗長な防衛的チェックを置き換える。
PHPStanの経験も同じだ。リテラル型の返り値(array{name: string, age: int})はコードベース全体に流れ、境界ではなく消費サイトで正確なエラーを表面化させる。
寛大なパラメータがコアーションノイズを避ける
Section titled “寛大なパラメータがコアーションノイズを避ける”逆に、パブリック境界での過度に厳格なパラメータ型は、すべての呼び出し側に税を課す。def render(content: String)と宣言されたメソッドがString | nilの呼び出し側を拒否する場合——たとえメソッドが内部でnilをガードしていても——呼び出し側はrender(content || "")と書かざるを得ない。この回避策はコードベース全体に広がり、意図を隠し、ラッパー式が呼び出し側の実際のセマンティクスからずれるにつれてメンテナンス負荷を生み出す。
適切なツールは構造的インターフェースまたはケイパビリティロールだ。_ToStr、_ConvertibleToString、またはメソッドがnilを処理する場合は単純にString | nil。呼び出しサイトでの解析器の仕事は、引数がメソッドで使用可能かどうかを検証することであって、特定の名前的キャリアを持っているかどうかではない。
これは安全性を下げることとは異なる。メソッドのボディは依然として、より広いパラメータ型の下で正しくなければならない——そして、Rigorのナローイングティアはまさにボディが必要な精確な型を回復できるメカニズムだ。
def render(content) # parameter: String | nil return "" if content.nil? # narrowed to String inside the body content.upcase # safe: receiver is String hereend回避策増殖アンチパターンを避ける
Section titled “回避策増殖アンチパターンを避ける”過度に厳格な静的解析での一般的な失敗モード: パラメータ境界での単一の偽陽性が、呼び出しサイトごとに回避策をコピーペーストするようユーザーを誘導する(x.to_s、x || default、T.cast(x, U))。回避策はその後、どれが本物の変換でどれが仮置きだったかを誰も覚えていないため、後で取り除くのが難しい重要部分になる。原則の節2は、呼び出しサイトでのコアーションの積み重ねではなく、設計時にパラメータを広げる方向に偏ることで、このアンチパターンを防ぐ。
境界ケース: RBSラウンドトリップ
Section titled “境界ケース: RBSラウンドトリップ”この原則はADR-1のRBSラウンドトリップルールを変更しない。
- RBS → Rigorはロスレスのまま。ユーザー提供のRBSシグネチャが拘束する。Rigorはこの原則を適用するためにパラメータを広げたり、返り値を厳格にナローイングしたりしてはならない(MUST NOT)。
- Rigor → RBSはエクスポート時に保守的に消去するまま。精確に推論された返り値はエクスポート時に広いRBS形式に消去されてもよい(MAY)。この原則はより厳格なエクスポートを強制しない。
したがって、この原則は解析器がシグネチャの作成権を持つ場合のデフォルトのみを設定する。組み込みカタログエントリー、RBSシグネチャが存在しない場合に推論されたユーザーメソッド型、およびRBS::Extendedペイロードにおいてである。既存のRBS作成者が記述したものは尊重される。
プラットフォームホストの正確性が精度に勝る
Section titled “プラットフォームホストの正確性が精度に勝る”節1は解析器を最も厳格な正確性保持キャリアに向ける。「正確性保持」には、解析器ホストの答えだけでなく、ユーザーのデプロイ対象が観察する答えを保持することが含まれる。
Fileのパス操作メソッド(basename、dirname、extname、join、split、absolute_path?)はFile::SEPARATOR/File::ALT_SEPARATORを読み取り、WindowsとPOSIXホストで異なる答えを生成する。
File.basename("a\\b.rb") # "a\\b.rb" on POSIX, "b.rb" on WindowsFile.absolute_path?("/foo") # true on POSIX, false on Windows (no drive letter)解析器を実行しているRubyプロセスはONEプラットフォームをホストする。Constant<String>に折り畳むと、解析器ホストの答えが推論型に黙って焼き付けられ、異なるセパレータポリシーのホストで誤って報告される。したがって節1の「証明できる限り厳格に」は、デフォルトではプラットフォーム固有のConstantを除外する——プラットフォーム非依存エンベロープ(Nominal[String]/Tuple[Nominal[String], Nominal[String]]/bool)が最も厳格な正確性保持結果である。
単一プラットフォームプロジェクト(ほとんどの内部ツール、Linuxコンテナにデプロイされたアプリ、開発者マシンのみで実行されるスクリプト)は設定でオプトインできる。
fold_platform_specific_paths: trueこのオプトインはプラットフォーム移植性と精度の恩恵をトレードオフする。デフォルトはそのトレードオフを拒否し、解析器がデプロイ対象が見ない答えを生成することなく任意のホストで安全に使用できるようにする。
将来のnon-empty-string改良キャリア(imported-built-in-types.mdを参照)は、プラットフォーム固有の情報を漏らすことなくパスメソッドの返り値を厳しくする。File.basename(p)の空でないパスは、セパレータに関係なく常に空でない。今日このキャリアは文書化されているが未実装であり、そのインフラが存在するまでプラットフォーム非依存のデフォルトはNominal[String]のままである。
正確性は依然として優先される
Section titled “正確性は依然として優先される”節1と正確性が矛盾する場合、正確性が優先される。例:
Integer#==(Object) -> boolは、解析器が特定の呼び出し5 == 5をConstant[true]に折り畳めるとしても、そのように厳しくしてはならない(MUST NOT)。シグネチャはすべての呼び出し側のメソッド契約を記述し、折り畳みはConstantFoldingを通じて呼び出しサイトを厳しくする。Array#eachはブロックの返り値型を配列の要素型として広告してはならない(MUST NOT)——ブロックは副作用のために実行され、返り値は受信側自体である。Cボディが実際に返すものを厳格に返すことが原則の求めであり、想像できる最も具体的なファセットを厳格に返すことではない。
節2と正確性が矛盾する場合、正確性が優先される。例:
Integer#bit_length() -> non_negative_intは、受信側をNumericに広げてはならない(MUST NOT)——bit_lengthは真にInteger専用だ。Numericに広げると1.5.bit_lengthが型チェックを通り、実行時に失敗する。self.@dataをミューテートするメソッドは、@dataスロットのない構造的インターフェースに受信側を広げることができない。
この原則は、複数の正確性保持オプションが存在する場合のデフォルト選択を誘導する。どちらの方向でも正確性を弱める許可ではない。
この原則は既存のコードベースで観察できる。
MethodDispatcher::ShapeDispatchは、RBS宣言の返り値がIntegerであってもArray#size、String#length、Hash#size、Range#size、Set#sizeに対してnon_negative_intを返す——節1。MethodDispatcher::IteratorDispatchは、5.times { |i| ... }のブロックパラメータをIntegerではなくint<0, 4>として型付けし、精確な反復ドメインをボディに伝播させる——節1。ConstantFoldingは、重複排除結果がUNION_FOLD_OUTPUT_LIMITを超えるとnilを返すのではなく、デカルトユニオン折り畳みをIntegerRange[min, max]に広げる——節1(精度保持のグレースフルデグラデーション)。Type::IntegerRangeは、呼び出し側が最初にIntegerに広げることを強制せずにConstant[n]とより狭いIntegerRange値を受け入れる——節2。- 将来のケイパビリティロールカタログ(
_ReadableStream、_RewindableStream、…)とdocs/type-specification/structural-interfaces-and-object-shapes.mdの構造的形状ルールは、ユーザー定義メソッド境界での節2の専用ツールである。
未解決の疑問
Section titled “未解決の疑問”- ユーザー提供のパラメータが名前的に型付けされているが、すべての呼び出しサイトが構造的インターフェース互換の値を渡す場合、診断サーフェス(surface)が提案を報告すべきか? これはエラーではなく節2のアドバイザリーになる。
- この原則は
RBS::Extendedケイパビリティロール適合性(%a{rigor:v1:conforms-to: _Frobbable})とどのように相互作用すべきか? 適合性アノテーションは節2を有効にするツールだが、未解決の疑問は、解析器がユーザーメソッドのパラメータ型を推論するときに自動生成すべきかどうかだ。 - カタログジェネレータは厳格に推論された返り値形式とRBS宣言の返り値の両方を出力して、消費側が精度の差分を見られるようにすべきか? 今日は推論された形式のみが記録される。
これらはv0.1.0以降に延期されており、関連するスライス(slice)と並行してトラッキングされている。
- ADR-1: 型モデルとRBSスーパーセット戦略 — ADR-5が改訂するRBSラウンドトリップルールを確立する。
- ADR-3: 内部型表現 — ADR-5が選択するキャリア(
Constant、Nominal、Union、Tuple、HashShape、IntegerRange)を定義する。 - ADR-4: 型推論エンジン — ユーザーメソッド型を推論するときにこの原則を観察するエンジン。
背景となる研究ノート
Section titled “背景となる研究ノート”docs/notes/20260518-matsumoto-2008-poly-records-rigor-review.md— 松本&南出2008は逆の非対称性を採用している。負の位置(引数)はシグネチャのみだが、正の位置(返り値)にはRuby実装も許容する。「呼び出し側の生活を楽にする」という同じ本能、しかし返り値側のスタンスは逆である。「なぜこの非対称性であって、もう一方ではないのか?」という根拠セクションが暗黙のままにしている問いに対する有用な対比材料。docs/notes/20260518-matsumoto-2010-cfa-rigor-review.md— 松本&南出2010はSemiRubyの健全性を証明している。Rigorは意図的に同レベルの形式的健全性を目指していない。このノートは、同著者による設計の系譜(2008年論文→2010年論文→Steep→現在)を記録しており、Rigorのロバストネス優先のスタンスを逸脱ではなく継続として正当化する。
© 2026 TypedDuck. Licensed under CC BY-SA 4.0.