コンテンツにスキップ

構造的インターフェースとオブジェクトシェイプ

RigorはRBSインターフェース、内部オブジェクトシェイプ(shape)、整理されたケイパビリティ(capability)ロールカタログを通じてRubyのダックタイピング(duck typing)をモデル化します。クラスとモジュール名は名前的のままです;構造的型付け(structural typing)は特定の境界でのみ適用されます。

_ClosableのようなRBSインターフェース型は名前付き構造的契約(contract)です。内部オブジェクトシェイプはローカル定義、シングルトンメソッド、モジュールメンバー、インクルードされたモジュール、プラグインファクト(fact)、または制御フローガードから推論される匿名の構造的型です。名前的型(nominal type、公称型とも)またはオブジェクトシェイプは、Rigorが互換性のある型を持つすべての必要なメンバーを提供することを証明できる場合、インターフェースに代入可能です。

この文書は以下を定義します:

  • 構造的型付けが適用される境界;
  • 代入可能性とメンバー互換性のルール;
  • 可視性、リーダー/ライターケイパビリティ、respond_to?セマンティクス;
  • オブジェクトシェイプエントリーとMethodEntryレコードのスキーマ;
  • ケイパビリティロールカタログとロール推論を制限する規律;
  • 診断対ヒントのエスカレーションルール。

プラグイン提供ファクトとRBS::Extendedアノテーションの具体的なルールはrbs-extended.mdにあります。

構造的型付けが適用される場所

Section titled “構造的型付けが適用される場所”

RigorはTypeScriptスタイルで通常のクラス間互換性をデフォルトで構造的にしてはなりません(MUST NOT)。クラスとモジュール名は名前的のままです。なぜならRBSはそれらの名前をRuby定数に関する宣言として使い、is_a?kind_of?のようなRubyランタイムチェックがクラス/モジュール関係に依存するからです。

構造的型付けはこれらの境界で適用されます:

  • RBSインターフェースが期待される場所に値を代入またはパスする場合;
  • 推論されたオブジェクトシェイプがインターフェースを満たすかチェックする場合;
  • 既知のシェイプに対して直接のメソッド送信をチェックする場合;
  • プラグイン提供の動的リフレクションを使ってシェイプまたは名前的型にメンバーを追加する場合。

これにより新しい表面構文を追加せずに擬似プロトコルモデルがRigorに与えられます:

interface _Closable
def close: () -> void
end
class Resource
def close
@handle.close
end
end
def close_all(items)
items.each(&:close)
end
# Rigorが`items`が`Array[_Closable]`であることを知っている場合、`Resource`は
# 構造的に`_Closable`を満たせます。RubyのInheritanceやRuntimeマーカーは不要です。
  • 具体的な名前的型は、そのインスタンスメソッドシェイプがすべてのインターフェースメンバーを満たす場合、インターフェースに代入可能です。
  • オブジェクトシェイプは、シェイプが代入可能なシグネチャを持つすべての必要なメンバーを含む場合、インターフェースに代入可能です。
  • 一つのインターフェースは、ソースインターフェースがターゲットインターフェースが必要とするすべてのメンバーを提供する場合、別のインターフェースに代入可能です。
  • インターフェースユニオン(union、合併型とも)は通常のユニオンのように振る舞います。
  • インターフェース積集合はすべての積集合インターフェースからすべてのメンバーを必要とします。
  • 呼び出し可能なオブジェクトシェイプは、シグネチャが互換性がある場合、既知のcallメソッドを通じてprocまたはインターフェース的な呼び出し契約を満たす場合があります(MAY)。
  • シングルトンクラスとモジュールオブジェクトシェイプは、シングルトンメソッドとモジュールレベルのメンバーを通じてインターフェースを満たす場合があります(MAY)。これはインスタンス側の構造的チェックの後に実装すべきです(SHOULD)。

メンバー互換性は名前の存在だけでなく、メソッド型互換性に従います。Rigorはそれらのメソッド代入可能性ルールが存在するようになったら、通常のメソッド代入可能性ルールを通じて可視性、アリティ(arity)、位置パラメータ、キーワードパラメータ、ブロック、オーバーロード、リターン型、レシーバー制約を比較しなければなりません(MUST)。

リーダーとライターのケイパビリティ

Section titled “リーダーとライターのケイパビリティ”

