コンテンツにスキップ

Appendix — Protocols, interfaces, and structural typing

Pythonから来た読者にとって、「protocol」は一つの特定の意味を持ちます。すなわちtyping.Protocol、PEP 544の構造的型付け(structural typing)です。クラスは適切なメソッドを持っていることによってプロトコルを満たし、継承は不要です。これは「静的なダックタイピング(duck typing)」であり、Python型付けユーザーが真っ先に手を伸ばすものの一つです。

その直感は正しいのですが、Rigorではこの言葉が罠になります。Rigorの構造的型付け機能は「protocol」とは呼ばれません。それはRBSのinterfaceです。一方で「protocol」という言葉はRigorにも登場しますが、それは別の機能、すなわちフレームワークのパススコープな振る舞い的契約ADR-28)を指します。この付録は、あなたが正しい方を選べるよう、この二つを解きほぐします。

一行版。 Rigorのinterface構造的です ── GoのinterfaceやPythonのProtocolと同じです。クラスはメソッドを持っていることで適合します。implements句はありません(Rubyにはそもそも存在しません)。これはJava/PHPの名前的なinterface、すなわちクラスが名前で適合を宣言するものとは異なります。素の「interface」という言葉は多くのRuby開発者にはJava/PHP流のものとして読まれてしまう(Rubyにinterfaceキーワードがないため、直感が外部から持ち込まれる)ので、Rigorのドキュメントは初出時にそれを限定します ── 「構造的インターフェース」または「RBSインターフェース」と ── あなたもそれについて書くときはそうすべきです。

この付録の内容 「protocol」と呼ばれる二つのもの · 構造的型付け:RBSのインターフェース · オブジェクトシェイプとケイパビリティロール · 言葉と意味論、言語をまたいで · プロトコル契約(ADR-28) · インターフェース対プロトコル契約 · 自分が欲しいのはどちら? · 次に読むもの

「protocol」と呼ばれる二つのもの

