RBSのIntersection(&)について

自分向けのまとめです。

Intersection(&)

RBSにはIntersection型があります。Object & _Each[Integer]のように&で繋げている型のことです。

その前にUnion(|)とは

String | Symbolのように、|で繋いだ型をUnion型と言います。こちらは理解は簡単です。

「String もしくは Symbol のどちらか一方」という解釈になります。使用頻度は多めです。

Intersectionの使い方

それをふまえてObject & _Each[Integer]は何を意味しているのでしょうか?

Objectかつ_Eachインターフェースを持つってどういうこと?Objecteachメソッドはないよ???」

というのが、私が最初にIntersection型に抱いた感想でした。

型について学んでいくうちに、何となく言語化できたパターンがあったので書き残しておきます。

最低限classを保証したい

一番多い利用ケースは、BasicObjectの排除だと思います。 例えばeachメソッドを使うから_Eachを指定したいんだけど、nil?is_a?等もじつは使ってた場合。 この場合、_Each_Nil_Isaなんていう別のInterfaceを作ってもいいのですが、このnil?is_a?を使用しているのは、インターフェースというよりは単にObject class以上のオブジェクト(ややこしい)を想定しているだけである場合が多いです。こんな場合に使えるのがObject &という指定方法です。これで

「最低限Object classのインスタンスであることを保証しつつ、eachメソッドが使えること」

という型を表現できます。「最低限Object classのインスタンスである」ということは、言い換えれば「BasicObjectは除く」とも考えられます。

型同士を足し算したい

では本当にインターフェースとして複数のメソッドを使えることを表したい場合はどのような方法があるでしょうか?

1) 新しいinterfaceを作る

新しく以下のようなinterfaceを作るパターンです。

interface _Each_Nil_Isa
  def each: 略
  def nil?: 略
  def is_a?: 略
end

これでもいいのですが、もし_Eachが修正されたらそれに追従したい場合もありますよね。

2) 新しいinterfaceを作りつつ、元からあるものはincludeする

それならば

interface _Each_Nil_Isa
  include _Each
  def nil?: ...
  def is_a?: ...
end

のように元からあるインターフェースはincludeで合成して使用するのもアリだと思います。

3) Intersectionを使う

さらに別の方法がIntersectionを使用する方法です。

2)の方法は実は間違っていて、実際は型変数を使用します。

interface _Each_Nil_Isa[T]
  include _Each[T]
  def nil?: ...
  def is_a?: ...
end

しかしながらnil?is_a?は型変数を使用していません。_Eachにしか型変数は必要無い点がモヤりますよね。

そこで、新しいinterfaceを作るけど_Eachは除外してみます。

interface _Nil_Isa
  def nil?: ...
  def is_a?: ...
end

さらにこれを_Eachとくっつければいいわけです。

_Each[Integer] & _Nil_Isa

こんな感じで、型同士を足し算したいときに&を使えると覚えておくとよさそうです。

要はフィルター

2つの手法はどちらもif文の&&のように、単に条件を追加してフィルターしているとも考えられます。

Foo & _ToF

という型だったら、Foo型でフィルターしつつ、かつ_ToFでもフィルターしています。こう考えると、Intersection型とも友達になれるんじゃないでしょうか。

Intersection型の制約

なんでもかんでもIntersectionで繋げれるでしょうか?

例えばString & Integerとすると、「Stringのインスタンスであり、かつIntegerのインスタンスである」という意味になります。しかし、このインスタンスは存在し得ません。Rubyは1つのclassのみ継承できるので、Intersectionに書けるclassは1つまでに必然的になってしまいます。

moduleはいくらでもIntersectionで繋げられます。moduleはいくらでもincludeできるので、すべてのmoduleがincludeされていればいいわけです。

interfaceもいくらでもIntersectionで繋げられます。class/moduleはis_a?で調べてtrueが返ってくるインスタンスであればよいですが、interfaceは指定のメソッドを持っていればいいだけです。_ToF & _ToI & _ToSとつなげても全く不自然ではありません。

つまり

「Intersection型は、繋げれるのはclassは1まで、あとはいくらでもOK」

というルールが見えてきます。

moduleのself-typeもIntersection

moduleの定義にmoduleのselfが何かを設定できます。例えばEnumerable moduleは _Each をself-typeにしており、eachメソッドが使えることを保証しています。

self-typeの説明は最小限にして、このself-type、実は何個でも書けます。

module Foo : _ToI, _ToF, _ToA
end

この複数のself-typeは全ての条件を満たしていることを表しているので、Intersectionの関係にあります。 実際にsteepで存在しないメソッドを書いてみるとself-typeをのぞけるので見てみてください。

まとめ

Intersectionはともだち