リーダーとライターのケイパビリティはフィールド宣言ではなくメソッドケイパビリティです。attr_readerattr_writerattr_accessorはメソッドファクトのソースです; Rigorは結果として得られるxx=メソッドをシェイプ上の別々のエントリーとしてモデル化します。

  • 読み取り専用メンバーはリーダーメソッドで表され、その戻り値型において共変(covariant)です。
  • 書き込み専用メンバーはライターメソッドで表され、受け付ける値型において反変(contravariant)です。
  • attr_accessorペアのような読み取り-書き込みメンバーはリーダーとライターの両方の要件を組み合わせ、値型において実質的に不変です。

アクセサ構文はこれらのメソッドファクトの一つのソースです:

  • attr_reader :xは周囲のRuby可視性状態が変更しない限り公開リーダーメソッドxを貢献します。
  • attr_writer :xはライターメソッドx=を貢献し、リーダーを意味しません。
  • attr_accessor :xは両メソッドを貢献しますが、Rigorはそれらを2つのメソッドエントリーとしてモデル化しなければなりません(MUST)。
  • 手動で定義またはオーバーライドされたxまたはx=メソッドは、通常のRubyメソッドルックアップとソース順序に従ってメソッドファクトを置換または絞り込みます。

リーダーとライターのケイパビリティは純粋性を意味しません。リーダーは状態をミューテートする場合があり(MAY)、ライターはシグネチャまたは実装が別に証明しない限り任意のRuby値を返す場合があります(MAY)。

可視性はすべてのメソッドシェイプエントリーのファーストクラスのファセットです。Rigorは少なくともpublicprotectedprivateと、メンバーが使える呼び出しコンテキストを追跡しなければなりません(MUST):

  • 外部の明示的レシーバー送信は公開メソッドを必要とします。
  • プライベートメソッドはプライベート呼び出しコンテキストでのみ呼び出せます(MAY)。通常の明示的レシーバー送信としてではありません。
  • プロテクテッドメソッドはRubyのprotectedレシーバー制限に従い、デフォルトでは公開構造的インターフェース要件を満たしてはなりません(MUST NOT)。
  • 公開構造的インターフェースは、インターフェースまたは内部チェックが明示的に別の可視性を求めない限り、公開メンバーを必要とします。

respond_to?チェックはオブジェクトを存在のみのシェイプ(例: 「公開メソッドcloseを持つ」)に絞り込む場合があります(MAY)。そのファクトは診断とガードされた送信に有用ですが、Rigorがメソッド型も知らない限り、インターフェースとの完全なシグネチャ互換性を証明しません。

オプションのinclude_private引数は可視性ファクトに影響しなければなりません(MUST):

  • obj.respond_to?(:foo)は真ブランチでfooの公開存在ファクトを記録します。
  • obj.respond_to?(:foo, false)は第2引数が静的にfalseの場合、デフォルトと同じです。
  • obj.respond_to?(:foo, true)は可視性がpublic、protected、またはprivateである存在ファクトを記録します。それ自体ではobj.fooが外部の明示的レシーバー呼び出しとして合法であることを証明しません。
  • 第2引数が静的に既知でない場合、Rigorはより弱いmaybe-private可視性ファクトを記録しなければなりません(MUST)。

メソッドがrespond_to_missing?またはmethod_missingを通じてのみ存在する場合、ファクトは動的provenanceと未知またはプラグイン提供のシグネチャで記録されなければならず(MUST)、診断が呼び出しが受け付けられた理由を説明できるようにします。

オブジェクトシェイプエントリースキーマ

Section titled “オブジェクトシェイプエントリースキーマ”

オブジェクトシェイプエントリーはRubyの動的サーフェス(surface)を静的プロトコル証明と混同しないように十分なメタデータを持たなければなりません(MUST):

  • メンバーの種類(メソッド、リーダー、ライター、定数、インデックス操作など);
  • 呼び出しシグネチャまたは読み取り/書き込み可能な値型;
  • 可視性と有効な呼び出しコンテキスト;
  • ソース定義、RBS、プラグイン、respond_to?method_missingのようなソースとprovenance;
  • 安定性とミューテーション情報;
  • yesmaybenoのような確実性relations-and-certainty.md参照)。

