コンテンツにスキップ

クラス

この章はクラス側の型付けを扱います — 異なる位置でのselfの意味、定数の解決、そしてRigorがattr_*Data.defineStruct.new宣言をどう読むか。物語というよりリファレンスです: 一度通して読み、必要なセクションに戻ってきてください。

この章の内容 インスタンス側とクラス側のself · 定数 · attr_* · インスタンス変数 · Data.define · Struct.new · 継承 · class / singleton(C) · カスタム=== · エイリアスクラス · モジュール · protected / private

インスタンス側とクラス側のself

Section titled “インスタンス側とクラス側のself”

インスタンスメソッド本体の内側では、selfは囲むクラスのNominal[T]です:

class User
def name
self # Nominal[User]
end
end

シングルトンメソッド本体の内側(def self.fooまたはdef User.foo)では、selfSingleton[T] — インスタンスではなくクラスオブジェクト自体 — です:

class User
def self.find(id)
self # Singleton[User]
end
end
User # Singleton[User]
User.find(1) # Nominal[User] (RBSで宣言)
User.new # Nominal[User]

この区別はメソッドディスパッチで重要です: インスタンスメソッドはNominal[User]で実行され、シングルトンメソッドはSingleton[User]で実行されます。Rigorはメソッドがどちら側にあるかを知るために、RBSシグのコロンの右側(def self.find: (Integer) -> User)を読みます。

定数ルックアップは4つのソースを順に辿ります:

  1. 字句スコープFooclass A; module B; ...の内側で参照されている場合、RigorはA::B::FooA::FooFooを探します。
  2. RBSコアとバンドルされたstdlibStringIntegerSymbolArrayPathnameURIOptParseJSONYAMLなど。
  3. プロジェクトRBS。 プロジェクトのsig/ファイルがルックアップに追加されます。
  4. インソースクラス探索。RBSが存在しない場合、Rigorはclass Foomodule Bar、定数代入(MAX = 100)を辿ります。
MAX = 100
class Counter
def initial = MAX
end
Counter.new.initial # Constant<100> — 定数値が
# インソースクラスルックアップを
# 通じて伝播する

Rigorがたたみ込める右辺を持つ定数にはConstant<value>型が付きます。そうでない定数には、より広いRBS消去形式が付きます。

Rigorはattr_*宣言を読み、メソッド定義として扱います。リーダーの戻り値型は対応するインスタンス変数の推論された型と一致します:

class User
attr_reader :name
def initialize(name)
@name = name
end
end
u = User.new("Alice")
u.name # Constant<"Alice"> — インソースディスパッチ +
# インスタンス変数追跡を通じて

attr_writerはセッターを公開します; attr_accessorは両方を公開します。セッターの引数型は呼び出し元が提供するものです。def.ivar-write-mismatchルール(v0.1.2)は、同じクラスボディ内の同じインスタンス変数への2つの書き込みが具体クラスで一致しているかチェックします — 正確な契約(contract)については第8章 — エラーの読み方を参照してください。明示的なインスタンス変数型を作成せずに、同じクラス内でのStringからArrayへの誤ったリバインドをキャッチできます。

メソッドをまたいだインスタンス変数

Section titled “メソッドをまたいだインスタンス変数”

Rigorはクラスのすべてのメソッドにわたってインスタンス変数の事実を蓄積します:

class Counter
def initialize
@count = 0 # init後の@count: Constant<0>
end
def bump
@count += 1 # @countがint<1, max>に再バインドされる
end
def value
@count # int<0, max> (見られた書き込みのユニオン)
end
end

各読み取り地点でのインスタンス変数型は、静的に見えるすべての書き込みのユニオン(union、合併型とも)です — 同じクラスの別のメソッドからの書き込みも含みます。

Data.defineは小さなイミュータブルな構造体を生成します。Rigorは宣言を認識し、コンストラクタのアリティ(arity)、フィールドごとのアクセサ、結果のクラス型を公開します:

Point = Data.define(:x, :y)
p = Point.new(x: 3, y: 4)
assert_type("Nominal[Point]", p)
assert_type("Constant<3>", p.x)
assert_type("Constant<4>", p.y)

