自分向けのまとめです。
Intersection(&)
RBSにはIntersection型があります。Object & _Each[Integer]
のように&
で繋げている型のことです。
その前にUnion(|)とは
String | Symbol
のように、|
で繋いだ型をUnion型と言います。こちらは理解は簡単です。
「String もしくは Symbol のどちらか一方」という解釈になります。使用頻度は多めです。
Intersectionの使い方
それをふまえてObject & _Each[Integer]
は何を意味しているのでしょうか?
「Object
かつ_Each
インターフェースを持つってどういうこと?Object
にeach
メソッドはないよ???」
というのが、私が最初に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はともだち