メソッドとブロック
この章では、Rigorがメソッド呼び出しについて知っていること — レシーバーの型、引数の型、推論された戻り値型、ブロックが付属している場合のブロックパラメータ — を扱います。いくつかのセクションは呼び出し元診断のリファレンスも兼ねており、見出しにルールIDが現れます。
この章の内容 メソッドディスパッチ · 引数型(
call.argument-type-mismatch) · アリティ(call.wrong-arity) · 未定義メソッド(call.undefined-method) · nilレシーバー(call.possible-nil-receiver) · 戻り値型推論 · 戻り値の不一致(def.return-type-mismatch) · ブロックパラメータ · 番号付きパラメータとit· ブロックローカル · クロージャエスケープ
メソッドディスパッチ — 呼び出し元でRigorが見るもの
Section titled “メソッドディスパッチ — 呼び出し元でRigorが見るもの”Rigorがreceiver.method(args, &block)に遭遇したとき、結果を生成する最初のものを採用しながら、固定されたディスパッチ層のシーケンスを実行します:
- 定数たたみ込み。 すべての引数が
Constant<...>または定数のタプルで、レシーバーが既知の名前的クラスで、メソッドがクラスごとの「純粋な」カタログにある場合。Rigorは解析時にメソッドを実行して結果を返します。1 + 2→Constant<3>、[1, 2, 3].first→Constant<1>。 - シェイプ(shape)ディスパッチ。 レシーバーが
Tuple/HashShape/IntegerRange/ リファインメント(refinement、篩型とも)を持ち、メソッドにシェイプごとのルールがある場合。Tuple[A, B, C].size→Constant<3>;int<0, max>.zero?→Constant<true> | Constant<false>。 - RBSディスパッチ。 クラスにそのメソッドのRBSシグがある場合。引数型がパラメータ契約(contract)に対してチェックされます(後述);戻り値型はシグから読まれ、
RBS::Extendedディレクティブによって締め付けられることがあります。 - インソースディスパッチ。 クラスにRBSはないが、Rigorがプロジェクト内の
def(またはdefine_method、attr_*)を見つけた場合。パラメータ型はチェックされません(契約がない);戻り値型はメソッド本体から推論されます。 - フォールバック。 上記のいずれも当てはまらない — 呼び出しは
Dynamic[Top]を返し、沈黙を保ちます。
「最初に一致したものが勝つ」カスケード構造が、厳密なRBSシグ + RBS::Extendedディレクティブを持つメソッドがインソース本体の推論された戻り値型をオーバーライドする理由です。シグレベルでの締め付けは、RBSが表現するよりも狭い戻り値型を持つドメイン固有のメソッドについてRigorに教える、サポートされた方法です。
引数型付け — call.argument-type-mismatch
Section titled “引数型付け — call.argument-type-mismatch”メソッドにRBSシグ(またはRBS::Extendedパラメータオーバーライド)がある場合、Rigorは各位置引数/キーワード引数を宣言されたパラメータ型に対してチェックします:
class Slug %a{rigor:v1:param: id is non-empty-string} def normalise: (::String id) -> ::StringendSlug.new.normalise("hello") # OK — Constant<"hello">はnon-empty-stringを満たす
Slug.new.normalise("") # error: argument-type-mismatch # ("" はnon-empty-stringが除外する # 唯一の値)
Slug.new.normalise(some_str) # some_strが空であることをRigorが # 証明できない場合はOK; # 「どちらかの可能性がある」ケースでは # Rigorは沈黙しますcall.argument-type-mismatchは、Rigorが引数がパラメータ契約を満たせないことを証明できる場合にのみ発火します。「空の可能性がある」は沈黙します — 偽陽性なしルール。
アリティ — call.wrong-arity
Section titled “アリティ — call.wrong-arity”レシーバークラスが静的に既知で、メソッドが発見可能(RBSシグまたはインソースdef)な場合、Rigorは引数の数をメソッドのアリティに対してチェックします:
[1, 2, 3].rotate(1, 2)# error: wrong number of arguments to `rotate' on Array# (given 2, expected 0..1)アリティチェックは省略可能な位置引数、スプラット、キーワード引数、オーバーロードシグネチャを考慮します。メソッドがオーバーロードされている場合、指定されたアリティを受け入れるすべてのオーバーロードが候補です — Rigorはどのオーバーロードも受け入れない場合にのみアリティをフラグします。
call.undefined-method
Section titled “call.undefined-method”レシーバークラスが静的に既知で、メソッドが(RBSシグ、インソースdef、インソースattr、Data.defineアクセサの)いずれにもない場合、Rigorは呼び出しをフラグします:
"hello".no_such_method# error: undefined method `no_such_method' for "hello"このルールは意図的に保守的です: 呼び出しがフラグされるのは、レシーバー型が静的に既知で、メソッドカタログが列挙可能な場合のみです。Dynamic[Top]レシーバー、メソッド本体内の暗黙的selfの呼び出し、定数宣言エイリアスクラス(YAML → Psych)は沈黙します。
call.possible-nil-receiver
Section titled “call.possible-nil-receiver”レシーバーの型がT | nilで、呼び出されたメソッドがNilClassで定義されていない場合、Rigorはフラグします:
def shout(name) name.upcase # warning: name: String?のときend修正は通常ガードを追加することです:
def shout(name) return "" if name.nil? name.upcase # name: StringになったendこのルールはRigorが出荷する最も高価値な診断の1つです — 非自明なRubyコードベースに散在するnilへのNoMethodErrorクラッシュのファミリー全体を捉えます。
インソースメソッドの戻り値型推論
Section titled “インソースメソッドの戻り値型推論”RBSシグなしでdefを書くと、Rigorはメソッド本体から戻り値型を推論します。推論された型は最後の式が評価するものです:
def double(n) n * 2end
double(5) # Constant<10> — Rigorが呼び出しをたたみ込む本体に複数のブランチがある場合、戻り値型は到達可能なすべての終端式のユニオン(union、合併型とも)です:
def kind(x) if x.is_a?(Integer) :int elsif x.is_a?(String) :str endend
kind(7) # Constant<:int>kind("hi") # Constant<:str>kind(:nope) # Constant<nil> — ifのelse分岐が欠けている # ことによる暗黙のnil本体途中のreturnは期待通りに動作します;明示的なraiseはそのブランチをユニオンから除外します(内部的にbotキャリア(carrier))。
def.return-type-mismatch
Section titled “def.return-type-mismatch”メソッドにRBS宣言の戻り値型と推論された型の両方がある場合、Rigorは推論された型が宣言された型に適合するかチェックします:
class Slug def normalise: (::String) -> ::Stringendclass Slug def normalise(s) s.empty? ? nil : s.upcase # warning: # (宣言はString、推論は # String | nil) endendこのルールはcall.argument-type-mismatchの対称的な対応物です: 引数側は「呼び出し元が間違った型を渡した」;戻り値側は「私が呼び出し元に間違った型を返した」。
ブロックパラメータ
Section titled “ブロックパラメータ”メソッドがブロックを受け取るとき、Rigorはレシーバーメソッドのシグネチャに基づいてブロックパラメータをバインドします。バンドルされたカタログのすべてのブロック使用メソッドにはメソッドごとのルールがあります:
[1, 2, 3].each do |n| assert_type("Constant<1> | Constant<2> | Constant<3>", n)end
%w[a b c].each_with_index do |word, idx| assert_type("Constant<\"a\"> | Constant<\"b\"> | Constant<\"c\">", word) assert_type("non-negative-int", idx)end
{name: "Alice", age: 30}.each_pair do |key, value| assert_type("Constant<:name> | Constant<:age>", key) assert_type("Constant<\"Alice\"> | Constant<30>", value)end位置ごとのバインディングはタプル、ハッシュシェイプ、範囲に対して機能します。レシーバーが拡幅されている(Tuple[…]ではなくArray[T])場合、ブロックパラメータは要素型Tです。
受信メソッドにメソッドごとのルールがない場合、ブロックパラメータはDynamic[Top]にフォールバックします。プロジェクトソースに書いたカスタムなブロック使用メソッドは、インソースディスパッチ層によって見られます — Rigorが本体を走査してyield呼び出しからパラメータ型を推論します — しかしその解析はカタログに載っている組み込みよりも制限があります。
番号付きパラメータとit
Section titled “番号付きパラメータとit”_1、_2、…、およびRuby 3.4のitは明示的なパラメータとまったく同じようにバインドされます:
[1, 2, 3].each { _1.succ }# _1: Constant<1> | Constant<2> | Constant<3>
[10, 20, 30].each { it.to_s }# it: 明示的な形式と同じブロックローカル宣言(do |i; x|)
Section titled “ブロックローカル宣言(do |i; x|)”;を接頭辞にした名前は、同名の外側ローカル変数をシャドウする新しいブロックローカル変数を導入します。Rigorはブロックエントリー時にこれらのローカル変数をConstant<nil>にバインドします — Rubyのランタイムセマンティクス — そしてブロック内の書き込みをそのブロックにローカルなものとして扱います:
x = 100[1, 2, 3].each do |i; x| # x: Constant<nil> この時点で — ブロックローカルなシャドウ x = i * 2 # x: Constant<2> | Constant<4> | Constant<6>end
assert_type("Constant<100>", x) # 外側のxは変更されていないクロージャエスケープとキャプチャされたローカル変数
Section titled “クロージャエスケープとキャプチャされたローカル変数”ブロックが外側のローカル変数をキャプチャすると、そのローカル変数へのブロックの書き込みが呼び出し後のローカル変数のビューに影響します。非エスケープとして既知のメソッド(Array#each、tapなど)では、呼び出し後のナローイング(narrowing)が保持されます;エスケープするメソッド(Thread.new、define_methodなど)では、ブロックが任意の後のタイミングで発火する可能性があるため、解析器はキャプチャされたローカル変数のナローイングを除去します。
これは保守的な判断です: エスケープ後のナローイング事実をランタイムが違反するかもしれないという主張をするよりも、広げすぎるほうがよいです。
次に読むもの
Section titled “次に読むもの”第6章はクラス側を扱います: Rigorがself、定数ルックアップ、attr_*宣言、クラスメソッドとインスタンスメソッドの区別をどう型付けするか。
© 2026 TypedDuck. Licensed under CC BY-SA 4.0.