コンテンツにスキップ

付録 — TypeProfから来た場合

TypeProfはRubyの公式な型推論ツール — Rubyコア内でメンテナンスされている型レベルの抽象インタープリタであり、アノテートされていない.rbファイルを読んで推論した型を教えてくれる。TypeProfを使ったことがある場合、最も重要なことはRigorとTypeProfがTypeProfの目玉となる約束を共有しているということ。どちらもまず.rbsを書くことを要求しない。Steepの付録が「両者とも同じRBSを消費する」で始まるのに対し、こちらはその逆 — 両者ともプレーンなRubyから型情報を生成するで始まる。興味深い違いは、どのように推論するか、そして結果をどう扱うかにある。

この付録は既にTypeProfの語彙で考えており、TypeProfのどの概念がRigorのどの概念に対応するかを知りたいユーザー向け。

この付録の内容 5秒ピッチ · 両者ともアノテーションなしで推論する · 型語彙 · 解析モデル · RBS生成vssig-gen · 推論の燃料としてのテスト · 診断: 副作用vs製品 · 起動 · TypeProfにあってRigorにないもの · RigorにあってTypeProfにないもの · 共存パターン · マイグレーションvignette

問いTypeProfRigor
主な仕事RubyからRBSプロトタイプを生成する証明可能なバグについてRubyをチェックする(rigor check
開始に.rbsが必要か?いいえ — .rbから推論するいいえ — .rbから推論する
推論戦略エントリーポイントからのプログラム全体の抽象解釈(「型レベル実行」)バジェット付きのローカルなメソッドごと推論+境界でのカタログ
スケールターゲット小さなファイル/プロトタイピングパスコードベース全体、インクリメンタル、キャッシュ
デフォルト出力RBSシグネチャ(+副作用としていくつかのエラー)診断(+sig-gen経由でオンデマンドのRBS)
診断の哲学抽象解釈がつまずいたものを報告するバグが証明可能な場合のみ沈黙を破る
リテラル精度出力で名前的型に拡大する(1IntegerConstant<1>、リファインメント(refinement、篩型とも)、IntegerRangeを保持する

TypeProfのスローガンが「Rubyを型レベルで実行して返ってきたものを書き留める」なら、Rigorのは「証明できるものを証明し、それだけにフラグを立て、スケールさせる」。ふたつが最も正確に重なるのはひとつの機能 — rigor sig-gen(第11章)はTypeProfのCLIが作られた仕事をする。

両者ともアノテーションなしで推論する — それが共通の基盤

Section titled “両者ともアノテーションなしで推論する — それが共通の基盤”

これが見出し。TypeProfとRigorは、.rbsファイルがゼロlib/ディレクトリで有用なものを与えてくれるふたつのRubyツール。対照的にSteepはシグネチャを前もって要求する。だからTypeProfからRigorに来たなら、「アノテーション不要」のスタンス(第1章)は既に馴染みあるものに感じられるはず — それはあなたがずっと前提にしてきた仮定なのだ。

# slug.rb — no sig/ directory anywhere
class Slug
def normalise(raw)
raw.strip.downcase
end
end

TypeProfはnormaliseを抽象的に解釈し、String#stripに続いてString#downcaseを見て、(Stringを渡す呼び出しサイトがあれば)def normalise: (String) -> Stringを出力する。Rigorは同じボディをローカルに推論し、sig-genの下で同じシグネチャを出力する — 同時に内部的には結果がnon-empty?制約のない小文字化された文字列キャリア(carrier)であることを知っており、それを境界でStringに消去する。

両者が分岐する箇所:

  • TypeProfは呼び出しグラフをたどる。rawStringであることを知るには、それを渡す呼び出しを見る必要がある — そのためTypeProfはエントリーポイント(またはメソッドを動かす小さなハーネス)を持つプログラムに向けたときに最も有用。
  • Rigorは各メソッドをそれ自身の条件でコア/stdlib/gemの型のカタログに対して推論し、推論バジェットで境界づけられ、特定できないパラメータについてはDynamic[Top]にフォールバックする — エントリーポイントもハーネスも不要。

型語彙 — TypeProf出力vs Rigorキャリア

Section titled “型語彙 — TypeProf出力vs Rigorキャリア”

TypeProfはRBSを出力するので、その出力語彙はRBS。RigorはそれをRBSと同じものに読み込み消去する。違いは内部精度にある。TypeProfは出力時にリテラルを拡大するが、Rigorはより豊かなキャリアを保持し境界でのみ消去する。

Ruby式TypeProf出力Rigor内部(表示)
1IntegerConstant<1>(消去: Integer
"foo".upcaseStringConstant<"FOO">(消去: String
[1, "a"][Integer, String]Tuple[Constant<1>, Constant<"a">]
{name: "x", age: 1}{name: String, age: Integer}HashShape{name: …, age: …}
x不明untypedDynamic[Top](表示: untyped
nilまたはIntegerInteger?`Integer
> 0でガードされたintIntegerpositive-intリファインメント(消去: Integer

実践的な含意: 同じファイルを両方に与えると、TypeProfのRBSとrigor sig-genのRBSは通常宣言レベルで一致する。なぜならRigorのRBS消去はTypeProfが決して追跡しなかった余分な精度(ConstantRefinedIntegerRange)をまさに意図的に捨てるから。解析器の内部では、Rigorはより多くを運んでいる — それがリファインメントキャリアによるナローイング(narrowing)と定数foldingの診断を駆動する。(TypeProfもis_a?nil?のような型同一性述語に対しては独自のフローセンシティブ(flow-sensitive)なナローイングを行う。運んでいないのは値述語によるリファインメントの層と、Rigorがそこから生成するキュレートされた診断のほうだ。)

解析モデル — 最大の概念的な違い

Section titled “解析モデル — 最大の概念的な違い”

ここでツールは本当に分かれる。

TypeProfはプログラム全体の抽象インタープリタ。エントリーポイントからプログラムを歩き、型レベルで実行する。すべてのメソッド呼び出しが抽象引数型を呼び出し先に伝播し、呼び出し先のボディが解釈され、戻り型が逆流する。これは手続き間(inter-procedural)であり、小さなプログラムでは驚くほど精密 — TypeProfはメソッドのパラメータ型を、それがどう呼ばれるかだけから推論できる。これはSteepもRigorも試みない。

そのコストはスケール。プログラム全体の抽象解釈は高コストで、解析は大規模または高度に多相なコードベースで組み合わせ爆発しうる。TypeProfは明示的に、小さなプログラム、単一ファイル、または出発点となるRBSパスのためのツールとして位置づけられている — 保存のたびに10万行のアプリに対して実行するチェッカーではない。

Rigorは境界にカタログを持つローカルでバジェット付きの推論器。一度に1メソッドを推論する。そのメソッドが別のメソッドを呼ぶとき、Rigorは呼び出し先のボディを再解釈するのではなく、既知の戻り型のカタログ(コア、stdlib、gemのRBS、プラグイン貢献)を参照する。解決できない呼び出しはDynamic[Top]になりそこで止まる。これは自己完結したおもちゃのプログラムではTypeProfより精密でない — RigorはTypeProfができるように単一の呼び出しサイトからパラメータ型を推論しない — が、推論バジェットで境界づけられ、ファイルごとのキャッシュ(ADR-6)に支えられているため、コードベース全体や実行をまたいでも使い続けられる。

TypeProfRigor
解析の単位エントリーポイントからのプログラム全体一度に1メソッド
メソッド間の型呼び出し先のボディを再解釈するカタログで呼び出し先を引く
呼び出しサイトからパラメータを推論するか?はい(その看板の動き)いいえ(パラメータはデフォルトでDynamic[Top]
境界づけられているか?実践的な爆発の限界で明示的な推論バジェットで
インクリメンタル/キャッシュ?TypeProf 2/--lspで改善実行とマシンをまたぐファイルごとのキャッシュ

トレードは現実的で意図的。TypeProfはプログラム全体の解釈で小さな入力に対する精度を買い、Rigorはローカルな推論とカタログでスケールと沈黙を買う。

RBS生成 — typeprof CLI vs rigor sig-gen

Section titled “RBS生成 — typeprof CLI vs rigor sig-gen”

これはツールが同じ仕事をするひとつの機能であり、直接比較する価値がある。RubyからRBSを生成することはTypeProfの存在理由そのものだが、Rigorにとってはひとつの二次的なコマンド、第11章である。

typeprof foo.rbrigor sig-gen
出力stdoutへのRBS--print--diff--write経由のRBS
パラメータ型呼び出しサイトから推論保守的、--paramsポリシー、ADR-5のトレードオフ
既存のsigゼロから再生成new-filenew-methodtighter-returnの分類、人間が締め付けた戻りを決して上書きしない
出力のリテラル精度名前的型に拡大RBSに消去(境界で同じ拡大)
フィードバックする駆動シグナル手編集する一回限りのプロトタイプsig-genのギャップそのものがRigorがより良く推論すべきシグナル

このリポジトリ特有の注記: プロジェクトの常設ポリシー(AGENTS.md § “RBS Authorship”)は、手書きまたはAI生成のRBSよりもrigor sig-genを優先することである。まさにsig-gen出力のギャップが、パッチで塞いだシグネチャよりも価値あるフィードバックだから。typeprofを実行してその出力をsig/に貼り付けることに慣れているなら、Rigorの対応物はrigor sig-gen --diff — ただしマインドセットは「足場を組んでから編集する」から「足場が間違っているなら推論を直す」へと移る。

推論の燃料としてのテスト — 双方向の問い

Section titled “推論の燃料としてのテスト — 双方向の問い”

解析モデルのセクションの自然な続き: TypeProfが呼び出しサイトからパラメータ型を学ぶなら、そしてテストが呼び出しサイトそのものなら、ふたつのツールはあなたのspec/(またはtest/)スイートをどう使うのか。本当はふたつの方向があり、ツールは各方向で異なる。

テストはメソッドがどう呼ばれるかの具体例の山なので、パラメータ型の証拠になる。ここで解析モデルが最も鋭く透けて見える。

  • TypeProfは呼び出しサイトを燃料にする — そしてテストはそのうちの一種にすぎない。TypeProfに「テスト」という概念はない。プログラム全体を抽象的に解釈してパラメータを推論するので、見えるあらゆる呼び出しがパラメータ型を供給する: トップレベルのコード、__END__下の例、使い捨てのドライバ、specの1行 — TypeProfにとってはすべて同じ。Foo.new.bar(42)を実行する呼び出しが — どこにあろうと — TypeProfがbarIntegerを取ると学ぶ手段であり、そうした呼び出しがなければパラメータはuntypedのまま。テストはたまたま呼び出しサイトの豊かな供給源であり(だからスイートを含む、コードを動かすものにTypeProfを向けると役立つ)、TypeProfはそれらをテストとして認識も特別扱いもしない。
  • Rigorはrigor checkのためにテストを読まない。そのローカルモデルはナローイングされていないパラメータをDynamic[Top]のままにする。バグ発見のゲートはそれらを締め付けるためにspec/を決して参照しない。テストがパラメータシグナルになるのは、オプトインのrigor sig-gen --params=observed --observe=spec/パス(第11章)に限られ、そこでは位置ごとに観測された引数型をすべての呼び出しサイトにわたってユニオン化する。

つまり対比は鋭い: TypeProfはspecを通常のRubyとして解釈し(itブロックはただの呼び出しサイトが増えるだけ)、対してRigorのsig-genコレクターはRSpec DSLを構造的にモデル化する — described_classsubjectletはただ実行されるのではなく、バインディングとして認識される:

RSpec.describe Calc do
subject { Calc.new } # :subject → Nominal[Calc]
let(:other) { Calc.new } # :other → Nominal[Calc]
it { subject.greet("Alice") } # observed: Calc#greet receives String
it { described_class.new.add(1, 2) } # observed: Calc#add receives Integer, Integer
end

逆の流れ: シグネチャがsig/に着地すると、spec自体をチェック可能にする。rigor check(およびrigor-rspecプラグイン)はsubjectletのボディを実際の戻り型に対して型付けし、マッチャーをチェックする。これが次のsig-gen実行を鋭くする — 本物のループ。(ふたつのRSpecマシンが別物であることに注意: sig-genのコレクターは組み込みでプラグインを必要としない。rigor-rspecは独立した診断解析器。両者は並んで実行され、共有されない。)

観測から導出されたパラメータはほぼ常に狭すぎる。スイートでStringでしか動かされないメソッドは、はるかに多くを受け入れる場合でも(String)を取るように見える。それについて何をするかでツールが分かれる。

  • TypeProfは観測された狭いパラメータを出力する — その仕事は型レベルの実行が見たものを報告すること。
  • Rigorはそれをデフォルトにすることを拒むロバストネス原則(厳格な戻り、寛容なパラメータ)の下で、--params=observedは意図的なオプトインであり、その出力は凍結された契約(contract)ではなくレビューすべき提案。デフォルトのuntypedは、スイートの現在の使い方が将来のすべての呼び出し元の義務に静かになるべきではない、というスタンスである。
TypeProfRigor
テストの役割特別な役割はない — テストは推論を駆動する呼び出しサイトのひとつにすぎないsig-genだけのオプトイン燃料(checkゲートには決して使わない)
specの読まれ方通常のRubyとして実行(テストとして認識しない)subjectletdescribed_classを構造的に認識
観測された狭いパラメータそのまま出力レビュー可能な提案として扱う(ADR-5、オプトイン、人間のゲート)
双方向ループ1パス内での引数 ↔ 戻りspec → sig → spec-checking → より鋭いsig

Rigor側のふたつの正直な制限(どちらも第11章): describecontextをまたいでネストされた同名のletバインディングは後勝ち(last-wins)であり、beforearoundフックのボディはバインディングの変異について参照されない。

TypeProfは解釈しながらいくつかのエラーを報告する — 未定義のメソッド、明らかに不可能な操作 — が、これらは推論パスの副産物であり、キュレートされたリンターではない。ノイジーになりうるし、TypeProfはバグ発見のゲートとして掲げられていない。

Rigorはこれを反転させる。診断ストリームこそが製品であり、統治するルール(ADR-0、およびツール全体が中心に据える偽陽性の規律)はバグが証明可能でない限り沈黙を保つこと。rigor check libを実行すると、インタープリタが意外に思ったすべての記録ではなく、小さく高信頼な発見のセットが得られる。

だからTypeProfから来たときのメンタルな移行はこう: Rigorの出力を「見つけた型」だと期待しない(それにはsig-genを使う)。「コードが証明可能に間違っている少数の箇所」だと期待すること。

TypeProfRigor
typeprof app.rbrigor sig-gen --print lib/app.rb(RBS用)/rigor check lib(バグ用)
typeprof -I sig app.rb(RBSをロード).rigor.ymlsignature_paths:(自動検出)
typeprof --lsp(IDE向けTypeProf)rigor lsp(ADR-19)
呼び出しサイト推論を駆動するハーネスファイル不要 — メソッドごとのローカル推論
コア/stdlib RBSを自動ロードコア/stdlib型のカタログを自動ロード

両ツールとも標準のrbs-collectionメカニズム、Steepが使うのと同じものを通じてgem RBSを読む。

  • 呼び出しサイトからのパラメータ推論。TypeProfのプログラム全体の解釈は、メソッドのパラメータ型をそれがどう呼ばれるかから推論することを可能にする。Rigorはしない — アノテートされておらずナローイングされていないパラメータは、.rbsまたはガードが別のことを言うまでDynamic[Top]
  • 真の手続き間ボディ解釈。TypeProfは呼び出し先のボディを再解釈する。Rigorは呼び出し先をカタログで引く。小さな自己完結したプログラムでは、TypeProfはRigorがたどるよりも遠くまでデータフローをたどれる。
  • 第一級の「シグネチャ全体を推論する」出力。それはTypeProfの主たる製品であり、まさにそれに狙いを定めて長年チューニングされてきた。rigor sig-genは意図的により保守的(特にパラメータについて、ADR-5に従う)。
  • 値述語によるリファインメントキャリア。どちらのツールもis_a?nil?casewhenのような型同一性述語に対してはフローセンシティブなoccurrence typingを行う — TypeProfも含めて。Rigorが加えるのはリファインメントキャリアだ。unless s.empty?n > 0のような述語が、名前付きのリファインメント型(non-empty-stringpositive-int)へとナローイングされる。TypeProfにはリファインメントキャリアの概念がないため、これらの値述語によるリファインメントはStringIntegerへと拡大して戻ってしまう。
  • メソッド呼び出しを通じた定数folding"foo".upcaseConstant<"FOO">に解決される。TypeProfの出力はString
  • キュレートされ偽陽性の規律disciplineを持つ診断ゲート。Rigorはまずチェッカー。TypeProfのエラーは副産物。
  • キャッシュを持つコードベース全体のスケール。推論バジェットとファイルごとのキャッシュ(ADR-6)は、毎回の実行で大きなアプリに対してrigor checkを使えるようにする。TypeProfはより小さな入力向けに位置づけられている。
  • RBS::Extendedディレクティブ%a{rigor:v1:…}のリファインメント/述語/アサーション文法(第7章)にTypeProfの対応物はない。
  • プラグインエコシステム。Sorbet入力アダプタ、Railsサイドのナローイング、flow_contribution_for戻り型貢献 — TypeProfがモデル化しない拡張サーフェス(surface)。

ふたつのツールはライフサイクルの異なる点に位置するため、自然に合成される:

  1. TypeProfでブートストラップする(オプション)。sigのない真新しいファイルや小さなライブラリで、typeprofは読むための最初のRBSドラフトを与えてくれる。(またはステップ2に直行する — Rigorはそれを必要としない。)
  2. Rigorでチェックする。証明可能なバグのゲットのためにrigor check libを実行する。これが日々のシグナル。
  3. rigor sig-genでsigを生成する — Rigor自身の推論とラウンドトリップし、tighter-returnの非上書きルールを尊重するRBSが欲しいとき。
  4. RigorをCIに追加する。チェッカーゲートが毎回のpushで実行される。TypeProfはローカルで時折のプロトタイピングの助けにとどまる。

両者のRBSが食い違うときの常設ルール: TypeProfはRigorがuntypedと報告する呼び出しサイトからより狭いパラメータを推論しうる。それはRigorのバグではない — ローカルvsプログラム全体のトレードである。Rigorにそのより狭いパラメータを尊重させたいなら、それをsig/に書く(すると両ツールがそれを読む)か、エンジンがナローイングできるガードを追加する。

中規模のgemのsig/をブートストラップするためにTypeProfを使ってきたとする。typeprof lib/**/*.rbを実行し、プロトタイプを手編集し、コミットする。その上にRigorのバグ発見が欲しい。

手順:

  1. 生成したsig/を残す。RigorはSteepがそうするのとまったく同じようにそれを入力として読む — TypeProf生成のRBSはただのRBS。
  2. rigor check libを一度実行する。TypeProfが与えたのとは異なる種類の出力を期待する。シグネチャではなく、証明可能な発見の短いリスト — ナローイング対応の診断(flow.always-truthy-condition)、コミットしたsigに対する戻りの不一致(def.return-type-mismatch)。バグかノイズかをトリアージする。
  3. RBS生成ステップをrigor sig-genに切り替える。以前はtypeprofを再実行して再編集していたところで、代わりにrigor sig-gen --diffを実行する。分類モデル(new-filenew-methodtighter-return)は、手で締め付けたパラメータ型を潰さないことを意味する。
  4. オプションでRBS::Extendedでsigを締め付ける。TypeProfは%a{rigor:v1:…}を通常のRBSコメントとして扱い無視する。Rigorはそれらをリファインメントディレクティブとして読む。同じ.rbsファイルがより厳格なRigor出力と変更なしのTypeProf出力を生む。

基盤となる前提 — 交換フォーマットとしてのRBS、デフォルトとしての推論 — が共有されているため、移行は低摩擦。それはまさにTypeProfがあなたを訓練してきたものだ。

この付録セクションの残りを順番に読む必要はおそらくない。3つの有用なポインタ:

他のツールと比較したい場合は、兄弟付録ページがSteep — Rubyのもうひとつの静的チェッカー — に加え、TypeScriptPHPStanmypyをカバーしている。

© 2026 TypedDuck. Licensed under CC BY-SA 4.0.