Part 7 受理判定と「たぶん」
この章のゴールは、「この型、ここに渡して合う?」を判定するacceptsを作ることです。ただし答えは「はい」か「いいえ」の二択ではなく、:yes/:no/:maybeの三択にします。この「:maybe」が、Rigorがやさしくいられる最大の理由です。
『しくみ』では7章「部分型付け」(『TAPL』15章「部分型付け」)が対応します。同書は「合う/合わない」を
true/falseの二択で答えました。私たちはそこに一つ足します。
7-1. 「合う?」という新しい問い
Section titled “7-1. 「合う?」という新しい問い”これまでtype_ofは、式から型を上向きに求めるだけでした(1 → Const[1])。でもエラーを見つけるには、別の問いが要ります。
このメソッドは
Integerを欲しがっている。あなたが渡したのはStringだ。これは合う?
これが受理判定です。『しくみ』7章ではこれをsubtype(a, b)(aはbの部分型か)として作り、答えはtrue/falseの二択でした。
ところがRubyでこれを二択にすると、すぐ困ります。type_ofは、わからない式に対してDynamic(untyped)を返すのでした。Part 1でそう決めました。では:
Integerを欲しがっている所に、untypedが来た。これは合う?
trueと答えれば、本当はバグでも見逃すかもしれません。falseと答えれば、ちゃんと動くコードを怒ってしまうかもしれません。どちらも嘘です。本当の答えは「わからない(たぶん)」です。
だからacceptsの答えは三択にします。:yes/:no/:maybeです。
7-2. まず白黒がつく所(確実に合うか、確実に合わないか)
Section titled “7-2. まず白黒がつく所(確実に合うか、確実に合わないか)”「合う」とは何か。むずかしく考えず、「渡した値が、欲しがっている型の箱に入るか」と思ってください。Integerという箱には1も2も入ります。Stringの"x"は入りません。
ただしConst[1]とNominal[:Integer]をそのまま比べても一致しません。値そのものの型とクラスの型は別物だからです。だから比べる前に、値の型をクラスに丸めてそろえます(widen)。Part 1の「丸め」がここでも効きます。
# Const[1] のような「値そのもの」は、いったんクラスに丸めてから比べるdef widen(t) = t.is_a?(Type::Const) ? Type::Nominal[t.value.class.name.to_sym] : t
def accepts(expected, actual) widen(expected) == widen(actual) ? :yes : :noendaccepts(Type::Nominal[:Integer], Type::Const[1]) # => :yes (1 は Integer の箱に入る)accepts(Type::Nominal[:Integer], Type::Const["x"]) # => :no ("x" は入らない)この「箱に入るか」が、『しくみ』7章の部分型付けそのものです。用語は覚えなくて大丈夫です。小さい箱は大きい箱に入る、とだけ押さえておいてください。『しくみ』は箱の大小を丁寧に階段状に定義しましたが、私たちはまず「クラスが同じか」という一番素朴な所から始めます。
この章の三つの視点(パースペクティブ):
- ① 型理論:値が期待の型に入るかが部分型付けです(『しくみ』7章)。
- ② Rubyだと:
1はInteger、"x"はString。クラスで大まかに判定できます。 - ③ Rigorだと:
Const[1]のような細かい型は、比べる前にクラスに丸めてから判定します。Part 1の「丸め」がここでも効きます。
7-3. 「たぶん」が出るとき(untypedが混ざる)
Section titled “7-3. 「たぶん」が出るとき(untypedが混ざる)”さて、ここからが本題です。Dynamic(untyped)が絡んだら、白黒つけません。:maybeを返します。
def accepts(expected, actual) return :maybe if expected.is_a?(Type::Dynamic) || actual.is_a?(Type::Dynamic) # ★追加 widen(expected) == widen(actual) ? :yes : :noendaccepts(Type::Nominal[:Integer], Type::Dynamic.new) # => :maybe (わからないものは、わからない)たった1行ですが、これがRigorを「動的言語にやさしい型チェッカー」にしている大黒柱の一本です。untypedは「型を見失った」という印です。見失ったものについて「合う」とも「合わない」とも言い張らない。それが誠実というものです。
- ① 型理論:未知(untyped)が混ざると、判定は片側に倒せません。
- ② Rubyだと:型注釈が無いコードは普通です。
foo.barの戻りなんて誰にもわかりません。 - ③ Rigorだと:わからない所は
:maybeにします。これが次の節で「脅かさない」に直結します。
7-3a. Unionが引数に来るとき(一番弱い答えを採る)
Section titled “7-3a. Unionが引数に来るとき(一番弱い答えを採る)”Union(「IntegerかStringのどちらか」のような型。Part 4、5で作りました)が渡されることもあります。渡す側(actual)がUnionのときは、実際にどのメンバの値が来るか分からないので、全メンバが通って初めて安全です。だからメンバを全部acceptsにかけて、一番弱い答えを採るだけです。一個でも:noなら:no、:noが無くても:maybeがあれば:maybe、全部:yesなら:yesです。
def accepts(expected, actual) return :maybe if expected.is_a?(Type::Dynamic) || actual.is_a?(Type::Dynamic)
if actual.is_a?(Type::Union) # 全メンバが通って初めて :yes(一番弱い答え) results = actual.members.map { |m| accepts(expected, m) } return :no if results.include?(:no) return :maybe if results.include?(:maybe) return :yes end
if expected.is_a?(Type::Union) # どれか一つに合えば :yes(一番強い答え) results = expected.members.map { |m| accepts(m, actual) } return :yes if results.include?(:yes) return :maybe if results.include?(:maybe) return :no end
widen(expected) == widen(actual) ? :yes : :noendexpectedがUnionの場合(accepts(Integer | String, Integer)など)は一番強い答えを採ります。どれか一つに合えば:yesです。「IntegerかStringのどちらかを期待」していて、渡されたのがIntegerなら問題ない、という直感です。
これで1 + (c ? 1 : 2)のようにUnionが引数に来ても、中身が全部Integerなら:yesで黙ります。誤検知しません。Integer | StringのようにStringが混じれば:noで報告します。
- ① 型理論:Unionのメンバ全員が通って初めて
:yesです(一番弱い結論を採る、union-subtyping)。 - ② Rubyだと:
c ? 1 : 2はInteger | Integerだから問題ありません。c ? 1 : "a"はUnionになり、Integerを期待する式に渡すと:noになります。 - ③ Rigorだと:Unionへの照合でも「分からなければ
:maybe」です。全員:yesのときだけ怒れます。
▼ 図7-1
acceptsの三値判定(untypedは:maybe、Unionは一番弱い答え)
7-4. 「たぶん」は罰しない(この章の山場)
Section titled “7-4. 「たぶん」は罰しない(この章の山場)”acceptsをcheckで使います。エラーを報告する場所は、「欲しい型が決まっている所」だけです。Part 2で作った手書きディスパッチ表を思い出してください。Integer#+は「Integerを1つ欲しい」と書いてありました。そこが「欲しい型が決まっている所」です。
# Part 2 の表(再掲)。param に「欲しい型」が書いてあるMETHODS = { [:Integer, :+] => { params: [Type::Nominal[:Integer]], returns: Type::Nominal[:Integer] }, # ...}
def check_call(recv_type, name, arg_types, diagnostics) sig = METHODS[[class_of(recv_type), name]] return Type::Dynamic.new unless sig # 知らないメソッド → 脅かさない(Part 2 の方針)
sig[:params].zip(arg_types).each do |want, got| case accepts(want, got) when :no diagnostics << "expected #{want} but got #{got}" when :maybe, :yes # 何もしない ← ここが全て end end sig[:returns]end見てのとおり、怒るのは:noのときだけです。:yesはもちろん、:maybeでも黙ります。
check("1 + 2") # 文句なし(:yes)check('1 + "x"') # ["expected Integer but got \"x\""](:no)check("1 + foo.bar") # 文句なし! foo.bar は untyped → :maybe → 黙るInteger | Integer: OK (no errors)expected Integer but got 1 | "a"ここで、この本で一番大事な一文を置きます。専門用語は要りません。
『しくみ』7章にも、よく似た問題意識のコラムがありました。「健全な型システムが良いプログラムを弾くのは誤検知だ」と。『しくみ』は誤検知を減らす方向で工夫しました。Rigorはもう一歩進めて、「わからない=たぶん=黙る」を仕組みの真ん中に置くことで、誤検知をそもそも生みにくくしています。
そして実は、Part 1、2で+のときに書いた場当たり的なintegerish?チェック、あれはこのacceptsの手書き版でした。いまそれを正式な仕組みに置き換えたわけです。
7-5. 返りは厳しめ、引数は緩め(小さなコラム)
Section titled “7-5. 返りは厳しめ、引数は緩め(小さなコラム)”最後に小さな観察を一つ。いま私たちは、
- 引数を見るときは
acceptsで緩く判定しました(:maybeを通す) - 一方、戻り型は
type_ofできっちり求めています(Integer#+ならIntegerと断言)
この非対称、「返すものは厳しく正確に、受け取るものは緩く寛容に」は偶然ではなく、Rigorがわざと守っている作法です。受け取りを厳しくすると、呼ぶ側が無駄な型変換を書かされて窮屈になります。返しを緩くすると、その値を使う側が損をします。だから逆にするのです。1
7-6. この章のまとめ
Section titled “7-6. この章のまとめ”足したものは、関数accepts(:yes/:no/:maybeを返す)と、checkがそれを使って:noのときだけ怒る仕組みです。コードはaccepts本体4行+Unionのおまけ数行+checkの差し替えだけです。新しい型キャリアは増えていません。
この章の三つの視点:
| 内容 | |
|---|---|
| ① 型理論(『しくみ』7章 / 『TAPL』15章) | 値が期待の型に入るか=部分型付け |
| ② Ruby/RBS | ダックタイピング、型注釈の無いコード、untypedが普通に混ざる |
| ③ Rigor実装の問題 | 二択だと誤検知or見逃しになる → 三値(:maybe)で「わからない」を表に出し、罰しない |
続編に送ったもの(ここで扱うと易しさが壊れるため):
- 箱の大小をきちんと階段状に定義する本物の部分型(『しくみ』7章のwidth/depth)
- 変性(関数を渡すとき、引数の向きだけ逆になる「反変」)。『しくみ』7章の山場
type_of(求める)とaccepts(確かめる)の2方向をまとめて「双方向型付け」と呼ぶ話
accepts(Nominal[:Integer], Union[[Const[1], Const["x"]]])が:noになることを確かめよ(一番弱い答え)。- expectedがUnionのとき(
accepts(Integer | String, Integer))の挙動を上記の実装で確かめよ。:yesを返すのはなぜか、:noとはならない理由とあわせて説明せよ。 :maybeが出るのにcheckが黙る例を1つ作り、「:maybeは罰しない」を確かめよ。
次章予告(Part 8):手書きのMETHODS表を、本物のRBSから引くように差し替えます。「型は別ファイルに書く」というRuby/RBSの世界観に初めて触れます。
この章の実装(演習の答え合わせにも) →
impls/dist/part7/lib
Footnotes
Section titled “Footnotes”-
この「返りは厳密、引数は寛容」という作法には名前も付いていますが、覚えなくて構いません。なぜこの非対称が正しいのか(型理論やオブジェクト指向の置換可能性と、別々の出発点から同じ規則に至ること)は、後編Part 2で確認します。 ↩
© 2026 TypedDuck. Licensed under CC BY-SA 4.0.