Part 1 リテラルと算術
この章のゴールは、値に型をつける一番小さな仕組みを作り、checkとannotateを実Rubyで動かすことです。
新しく出すのはConstという型ひとつと、type_ofという関数ひとつだけです。
1-1. 型を「データ」として表す
Section titled “1-1. 型を「データ」として表す”型をむずかしく考える必要はありません。ここでは「値につけた小さなラベル」だと思ってください。1には「整数」、"hi"には「文字列」というラベルが付きます。
型チェックとは、このラベル同士がケンカしていないかを見ることです。
『型システムのしくみ』2章でも、型を{ tag: "Number" }のようなただのデータで表していました。私たちも同じく、型をRubyのオブジェクトで表します。
ここで頭を一つ切り替えてください。型はIntegerのような「クラス」そのものではなく、型を表すデータです。Const[1]のように「クラスでは書けない細かい型」も作りたいので、専用のデータで表します。
この章で使うのは、次の3つだけです。
module Chibirigor # 「この値そのもの」を表す型。例: Const[1], Const["hi"] Const = Data.define(:value) do def to_s = value.inspect end
# 名前付きクラスを表す型。例: Nominal[:Integer](1-2 の「丸め」で使う) Nominal = Data.define(:name) do def to_s = name.to_s end
# 「知らない・確かめようがない」を表す型(あとで大活躍する) Dynamic = Data.define do def to_s = "untyped" endendここで、三つの視点(この本の恒例の枠組み)で整理してみます。
- ① 型理論:型は値につけるラベルで、内部ではただのデータです(『しくみ』2章)。
- ② Rubyだと:
1のクラスはInteger、"hi"のクラスはStringです。RubyもRBSも「1はInteger」とまでしか言いません。 - ③ Rigorだと:Rigorはもう一歩踏み込んで、
1という値そのものを型にします(Const[1])。「Integer」ではなく「1」です。なぜそんな細かいことをするのか。これがあとでcaseの枝分けや定数の計算で効いてきます。いまは「Rigorは型を細かく覚える」とだけ覚えておけば十分です。
型がわからないときや確かめようがないときは、Dynamic(untyped)に逃がします。「知らない、確かめようがないなら黙っておく」という印です。
1-2. 式から型を求めるtype_of
Section titled “1-2. 式から型を求めるtype_of”型チェッカーの心臓は、「式を受け取って型を返す」関数ひとつです。『しくみ』ではこれをtypecheckと呼んでいました。私たちはtype_ofと呼びます。
コードはPrismでパースします。Prism.parse("1").valueを辿ると、1はIntegerNode、"hi"はStringNode、というふうに種類ごとのノードになっています。種類で場合分けするだけです。
module Chibirigor module_function
def type_of(node, diagnostics) case node when Prism::IntegerNode then Const[node.value] when Prism::FloatNode then Const[node.value] when Prism::StringNode then Const[node.unescaped] when Prism::TrueNode then Const[true] when Prism::FalseNode then Const[false] when Prism::CallNode then type_of_call(node, diagnostics) else Dynamic.new # 知らないノードは「脅かさない」── だまって untyped を返す end endend『しくみ』のtypecheckがswitch (t.tag)だったのと、ほとんど同じ形です。違いは最後の行です。
『しくみ』は対象がきっちりしたミニ言語なので、知らない構文に出会うことはありません。
でも私たちが相手にするのは本物のRubyです。知らないものは必ず出てきます。そのときエラーにせずDynamic(untyped)を返します。
これがRigorの入口の姿勢です。
1 + 2はどうなる?
Section titled “1 + 2はどうなる?”ここでRubyならではの事実が一つあります。1 + 2の+も「メソッド送信」です。Prismでは1に+というメッセージを送るCallNodeになります(1.+(2)と同じ)。
いまは算術だけを、ごく素朴に書きます。
module Chibirigor module_function
def type_of_call(node, diagnostics) recv = type_of(node.receiver, diagnostics) args = node.arguments&.arguments || []
if node.name == :+ && integerish?(recv) arg = type_of(args.first, diagnostics) unless integerish?(arg) diagnostics << diagnostic(node, "can't add #{arg} to an integer") return Dynamic.new end # ★ ここがポイント:Const[3] とは計算せず、Integer に「丸める」 return Nominal[:Integer] end
Dynamic.new # それ以外のメソッドはまだ知らない → 脅かさない end
def integerish?(t) (t.is_a?(Const) && t.value.is_a?(Integer)) || t == Nominal[:Integer] end
# 診断は「どの行の・何が問題か」を持つ小さなハッシュ def diagnostic(node, message) { line: node.location.start_line, message: message } endendNominal[:Integer]は1-1で定義した「整数クラス」を表すラベルです。
ここで、三つの視点の③(Rigorが困った所)が自然に顔を出します。
type_of(1)はConst[1]、type_of(2)はConst[2]です- では
type_of(1 + 2)はConst[3]にすべきでしょうか- したい気もしますが、それには足し算を実際に計算しないといけません
x + 2ならもう値はわかりません
- そこで結果は
Integerに丸めます- 「値そのもの」を覚えるのは便利ですが、どこかで手放して大ざっぱな型に戻す必要があります
- いまは「足し算の結果は
Integer」とだけ覚えれば十分です - この”いつ丸めるか”を実Rigorがどう体系立てるかは、後編で扱います
1-3. 矛盾を見つけるcheck(でも止まらない)
Section titled “1-3. 矛盾を見つけるcheck(でも止まらない)”type_ofができたので、矛盾の報告であるcheckを作ります。やることは「トップレベルの文をひとつずつtype_ofにかけ、その途中で見つかった文句(diagnostics)を集める」だけです。
module Chibirigor module_function
def check(source) program = Prism.parse(source).value diagnostics = [] program.statements.body.each { |stmt| type_of(stmt, diagnostics) } diagnostics endendcheckの戻り値は{line:, message:}の配列です(どの行で何が問題か)。動かしてみます。
Chibirigor.check("1 + 2") # => [] (文句なし)Chibirigor.check('1 + "x"') # => [{ line: 1, message: "can't add \"x\" to an integer" }]Chibirigor.check("foo.bar") # => [] ← 知らないメソッドは黙って通す『しくみ』のtypecheckは矛盾を見つけるとthrowしてそこで止まりました。私たちは違います。文句を配列に貯めて、最後まで読み進めます。
一つ目のエラーで止まらないし、わからない所(foo.bar)はそっと通します。これも「動くコードを脅かさない」の一部です。
- ① 型理論:型チェックは、ラベルの矛盾検出です(『しくみ』2章)。
- ② Rubyだと:
1 + "x"は実行すればTypeErrorになります。foo.barはfoo次第で動くかもしれません。 - ③ Rigorだと:確実に矛盾する所だけ報告し、わからない所は黙ります(止まらず、脅かさず)。
1-4. 求めた型を見せるannotate
Section titled “1-4. 求めた型を見せるannotate”ここまで来ると、おまけがほぼタダで手に入ります。type_ofは型を作っているのだから、それを出力するだけで「推論した型を見せる」annotateになります。
checkが{line:, message:}の配列を返すのに合わせ、annotateは各文の{line:, type:}の配列を返します(行番号と、推論した型)。
module Chibirigor module_function
def annotate(source) program = Prism.parse(source).value program.statements.body.map do |stmt| type = type_of(stmt, []) # 文句は今は捨てる { line: stmt.location.start_line, type: type } end endend表示してみます。型はto_sで1/Integer/untypedのように出ます。
Chibirigor.annotate(<<~RUBY).each { |a| puts "#{a[:line]}: #{a[:type]}" } 42 "hello" 1 + 2 foo.barRUBY1: 422: "hello"3: Integer4: untyped42はConst[42]なので42と細かく出ます1 + 2は丸めてIntegerになりますfoo.barはわからないのでuntypedです- 「
untypedがどこに出るか」は「Rigorが型を見失った場所」であり、これが見えること自体がannotateの値打ちです(実Rigorのsig-genの発想の芽)
1-4b. 診断を読みやすくする(位置とキャレット)
Section titled “1-4b. 診断を読みやすくする(位置とキャレット)”診断は行だけでなく列や長さも持たせると、どこが問題かを指せます。Prismのノードは位置情報locationを持っているので、diagnosticをひとさじ拡張します。
def diagnostic(node, message) location = node.location { line: location.start_line, column: location.start_column, length: location.length, message: message }endするとCLI(exe/chibirigor)は、該当行の下にキャレット^^^を引けます。
$ ruby exe/chibirigor check bad.rbbad.rb:2:1: expected Integer but got "bad" 1 + "bad" ^^^^^^^^^たったこれだけで「どの行の、どの語が」問題かが一目で分かります。実Rigorの診断はここをさらに作り込み、SARIFやGitHubの注釈に変換されます(ADR-51)。
1-5. この章のまとめ
Section titled “1-5. この章のまとめ”作ったものは、型Const/Dynamic/Nominal、関数type_of/check/annotateです。全部で50行ほどです。『しくみ』2章のtypecheck(約40行)に、「丸め」「止まらない」「untypedに逃がす」が少し足された規模感です。
この章の三つの視点:
| 内容 | |
|---|---|
| ① 型理論(『しくみ』2章 / 『TAPL』8章) | 型は値につけるラベル=ただのデータ。式から型を求める関数が心臓 |
| ② Ruby/RBS | 1はInteger、+すらメソッド送信。RBSは「1はInteger」止まり |
| ③ Rigor実装の問題 | 値そのものを型にする(Const[1])と細かいが、いつIntegerに丸めるかが問題になる(続編で詳しく扱う) |
annotate("3.14")とannotate("true")の結果を確かめ、Const#to_sがどう効いているか説明せよ。- いまの
type_of_callは+だけを丸めます。1 - 2もIntegerになるよう拡張せよ(ヒント:条件を:+/:-の両方に)。 1 + 2 + 3をcheckすると診断は出ません。なぜ矛盾なく通るのか、type_ofの再帰で説明せよ。
次章予告:1 + 2で素通りした「+もメソッド送信」を、きちんと扱います。Rubyは何でもメソッド送信なので、「どのクラスのどのメソッドが何を返すか」を引く表(ディスパッチ)が要ります。そして「知らないメソッドはDynamicに逃がす」の続きと、§1-4で予告した定数畳み込み(畳めれば畳む)を発展ノートで扱います。
この章の実装(演習の答え合わせにも) →
impls/dist/part1/lib
© 2026 TypedDuck. Licensed under CC BY-SA 4.0.