メソッドエントリー(MethodEntry

Section titled “メソッドエントリー(MethodEntry)”

最初の実装では1つのメソッドシェイプエントリーを1つのresolvedされたRubyメソッド本体とペアにします:

  • MethodEntry(クラスまたはモジュール,メソッド名)ごとの1レコードであり、そのクラスまたはモジュールでその名前のランタイム解決されたメソッド本体に対応します。Rubyはランタイムでシグネチャごとのオーバーロードを持たないため、同じクラスの複数のdef foo定義は単一のエントリーに折り畳まれます。
  • 可視性はエントリーレベルで格納されます。private :fooおよび同様の可視性切り替えはメソッド全体に作用し、特定のシグネチャバリアントには作用しません。
  • RBSオーバーロード、RBS::Extendedペイロード、プラグイン貢献からのシグネチャバリアントはエントリー内のブランチリストとして格納されます。ブランチはエントリーの可視性を共有しますが、異なる引数シェイプ、リターン型、述語効果、ミューテーション効果を持つ場合があります(MAY)。
  • 条件付きdef、条件付きprivate、およびその他の動的に構築されたメソッド定義は最初の実装のスコープ外です。それらは通常の診断または動的由来ファクトとして浮上します。

オープンクラス、再オープン、モンキーパッチは並行するものを生成するのではなく、同じエントリーに貢献します:

  • ファイルをまたぐ各def fooは候補定義を貢献します。デフォルトのマージポリシーはRubyのランタイム解決に従います: RubyがDispatishする候補が勝ちます。通常の同クラス再定義の中では、これはソース順序と最後定義優先の解決です;祖先チェーンの中では、これはRubyがprependからクラスからincludeされたモジュールから親クラスチェーンへのルックアップ順序を使います。
  • Strictモードでは、明示的なオーバーライドマーカーなしにRBS可視シグネチャまたは可視性を変更する再定義で診断を発生させます(仮称rigor:v1:override=replace; rbs-extended.md参照)。そのマーカーが存在するまで、strictモードは疑わしいサイレントモンキーパッチを報告します。
  • モジュールインクルードとリファインメントはホストクラスのエントリーにフラット化されません。それらは所有するモジュールに残り、祖先チェーンを通じたルックアップに参加します。

Rigorはグローバルクラス等価ではなく、ケイパビリティロールとして一般的なRubyの「IO的」関係をモデル化します。

IOStringIOは動機となる例です。コードがストリームの読み取り、書き込み、巻き戻し、クローズのみを行う場合、StringIOはしばしばIOオブジェクトの良いテストダブルです。それはIOのサブクラスではなく、同じ完全なメソッドセットを持ちません。StringIOIOの部分型(subtype)として扱うと実際のランタイムの違いが消去されてしまいます。すべての実装にIO | StringIOと書くことを要求するのもRubyダックタイピングのポイントを外しています。

モデルは以下の通りです:

  • IOは実際のIOオブジェクトまたはファイルディスクリプタバックド挙動を必要とするAPIの名前的型のままです。
  • StringIOは別個の名前的型のままです。
  • 両クラスは読み取り可能、書き込み可能、シーク可能、フラッシュ可能、またはクローズ可能なストリームロールのような、より小さな構造的インターフェースを満たす場合があります(MAY)。
  • ストリームケイパビリティメソッドのみを呼び出すメソッドは、名前的IO型全体ではなく、対応するオブジェクトシェイプまたは名前付きインターフェースを必要とすると推論すべきです(SHOULD)。
  • ファイルディスクリプタ操作のようなIO固有のメンバーを呼び出すメソッドはIOまたはより具体的なファイルディスクリプタバックドロールを必要とすべきです(SHOULD)。

Rigorは一般的なスタンダードライブラリのケイパビリティロールの意見を持つコアカタログを提供しなければなりません(MUST)。カタログはRubyとスタンダードライブラリがすでに提供している場所で既存のRBS定義インターフェースを再使用し、既存のインターフェースが欠けているか明確に異なるケイパビリティを混同する場所でのみ少数のRigor固有ロールを追加します。

再使用されたRBSインターフェース(Rigorによって再定義されるのではなく、既存のRBSシェイプによってマッチされます):

インターフェース使用
_Each[T]Tに対するEnumerable反復
_Readerストリーム的な読み取りアクセス
_Writerストリーム的な書き込みアクセス
_ToSto_sを通じた暗黙の文字列変換
_ToStrto_strを通じた明示的な文字列強制変換
_ToIntto_intを通じた明示的な整数強制変換
_ToProcto_procを通じたブロック変換
_ToHash[K, V]to_hashを通じたHash強制変換
_ToA[T]to_aを通じたArray変換
_ToAry[T]to_aryを通じた厳密なArray強制変換
Enumerable[T]広範なコレクションプロトコル、ロールマッチングの名前的インターフェースとして扱われます
Comparable順序付けプロトコル、ロールマッチングの名前的インターフェースとして扱われます

最初のマイルストーンで追加されたRigor固有ロール(それぞれRigorのバンドルされたシグネチャに明示的なRBSインターフェースを持ちます):

ロール目的必要なメンバー
_RewindableStream先頭から再生できるストリーム的オブジェクトreadrewind
_ClosableStreamライフタイムをクローズできるストリーム的オブジェクトcloseclosed?
_FileDescriptorBacked実際のIOを必要とする診断を正当化する実OSバックドストリームfileno
_Callable[**A, R]callに応答するもの(_ToProcとは別)call(*A) -> R

プラグインはフレームワークロール、追加の適合ファクト、ロール固有の除外、maybe適合を追加する場合がありますが(MAY)、このカタログの再使用されたRBSインターフェースまたはRigor固有のロールのいずれかをサイレントに置換してはなりません(MUST NOT)。

以下のロール名とメソッドシグネチャは説明的なものであり、最終的なスタンダードライブラリシグネチャではありません:

interface _Reader
def read: (*untyped) -> String?
end
interface _RewindableStream
def read: (*untyped) -> String?
def rewind: () -> untyped
end
def slurp(stream)
stream.rewind
stream.read
end
# 推論された要件: _RewindableStream
# `IO`と`StringIO`はシグネチャが一致すれば両方ともその要件を満たせます。

これにより完全なメソッドセットの比較が避けられます。構造的サブタイピング(subtyping)は値がターゲットロールの必要なメンバーを提供するかを尋ねます;ソースオブジェクトとターゲットオブジェクトが同じ完全なサーフェスを公開することを必要としません。

メソッドが受け取ったのと同じストリームオブジェクトを返す場合、Rigorはロールに拡幅するのではなく、ジェネリクスを通じて具体的な入力型を保持すべきです(SHOULD):

def reset: [S < _RewindableStream] (S stream) -> S

ジェネリック保持はロール抽出とは別のルールです。メソッドが受け取ったのと同じパラメータオブジェクトを返す場合、本体がオブジェクト同一性を保持するとき、Rigorは[S < _RewindableStream] (S stream) -> Sのような型変数を優先すべきです(SHOULD)。パラメータ要件が構造的であるからといって、リターンを_RewindableStreamに拡幅してはなりません(MUST NOT)。本体が値を置換したり、無関係なオブジェクト間で分岐したり、委譲されたオブジェクトを返す場合があるなら(MAY)、Rigorは通常の推論されたリターン型にフォールバックすべきです(SHOULD)。

実装が本当にクラス固有の挙動を持つ場合、ユニオンは有用なままです。メソッドがIOStringIOで分岐し、各クラス固有のメンバーを呼び出し、クラス固有の値を返す場合、IO | StringIOは忠実な型です。通常のダックタイプされたストリーム消費には、ケイパビリティロールが好ましいモデルです。

RBS消去は一致する名前付きインターフェースが存在する場合はそれを優先すべきです(SHOULD)。既知のインターフェースにマッチしない匿名オブジェクトシェイプは保守的な名前的ベースまたはtopに消去されます。消去アルゴリズムはrbs-erasure.mdにあります。

ケイパビリティロール推論の規律

Section titled “ケイパビリティロール推論の規律”

ケイパビリティロール推論は制限されなければなりません(MUST)。Rigorはすべての呼び出しサイトを繰り返し再解析するのではなく、各パラメータとレシーバーのメソッドごとの要件サマリーを推論すべきです(SHOULD)。サマリーには、メソッド本体が実際に必要とするメンバー(メソッド名、可視性、アリティ、キーワードとブロック要件、リターン使用制約、ミューテーション要件、provenance)が含まれます。Rigorが名前付きインターフェースまたは名前付きインターフェースの小さな積集合が良い表現であることを証明するまで、匿名のオブジェクトシェイプ要件です。

最初の実装はローカルで単調な推論を保持すべきです(SHOULD):

  • メソッド本体を関連するメソッドバージョンごとに1回解析し、要件サマリーをキャッシュします。
  • 直接呼び出しには既存のシグネチャまたはキャッシュされたサマリーを使います;デフォルトでは呼び出し先をインラインに再帰しません。
  • 再帰メソッドまたは相互再帰サマリーでは、未知または拡幅プレースホルダーから始め、小さな固定点バジェットまでのみ反復します。
  • sendpublic_send、未知のmethod_missing、動的委譲はプラグインまたはシグネチャが正確なターゲットを提供しない限り動的要件として扱います。
  • 診断に必要なメンバーセットを保持し、バジェットを超えるときに長いオーバーロード展開のような低価値な詳細を削除することで、大きな要件シェイプを拡幅します。

名前付きインターフェースマッチング

Section titled “名前付きインターフェースマッチング”

名前付きインターフェースマッチングはすべてのインターフェースのスキャンではなく、インデックス化すべきです(SHOULD)。Rigorは必要なメンバー名と可視性から候補インターフェースへのインデックスを維持する場合があります(MAY)。候補インターフェースは少なくとも1つの必要なメンバーを共有し、安価なアリティまたは可視性フィルタを通過した場合にのみ比較されます。候補セットが大きすぎる場合、Rigorは高価なグローバル検索を実行するのではなく、匿名シェイプを保持して一般化ヒントを避けるべきです(SHOULD)。

複数の名前付きインターフェースがマッチする場合、選択は決定論的で保守的でなければなりません(MUST):

  1. 正確なメンバーシグネチャマッチを優先します。
  2. 無関係な偶然のインターフェースより設定されたスタンダードライブラリロールを優先します。
  3. 追加の必要なメンバーが少ないものを優先します。
  4. 次に安定したレキシカル名順序。
  5. 複数の候補が意味的に曖昧なままなら、内部的に匿名シェイプを保持し、名前付きインターフェースの提案を発行しません。

名前付きロールの積集合は有用ですが、Rigorは数学的に最小のロール式を見つけるために無制限のセットカバー問題を解いてはなりません(MUST NOT)。最初の実装は厳密な候補制限の下で正確な単一インターフェースマッチ、明示的なスタンダードロールバンドル、または小さなgreedy積集合のみを使う場合があります(MAY)。それ以外は匿名シェイプを保持します。

候補制限はbudgets.interface_candidatesです; inference-budgets.mdを参照してください。

明示的な宣言はまだ重要です。外部RBSシグネチャがパラメータをIOと言う場合、Rigorはそれを公開名前的契約として扱わなければなりません(MUST)。実装と観測された呼び出しサイトが_Readerのみを必要とする場合、Rigorは宣言された型が推論されたケイパビリティ要件より狭いことを報告し、シグネチャを構造的インターフェースに汎化することを提案する場合があります(MAY)。公開IO契約を静かに構造的なものに書き直してはなりません(MUST NOT)。

推論されたロールと宣言された型が一致しない場合に使われるエスカレーションルールは明示的です:

  • 診断。 宣言されたパラメータ型を満たさない呼び出しは常に報告されます。本体がどのように実装されているかに関係なく。このレベルはどの設定とも無関係です。
  • ヒント。 本体の推論されたロールが宣言された名前的型より厳密に小さく、構造的インターフェースへの汎化がまだ型チェックされる場合、Rigorはhint.role-generalization.*診断を発行する場合があります(MAY)。ヒントはstyle.suggest_role_generalization設定スイッチでゲートされ、名前的契約を意図的に選んだライブラリがそこから押し出されないようにデフォルトでオフです。
  • サイレント。 それ以外の場合、推論結果は内部的に保持され、呼び出し元、リファクタリングツール、プラグインのScope APIに利用可能ですが、診断は発行されません。

3つのレベルはどの与えられたサイトでも相互排他的です。Rigorは同じパラメータで呼び出しを拒否しながらヒントを提供してはならず(MUST)、公開名前的契約を静かに構造的なものに書き直してもなりません(MUST)。

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