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のProtocol) | interface | 型束における構造的な型。 | RBSのinterface _Foo、sig/から読み込まれる |
「app/actions/配下のすべてのアクションは#handleを定義しなければならない」── ツールによって強制されるフレームワークの規約 | protocol contract | ファイルパスによってクラスに束縛される振る舞い的契約。プラグインが宣言する。 | プラグインのprotocol_contracts:マニフェストフィールド(ADR-28) |
この二つは本当に異なる軸であり、一つのものの二つの味付けではありません。そしてこのページの残りの大部分は、この二つを区別し続けることについて述べます。一行で言える判別基準は次のとおりです。
- interfaceはシグネチャ内で名前を挙げる型です。検査はインターフェースが言及される場所で起こります(
def f: (_Closable) -> voidはfの引数を検査します)。 - 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 ideainterface _SupportsClose def close: () -> voidend互換性のあるシグネチャで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──rewind/closeのケイパビリティ。_Callable──callに応答する。
これらはIOとStringIOを別々の名前的型として保ちつつ、メソッドが実際に必要とするより小さな構造的ロールをそれぞれが満たせるようにします。これが構造的型付けの利点(具体的なクラスではなくケイパビリティに対して書く)であり、Rubyの実行時が依拠する名前的同一性を手放さずに済みます。
「Rigorのプロトコル」を探してここに来たのなら、このセクションがそれです。RBSインターフェース+オブジェクトシェイプ+ケイパビリティロールがRigorの構造的型付けのサーフェスです。そのいずれも「protocol」とは綴られていません。
言葉と意味論、言語をまたいで
Section titled “言葉と意味論、言語をまたいで”なぜRigorは構造的型をinterfaceと綴り、protocolを別のもののために取っておくのでしょうか。それは「protocol」という言葉と「構造的型付け」という意味論が言語をまたいで乖離してきたからであり、Rigorはruby読者を最も驚かせない綴りを選ぶからです(RBSはすでにinterfaceと言っています)。
この用語自体は古いものです。Smalltalk(1970年代)は、オブジェクトが理解するメッセージの集合をそのprotocolと呼び、クラスブラウザでは「メッセージプロトコル」にまとめていました。それは静的検査の構成要素では決してなく、ただ「このオブジェクトに何を送れるか」を名付けたもので、Rubyのダックタイピングの直接の祖先です。後続のすべての用法はこの核心的な考え ── 「適合するオブジェクトが提供するメソッドの集合」 ── を受け継ぎ、そのうえで適合性がどのように確立されるかについての独自の規則を追加します。
そうした規則のうち二つが重要であり、それらは綴りとは独立しています。
- 構造的/暗黙的 ── メソッドを持っていることによって適合する。宣言は不要。(Pythonの
Protocol、Goのinterface、RBSのinterface、Smalltalkの本来の意味。) - 名前的/明示的 ── 適合すると宣言することによって適合する。(Java/PHPの
interface ... implements、Swift/Objective-Cの明示的な採用を伴うprotocol。)
綴りは規則を追跡しません ── この二つはずっと前に交差しました。
| 言語 | 綴り | 適合性 | Rigorと同じか? |
|---|---|---|---|
| Smalltalk | ”protocol” | (動的、ダックタイピング) | この考えの祖先 |
| Rigor / RBS | interface | 構造的/暗黙的 | — |
| Python (PEP 544) | Protocol | 構造的/暗黙的 | ✅ 同じモデル、異なる言葉 |
| Go | interface | 構造的/暗黙的 | ✅ 同じモデル、同じ言葉 |
| Java / PHP | interface | 名前的/明示的なimplements | ❌ 同じ言葉、正反対のモデル |
| Swift / Objective-C | protocol | 名前的/明示的な採用 | ❌ 異なる言葉かつ異なるモデル |
つまり読者の直感は、その人がどこから来たかに完全に依存します。
- 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はあなたのProtocol/interfaceです ── 構造的、暗黙的、名前が挙げられた場所で検査される。
そしてSmalltalkの意味 ── 「適合する型が提供しなければならない、名前付きのメッセージの集合」 ── こそ、Rigorが次のセクションでプロトコル契約の名のもとに復活させるものにほかなりません。ふさわしいことに、それがRigorが実際に「protocol」と綴る唯一のものです。
プロトコル契約(ADR-28)
Section titled “プロトコル契約(ADR-28)”さて、もう一方の軸です。Rack型のWebフレームワークは、コントローラーアクションがリクエストを受け取りレスポンスを返すことを期待します。ジョブフレームワークは#performを期待します。シリアライザは#callを期待します。この規約は実在しますが、それはフレームワークの説明文の中に住んでいます ── それを記録するクラス宣言は存在せず、それを検査するものも何もないため、違反は実行時の予期せぬ事態となります。
RBSインターフェースはこれを表現できません。RBSは(Pythonと同様に)「このディレクトリ配下のすべてのクラスはインターフェースIを実装する」という形式を持ちません。インターフェースはシグネチャがそれを名指す場所でしか効きませんが、これらのコントローラーは何も名指していません。その隙間こそ、ADR-28のプロトコル契約が埋めるものです。
フレームワークを知るプラグインは、そのマニフェストに契約を宣言します(param_typesは位置引数ごとの{ index:, type_name: }供給の配列です)。
# inside a framework plugin's manifest — an illustrative serializer contractprotocol_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)を確認します。
供給の側こそが要を担います。それがなければrequestはDynamic[Top]になり、これはあらゆるメソッドに応答するため、それから組み立てられるどんな戻り値もまたDynamic[Top]となり、戻り値の検査は空虚になります。
(プラグイン作者ではなく)アプリケーション開発者として注意すべきことが二つあります。
- あなたは
protocol_contracts:を決して書きません ── フレームワークのプラグインが書きます。あなたはただのdef handle(request)を書けば、それが無償で検査されます。 missing-protocol-method/protocol-return-mismatchの診断はプラグイン診断であり、プラグインのplugin.<id>.という由来のもとで発行されます ── コアのRigorルールではありません。実例の参照先はexamples/rigor-web/(最小限のチュートリアル)とplugins/rigor-hanami/(本番のHanami 2のアクション)です。
インターフェース対プロトコル契約
Section titled “インターフェース対プロトコル契約”RBSのinterface(構造的型) | ADR-28のプロトコル契約 | |
|---|---|---|
| それは何か | 型束の中の型 | ツールによって強制される規約。型ではない |
| 言語をまたいだ対応物 | PythonのProtocol、Goのinterface | Smalltalkの「必要なメッセージ集合」の意味 ── ただしファイルパスによって束縛される。これは主流のprotocol/interfaceのいずれも持たない仕組み |
| クラスがどうオプトインするか | 構造的に ── ただメソッドを持てばよい | 暗黙的に ── パスglobの配下に定義されればよい |
| どこで参照されるか | シグネチャ内で名指される((_Closable) -> void) | どこでも名指されない。ファイルパスによって束縛される |
| 検査がどこで発火するか | インターフェースを名指す使用箇所で | 契約されたdefで(供給)+クラスごとに(検査) |
| 誰が宣言するか | .rbsを書く者 | フレームワークプラグインのマニフェスト |
| 引数型を供給するか? | しない(型であり、名指された場所で使われる) | する ── 注釈のないdefへ |
| 診断 | 使用箇所でのコア型エラー | missing-protocol-method、protocol-return-mismatch(プラグイン) |
この命名の重複は歴史的なものです(上記の言語をまたいだ寄り道を参照)。Smalltalkの「必要なメッセージの集合」の意味はプロトコル契約の中に生き残り、一方でPythonのtyping.Protocolは、Ruby/RBSがinterfaceと綴る構造的型の考えのためにこの言葉を再利用しました。よってRigorでは、「protocol」が構造的型を意味することは決してありません。
自分が欲しいのはどちら?
Section titled “自分が欲しいのはどちら?”#closeを持つものなら何でも受け取るメソッドを書きたい → それは構造的型です。RBSのinterface _Closableを宣言してそれに対して注釈を付けます(または無名の場合は推論されるオブジェクトシェイプに頼ります)。第7章 ── RBSとRBS::Extendedを参照。- あるディレクトリ内のすべてのクラスが、与えられた引数/戻り値型を持つフレームワークのメソッドを実装することを強制したい → それはプロトコル契約であり、プラグイン作成の機能です。ADR-28と
examples/のウォークスルーを参照。 - あなたがそのようなフレームワークを使うアプリケーション開発者である → あなたはどちらも明示的には行いません。慣用的なRubyを書けば、フレームワークのプラグインが契約を供給し、Rigorがそれに照らしてあなたのアクションを検査します。
missing-protocol-methodを見たら、それはフレームワークが要求するメソッドを忘れたということです。protocol-return-mismatchを見たら、それはあなたのアクションが間違ったシェイプを返しているということです。
次に読むもの
Section titled “次に読むもの”- 第7章 ── RBSとRBS::Extended ── インターフェースが住む
.rbsの書き方。 - 第9章 ── プラグインとexamples/のランディングページ ── プロトコル契約が作成される場所。
docs/type-specification/structural-interfaces-and-object-shapes.md── インターフェース、オブジェクトシェイプ、ケイパビリティロールの規範的仕様。- ADR-28 ── プロトコル契約の設計判断と、却下された代替案(ディレクトリによって束縛されるRBSインターフェースがなぜその道ではなかったかを含む)。
- 別のチェッカーから来た?mypy / Pyright付録が
Protocol↔RBSのinterfaceをPython側から対応づけ、型理論付録が名前的型付け対構造的型付けをより広い見取り図の中に位置づけます。
© 2026 TypedDuck. Licensed under CC BY-SA 4.0.