Section titled “「protocol」と呼ばれる二つのもの”
あなたが言いたいのは…Rigorでの言葉それは何かどこに存在するか
「静的なダックタイピング」── クラスがメソッドを持っていれば適合する(PythonのProtocolinterface型束における構造的なRBSのinterface _Foosig/から読み込まれる
app/actions/配下のすべてのアクションは#handleを定義しなければならない」── ツールによって強制されるフレームワークの規約protocol contractファイルパスによってクラスに束縛される振る舞い的契約。プラグインが宣言する。プラグインのprotocol_contracts:マニフェストフィールド(ADR-28)

この二つは本当に異なる軸であり、一つのものの二つの味付けではありません。そしてこのページの残りの大部分は、この二つを区別し続けることについて述べます。一行で言える判別基準は次のとおりです。

  • interfaceはシグネチャ内で名前を挙げるです。検査はインターフェースが言及される場所で起こります(def f: (_Closable) -> voidfの引数を検査します)。
  • protocol contract適合するクラスやどのシグネチャからも一切言及されません。プラグインが「このディレクトリ配下のクラスはこの契約を担う」と述べ、エンジンが暗黙的にそれらを供給・検査します。

構造的型付け:RBSのインターフェース

Section titled “構造的型付け:RBSのインターフェース”

これはPythonのProtocolの直接の対応物です。RBSは最初のリリースから構造的なinterface _Fooを持っています。Rigorはそれらをsig/から読み込み、適合性を構造的に検査します。

# Python (PEP 544)
class SupportsClose(Protocol):
def close(self) -> None: ...
# RBS — the same idea
interface _SupportsClose
def close: () -> void
end

互換性のあるシグネチャでcloseを定義したクラスは、includeもスーパークラスも実行時マーカーもなしにそのインターフェースを満たします。マッチは構造的です。規範的仕様(structural-interfaces-and-object-shapes.md)より:

RBSのインターフェース型…は名前付きの構造的契約である。名前的型(nominal type)またはオブジェクトシェイプは、必要なメンバーすべてを互換性のある型で提供することをRigorが証明できるとき、インターフェースに代入可能である。

Rigorはどこで構造的マッチングを適用するのでしょうか。意図的に境界で適用し、デフォルトではクラス対クラスでは適用しません。

  • RBSインターフェースが期待される場所に値を代入または渡すとき。
  • 推論されたオブジェクトシェイプがインターフェースを満たすかどうかを検査するとき。
  • 既知のシェイプに対する直接のメソッド送信。
  • プラグインが提供する動的リフレクションがシェイプにメンバーを追加するとき。

通常のFoo/Barのクラス互換性は名前的なままです。Ruby自身のis_a?kind_of?はクラス階層に依存し、RBSはクラス名をRubyの定数についての宣言として使うため、Rigorはクラスについて完全にTypeScript風の構造的型付けには踏み込みません。構造的型付けは、PythonでProtocolに対して注釈を付けることでオプトインするのとまったく同じように、インターフェースの名前を挙げることでオプトインするツールです。(mypy / Pyright付録が同じ対応をPython側から扱っています。)

オブジェクトシェイプとケイパビリティロール

Section titled “オブジェクトシェイプとケイパビリティロール”

RBSインターフェースは名前付きの構造的型です。Rigorは値が実際に応答するものから無名の構造的型 ── オブジェクトシェイプ(object shape) ── も推論し、よくあるIO的な契約のためのケイパビリティロール(capability role)の厳選されたカタログを提供します。

  • _Reader_Writer ── ストリームの読み取り/書き込みの両半分。
  • _RewindableStream_ClosableStream ── rewindcloseのケイパビリティ。
  • _Callable ── callに応答する。

これらはIOStringIOを別々の名前的型として保ちつつ、メソッドが実際に必要とするより小さな構造的ロールをそれぞれが満たせるようにします。これが構造的型付けの利点(具体的なクラスではなくケイパビリティに対して書く)であり、Rubyの実行時が依拠する名前的同一性を手放さずに済みます。

「Rigorのプロトコル」を探してここに来たのなら、このセクションがそれです。RBSインターフェース+オブジェクトシェイプ+ケイパビリティロールがRigorの構造的型付けのサーフェスです。そのいずれも「protocol」とは綴られていません。

言葉と意味論、言語をまたいで

Section titled “言葉と意味論、言語をまたいで”

なぜRigorは構造的型をinterfaceと綴り、protocolを別のもののために取っておくのでしょうか。それは「protocol」という言葉と「構造的型付け」という意味論が言語をまたいで乖離してきたからであり、Rigorはruby読者を最も驚かせない綴りを選ぶからです(RBSはすでにinterfaceと言っています)。

この用語自体は古いものです。Smalltalk(1970年代)は、オブジェクトが理解するメッセージの集合をそのprotocolと呼び、クラスブラウザでは「メッセージプロトコル」にまとめていました。それは静的検査の構成要素では決してなく、ただ「このオブジェクトに何を送れるか」を名付けたもので、Rubyのダックタイピングの直接の祖先です。後続のすべての用法はこの核心的な考え ── 「適合するオブジェクトが提供するメソッドの集合」 ── を受け継ぎ、そのうえで適合性がどのように確立されるかについての独自の規則を追加します。

そうした規則のうち二つが重要であり、それらは綴りとは独立しています。

  • 構造的/暗黙的 ── メソッドを持っていることによって適合する。宣言は不要。(PythonのProtocol、GoのinterfaceRBSのinterface、Smalltalkの本来の意味。)
  • 名前的/明示的 ── 適合すると宣言することによって適合する。(Java/PHPのinterface ... implements、Swift/Objective-Cの明示的な採用を伴うprotocol。)

綴りは規則を追跡しません ── この二つはずっと前に交差しました。

言語綴り適合性Rigorと同じか?
Smalltalk”protocol”(動的、ダックタイピング)この考えの祖先
Rigor / RBSinterface構造的/暗黙的
Python (PEP 544)Protocol構造的/暗黙的✅ 同じモデル、異なる言葉
Gointerface構造的/暗黙的✅ 同じモデル、同じ言葉
Java / PHPinterface名前的/明示的なimplements❌ 同じ言葉、正反対のモデル
Swift / Objective-Cprotocol名前的/明示的な採用❌ 異なる言葉かつ異なるモデル

つまり読者の直感は、その人がどこから来たかに完全に依存します。

  • Swift/Objective-Cから:「protocol」は適合を宣言する型を意味します(struct Resource: Closable)。Rigorのinterfaceはそのような宣言を必要とせず、ただcloseを定義するだけです。(Objective-CのrespondsToSelector:と非形式プロトコルが実行時のダックタイピングの抜け道です。Swiftは拡張を通じた遡及的な適合を許しますが、採用は依然として明示的です。)そしてRigorで言葉を再利用する唯一のサーフェス ── 後述のプロトコル契約(protocol contract) ── もまた、Swiftのプロトコルではありません。それは採用句ではなくファイルパスによってクラスを束縛します。
  • Java/PHPから:「interface」はクラスが名前でimplementしなければならない契約を意味します。Rigorは言葉を再利用しますが規則は再利用しません。RubyのクラスはRBSのinterfaceを構造的に満たすのであって、implements句によって満たすのでは決してありません(Rubyにはimplements句がありません)。
  • PythonまたはGoから:あなたはすでにわが家にいます。RBSのinterfaceはあなたのProtocolinterfaceです ── 構造的、暗黙的、名前が挙げられた場所で検査される。

そしてSmalltalkの意味 ── 「適合する型が提供しなければならない、名前付きのメッセージの集合」 ── こそ、Rigorが次のセクションでプロトコル契約の名のもとに復活させるものにほかなりません。ふさわしいことに、それがRigorが実際に「protocol」と綴る唯一のものです。

さて、もう一方の軸です。Rack型のWebフレームワークは、コントローラーアクションがリクエストを受け取りレスポンスを返すことを期待します。ジョブフレームワークは#performを期待します。シリアライザは#callを期待します。この規約は実在しますが、それはフレームワークの説明文の中に住んでいます ── それを記録するクラス宣言は存在せず、それを検査するものも何もないため、違反は実行時の予期せぬ事態となります。

RBSインターフェースはこれを表現できません。RBSは(Pythonと同様に)「このディレクトリ配下のすべてのクラスはインターフェースIを実装する」という形式持ちません。インターフェースはシグネチャがそれを名指す場所でしか効きませんが、これらのコントローラーは何も名指していません。その隙間こそ、ADR-28のプロトコル契約が埋めるものです。

フレームワークを知るプラグインは、そのマニフェストに契約を宣言します(param_typesは位置引数ごとの{ index:, type_name: }供給の配列です)。

# inside a framework plugin's manifest — an illustrative serializer contract
protocol_contracts: [
Rigor::Plugin::ProtocolContract.new(
path_glob: "app/serializers/**/*.rb", # which files
method_name: :call, # the method every class must define
param_types: [{ index: 0, type_name: "ActiveRecord::Base" }],
return_type_name: "String",
severity: :error
)
]

するとエンジンは供給と検査(provide-and-check)を行います。

  • 供給(provide、エンジン側)。マッチするファイル内のdef call(record)を束縛するとき、契約のparam_typesが、注釈のない引数が通常受け取るはずのDynamic[Top]置き換えます。本体はその後、recordがその実際の型を担うかのように解析されます ── つまり本体内の誤用(record.no_such_column)は通常のcall.undefined-methodとして顕在化し、推論される戻り値型は精密になります。
  • 検査(check、プラグイン側)。プラグインは、マッチするファイル内のすべてのクラスがそのメソッドを定義していること(さもなくばmissing-protocol-method)、およびその推論される戻り値型がreturn_type_nameに適合すること(さもなくばprotocol-return-mismatch)を確認します。

供給の側こそが要を担います。それがなければrequestDynamic[Top]になり、これはあらゆるメソッドに応答するため、それから組み立てられるどんな戻り値もまたDynamic[Top]となり、戻り値の検査は空虚になります。

(プラグイン作者ではなく)アプリケーション開発者として注意すべきことが二つあります。

  • あなたはprotocol_contracts:決して書きません ── フレームワークのプラグインが書きます。あなたはただのdef handle(request)を書けば、それが無償で検査されます。
  • missing-protocol-methodprotocol-return-mismatchの診断はプラグイン診断であり、プラグインのplugin.<id>.という由来のもとで発行されます ── コアのRigorルールではありません。実例の参照先はexamples/rigor-web/(最小限のチュートリアル)とplugins/rigor-hanami/(本番のHanami 2のアクション)です。

インターフェース対プロトコル契約

Section titled “インターフェース対プロトコル契約”
RBSのinterface(構造的型)ADR-28のプロトコル契約
それは何か型束の中の型ツールによって強制される規約。型ではない
言語をまたいだ対応物PythonのProtocol、GoのinterfaceSmalltalkの「必要なメッセージ集合」の意味 ── ただしファイルパスによって束縛される。これは主流のprotocolinterfaceのいずれも持たない仕組み
クラスがどうオプトインするか構造的に ── ただメソッドを持てばよい暗黙的に ── パスglobの配下に定義されればよい
どこで参照されるかシグネチャ内で名指される((_Closable) -> voidどこでも名指されない。ファイルパスによって束縛される
検査がどこで発火するかインターフェースを名指す使用箇所で契約されたdefで(供給)+クラスごとに(検査)
誰が宣言するか.rbsを書く者フレームワークプラグインのマニフェスト
引数型を供給するか?しない(型であり、名指された場所で使われる)する ── 注釈のないdef
診断使用箇所でのコア型エラーmissing-protocol-methodprotocol-return-mismatch(プラグイン)

この命名の重複は歴史的なものです(上記の言語をまたいだ寄り道を参照)。Smalltalkの「必要なメッセージの集合」の意味はプロトコル契約の中に生き残り、一方でPythonのtyping.Protocolは、Ruby/RBSがinterfaceと綴る構造的型の考えのためにこの言葉を再利用しました。よってRigorでは、「protocol」が構造的型を意味することは決してありません。

  • #closeを持つものなら何でも受け取るメソッドを書きたい → それは構造的型です。RBSのinterface _Closableを宣言してそれに対して注釈を付けます(または無名の場合は推論されるオブジェクトシェイプに頼ります)。第7章 ── RBSとRBS::Extendedを参照。
  • あるディレクトリ内のすべてのクラスが、与えられた引数/戻り値型を持つフレームワークのメソッドを実装することを強制したい → それはプロトコル契約であり、プラグイン作成の機能です。ADR-28examples/のウォークスルーを参照。
  • あなたがそのようなフレームワークを使うアプリケーション開発者である → あなたはどちらも明示的には行いません。慣用的なRubyを書けば、フレームワークのプラグインが契約を供給し、Rigorがそれに照らしてあなたのアクションを検査します。missing-protocol-methodを見たら、それはフレームワークが要求するメソッドを忘れたということです。protocol-return-mismatchを見たら、それはあなたのアクションが間違ったシェイプを返しているということです。

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