ナローイング
キャリア(carrier)はあるプログラム地点における値を記述します。ナローイング(narrowing)は、制御フローが述語を通過するときにキャリアがどう変化するかを記述します。この章では、Rigorが現在認識しているナローイングのあらゆる形式を解説します。
メンタルモデル: 各述語は2つのスコープを生成します — 真値のエッジと偽値のエッジです。各エッジの内側では、変数のキャリアが述語が証明したものに鋭利化されます。述語が認識されなければ、両エッジはエントリースコープをそのまま共有します。
この章の内容 真偽性 ·
nil?·is_a?/kind_of?/instance_of?· リテラル等値 ·case/when· 論理演算 · 整数比較 · 述語メソッド · 名前付きキャプチャ正規表現 · 否定とunless· ローカル再バインド · まだナローイングされないもの · ナローイングトレースを読む
真偽性ナローイング
Section titled “真偽性ナローイング”最も単純な形式です。if xは「xが真値」と「xがfalseまたはnil」を分離します:
def shout(name) if name # name: String — 真値エッジで `false | nil` が除去される name.upcase else # name: Constant<false> | Constant<nil> "(no name)" endendこれこそ、ありふれたif valueイディオムが解析時に有用になる理由です: if本体の内側で、Rigorはvalueが非nilであることを知ります。
nil?とその逆
Section titled “nil?とその逆”def length(s) return 0 if s.nil? # s: Nominal[String] (String?のnil成分が消えた) s.lengthends.nil?は真値エッジをConstant<nil>にナローイングし、偽値エッジを「それ以外すべて」— 通常はnilが除去された元の型 — にナローイングします。
is_a?、kind_of?、instance_of?
Section titled “is_a?、kind_of?、instance_of?”これら3つはすべてクラス階層に基づいてナローイングします:
def kind(x) if x.is_a?(Integer) # x: Integer x + 1 elsif x.is_a?(String) # x: String x.length endendサブクラスの関係が考慮されます: is_a?(Numeric)はIntegerとFloatを受け入れ、それに応じてナローイングします。instance_of?はより厳密で — 完全に一致するクラスのみ — Rigorもそれに応じてナローイングします。
偽値エッジでは一致したクラスが除外されます:
x = some_call_that_returns_integer_or_stringunless x.is_a?(Integer) # x: String — Integerが除外される x.upcaseendリテラル値との等値比較
Section titled “リテラル値との等値比較”Rigorは信頼できるリテラル値に対して==と!=をナローイングします:
state = some_call_returning_a_symbolif state == :ready # state: Constant<:ready> send_requestelsif state == :pending # state: Constant<:pending> retry_in(5)endこれはstate自体が定数のユニオン(union、合併型とも)(Constant<:ready> | Constant<:pending> | Constant<:failed>)のときに最も有用です。各ブランチが1つのメンバーを剥がし、Rigorは最後のelseが残りの定数のいずれかであると証明できます — 「任意のSymbol」ではありません。
case / when
Section titled “case / when”case x; when …は、等値チェックとクラスチェックに対するナローイング構文糖です。各whenブランチではxが一致したメンバーにナローイングされます:
case nwhen 0 then :zero # Constant<0>when 1..9 then :small # int<1, 9>when 10 then :ten # Constant<10>else :large # それ以外すべてend結果型はブランチごとの結果のユニオンです。入力が有限リテラルユニオンのとき、すべてのメンバーが一致する場合、Rigorはelseブランチが到達不能であることを証明します。
case x; in pattern(1行パターンマッチング)も、Rigorが理解するパターン — クラスチェック、リテラル等値、配列/ハッシュ構造パターン — に対して同じようにナローイングします。
def safe_size(s) if s && !s.empty? # s: non-empty-string s.size endend&&は左から右へのナローイングを連鎖します: 右オペランドは左の真値エッジ下で評価されます。||は偽値エッジを連鎖します。!は2つのエッジを入れ替えます。
これは他のすべてと組み合わせられます:
if x.is_a?(Integer) && x > 0 # x: positive-intendis_a?がxをIntegerにナローイングし、次に整数比較がさらにint<1, max>にナローイングしました。
<、<=、>、>=、およびInteger#zero? / #positive? / #negative? / #nonzero? / Comparable#between?はすべて整数範囲をナローイングします:
def safe_index(arr, n) return :empty if arr.empty? return :out_of_range if n < 0 || n >= arr.size # n: int<0, arr.size - 1> (実際には: int<0, max> # が `n >= arr.size` に対して締め付けられる) arr.fetch(n)end範囲比較はリテラルと組み合わせられます:
n = some_inputif n.between?(1, 9) # n: int<1, 9>endリファインメントに対する述語メソッド
Section titled “リファインメントに対する述語メソッド”Rigorは少数の「型キャリア述語メソッド」を認識します — 戻り値型がboolで、真値/偽値エッジがレシーバーをナローイングするメソッドです:
| メソッド | レシーバーをナローイングする先 |
|---|---|
String#empty? | Constant<"">(真値) / non-empty-string(偽値) |
Array#empty? | Constant<[]>(真値) / non-empty-array[T](偽値) |
Hash#empty? | Constant<{}>(真値) / non-empty-hash[K,V](偽値) |
Integer#zero? | Constant<0>(真値) / non-zero-int(偽値) |
Integer#positive? | positive-int(真値) / non-positive-int(偽値) |
Integer#negative? | negative-int(真値) / non-negative-int(偽値) |
期待通りに組み合わせられます:
def first_word(s) return "" if s.empty? # s: non-empty-string s.split.first # ランタイムでは常にStringを返す、 # nilではない — Rigorもそれを知っているend名前付きキャプチャ正規表現ナローイング
Section titled “名前付きキャプチャ正規表現ナローイング”名前付きキャプチャを持つ正規表現がif / unlessの述語位置でマッチすると、キャプチャされたローカル変数はマッチ後にString | nilにバインドされ、真値ブランチではStringにナローイングされます:
def parse_date(s) if /(?<year>\d{4})-(?<month>\d{2})-(?<day>\d{2})/ =~ s # year, month, day: String (String | nilからナローイングされた) "#{year}/#{month}/#{day}" else "no match" endend(真値エッジをさらに特定のリファインメントキャリアにナローイングすること — \d{4}ならdecimal-int-stringを生成する — は、正規表現パターン → リファインメント名の認識器のトラックにおける需要駆動のフォローアップです。docs/ROADMAP.mdを参照。)
否定とunless
Section titled “否定とunless”どちらも否定されていない形式の機械的な鏡です。ナローイング目的ではunless xはif !xと同じです。x != yは!(x == y)と同じです。Rigorは2つのエッジを入れ替えます。
ローカル変数の再バインドはナローイングをリセットする
Section titled “ローカル変数の再バインドはナローイングをリセットする”ナローイングの事実はスコープローカルです。変数を再代入した瞬間に、その事実はリセットされます:
def example(s) return if s.nil? # s: String
s = some_other_call # sが再バインド — ナローイングが除去される s.upcase # s: 呼び出しの戻り値型によっては # 再びString?になるendこれがエンジンのナローイング事実が特定の変数名ではなく特定のスコープに紐付けられている理由です。再バインドは検出されますが、メソッド呼び出しを通じた変異は検出されません(Rigorは変異を追いません)。
まだナローイングされないもの
Section titled “まだナローイングされないもの”Rigorが今日ナローイングしない形式でよく期待されるもの:
respond_to?(:method_name)— 「このオブジェクトはそのメソッドに応答する」を証明するには、エンジンがまだ公開していない構造的ファセットが必要です。frozen?などの変異ガード — Rigorはまだミュータビリティをナローイング事実として追跡しません。- 任意のユーザー定義
case_eqに対する===によるオープンエンドのクラス比較 — Class / Module / Range / Regexpのみが認識されます。 selfターゲットディレクティブ内のメソッドチェーンレシーバー(get_user.admin?)— ナローイングするスコープバインディングがありません。ローカル変数、インスタンス変数、明示的self、暗黙的selfのレシーバーはすべてサポートされています。
ナローイングが認識されない場合、両エッジはエントリースコープをそのまま共有します — Rigorは間違った判断をするよりも保守的に留まります。
ナローイングトレースを読む
Section titled “ナローイングトレースを読む”特定の地点でRigorが何をナローイングしたかを確認したいとき:
def foo(x) if x.is_a?(Integer) dump_type(x) # この行にinfo診断を出力する endenddump_type(...)はイントロスペクションヘルパーです。ランタイムではno-op(Rigorのテストハーネスが使うKernel拡張に存在)で、推論された型を名前付きのdump.type診断として出力します。ナローイングが発火したことを確認するデバッグ時に使います。
assert_type("expected-string", value)はより厳密な兄弟です: 推論された型が文字列に一致しないとき診断を出力します。ハンドブックの例が動作を固定するために使っています。
次に読むもの
Section titled “次に読むもの”第4章は構造的キャリア — TupleとHashShape — を扱います。これらはArrayとHashの要素ごとのナローイングによく似ています。
© 2026 TypedDuck. Licensed under CC BY-SA 4.0.