探索はdefine_methodスタイルのブロック本体も辿るので、Point = Data.define(:x, :y) do ... endでも動作します。合成されたキーワード引数コンストラクタをオーバーライドするブロック定義のdef initialize(...)も含みます(v0.1.2)。同じルールがConst = Struct.new(*Symbol) do ... endにも適用されます — ブロックボディのメソッド発見が両方の形式にわたって均一に組み合わせられます。

Struct.new(*Symbol)は位置引数コンストラクタに加えてData.defineと同じアクセサを生成します。Rigorは両方の形式を処理します:

Coord = Struct.new(:x, :y)
c = Coord.new(10, 20)
assert_type("Constant<10>", c.x)
assert_type("Constant<20>", c.y)

Structはミュータビリティを追加します(アクセサはライターでもある)ので、インスタンス変数スタイルの蓄積が適用されます。Dataは読み取り専用です。

Nominal[Subclass]でメソッドを呼び出すと、Rigorはクラス階層を辿ります: まずサブクラスのRBS / インソース本体、次に各祖先のRBS / 本体、次に宣言順に含まれるモジュール。メソッドを定義した最初のものが勝ちます。

階層は次から読まれます:

  • RBSのclass Foo < Bar宣言。
  • インソースのclass Foo < Bar行。
  • Rigorが辿ったinclude / prepend / extend呼び出し。

階層が静的に不完全な場合(クラスがRigorが見つけられない親を参照している)、レシーバー型は最も深い既知の祖先にフォールバックします — Rigorが宣言を見たクラスに対しては、Dynamic[Top]になることはありません。

メソッドシグネチャが「クラスオブジェクト自体」を返すことがあります:

class Foo
def self.factory: () -> Foo # インスタンスを返す
def self.subclasses: () -> Array[singleton(Foo)] # クラスオブジェクトを返す
end

singleton(Foo)はクラスオブジェクトFooの型です。Singleton[Foo](Rigorの内部キャリア(carrier)表示形式)も同じ概念です。(Array[Foo]での)Fooは「Fooのインスタンス」/ Nominal[Foo]を意味します。

singleton(Foo)でインスタンスメソッドを呼び出すのはエラーです。ただしFoo自体がそのシングルトンメソッドを定義している場合は除きます — Stringsingleton(String)で、String#upcaseはインスタンスにあるので、String.upcasecall.undefined-methodをフラグします。

RigorはClass / Module / Range / Regexpに対して===を認識します — これらは標準のcase x; when …の形式です。ユーザークラスへのカスタムcase_eq実装は認識されません:

class IPv4
def self.===(s)
s.match?(/\A\d+\.\d+\.\d+\.\d+\z/)
end
end
case some_input
when IPv4
# Rigorはここで`some_input`をナローイングしません —
# IPv4.===はユーザー定義のcase等値で、エンジンは
# 特定のクラスにナローイングするとは証明できません。
some_input
end

このような場合、明示的なis_a? / respond_to?ガードを書くか、===メソッドにRBS::Extendedpredicate-if-trueディレクティブを使ってください(第7章参照)。

一部のRubyイディオムは定数代入でクラスエイリアスを作ります:

YAML = Psych

右辺がクラス自体の場合、Rigorはレシーバー型付けのためにエイリアスを追います — YAML.load(...)Psych.load(...)として扱われます。しかしメソッド存在チェックはエイリアス名に対して意図的に沈黙します;解析器はより多くのコンテキストなしに意図的なエイリアスと偶発的なシャドウを区別できないので、YAML.unknowncall.undefined-methodを発火しません。診断が必要な場合は正規名を使ってください。

型付けの目的では、module M; def foo; end; endはクラスと構造的に似ています。メソッドは同じように参照されます; include MMのメソッドをインクルードするクラスの階層に追加します。

extend selfスタイルのミックスインパターン(module_function / extend self)が認識されます — インスタンス側とシングルトン側の両方が同じメソッドを公開します。

Rigorは可視性修飾子を読み、def.method-visibility-mismatchルール(将来)の限定的なコンテキストでそれらを考慮します。今日、外部レシーバーへのプライベートメソッド呼び出しは診断を発火しません — 可視性は型システムの問題というよりもrubocop-styleリンターの関心事です。

第7章はRBSとRBS::Extendedを扱います — 推論だけでは証明できないものを超えるための外部シグネチャ表面です。

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