前回までのあらすじ
RBSがテストになるとおもしろいんじゃないか日記1 - スペクトラム
RBSがテストになるとおもしろいんじゃないか日記2 - スペクトラム
メソッド型
前回までで、1つの型について生成できる道筋をつけた。
今回の目標は(Integer) -> Integer
を検証したいので、単一の型だけではどうしようもできない。
RBSでは型は大きく3つに分類できる
- Type
- Method type
- Signature
それぞれ対応するパースメソッドが存在するのが分類基準だ。
RBS::Parser.parse_type
RBS::Parser.parse_method_type
RBS::Parser.parse_signature
Type
これは前回で考えた単一の型の話だ。例としてはObject
, Integer
, Set[String]
などはこれに入る。
MethodType
これが今回の主役で、(Integer) -> Integer
に相当するものだ。MethodTypeは3つの要素に分解できる
- 型変数
- 引数
- ブロック
- 返り値
型引数
これはいきなり難しいのだが、重要な要素だ。例えば、引数としてわかされたものをそのまま返すメソッドだとすると、
[T] (T) -> T
と書くことができる。このメソッドの引数をInteger
で呼べばInteger
が返るし、String
で呼べばString
が返るという挙動をこれで表せる。
今回の問題は、この型引数のランダムな値は何か?というものになる。
名前解決できる場合
例えばArray
のdef first: () -> Elem?
を見てみる。このElem
はArray
classの定義にあるclass Array[unchecked out Elem] < Object
のElemである。ようするにArray[Integer]
のInteger
だ。
このようにclass定義に存在する名前がメソッド定義に出てきた場合は解決可能に見える。Array[Integer]
の#first
では、Elem
をInteger
に置き換えて() -> Integer?
とすればいい。
よって名前解決できるなら単におきかえれば良い。
名前解決できない場合
では
[T] (T) -> T
のTが初めて出てくる名前であることも当然あるだろう。Steepでは、未解決のT
はあらゆるメソッドの呼び出しも許していない。触れてはいけないものなのだ。あらゆるメソッド呼び出しを塞いだオブジェクトは結構取り扱いが面倒である。いったんはBasicObject.new
とかでいいか……?
引数
引数もまあまあ難しい要素だが、今回はRBSに従っていればいいのでそこまで難しいことはしないはずだ。
例えば
(Integer, foo: String) -> void
なメソッドがあったとしたら、
foo(*[123], **{foo: 'aaa'})
というランダムな値を生成してメソッドに私てやるイメージだ。
Required positionals
ふつうの位置引数というやつで、(Integer)
のように定義も単純だ。
このランダム値を単純にRBS定義通りの個数用意すればいい。
Optional positionals
デフォルト値がついている、つけてもつけなくてもいいオプショナルな位置引数だ。
RBSでは(?Integer)
のようになる。
この場合のランダムな値はなんだろう。確率的に引数があったりなかったりすればいい気がする。
Rest positionals
0〜無限?個指定できる位置引数だ。これは0〜3個くらいをランダムで位置引数として用意すれば大抵のパターンを網羅できる気がする。
Trailing positionals
忘れがちなのがこれ。Rest positionalsの最後のいくつかを、この指定で取得できるという優れものだ。これはRequired positionalsと同じ扱いでよさそうだ。
Required keywords
必須キーワード引数だ。必須なのでRBSの定義通りランダム値を用意すればいい。
Optional keywords
オプションのキーワード引数だ。これもオプションということで、ランダムにあったりなかったりにしよう。
Rest keywords
なんでもキーワード引数だ。keyは何でもいいので、というか何でもいいことを確認したいので、適当な長さのSymbolを0〜3個ぐらい用意してkeyとし、valueはRBSの型指定から生成すればよさそう。
ブロック
RBSではブロックの型定義も書ける。() { () -> void } -> void
のように{ (ARG) -> RET }
で表現する。
今回はRBSからランダムな値を生成してメソッドを呼び出したい。
よって、このブロックは一旦Proc
オブジェクトにしてブロック引数として渡してあげる必要がある。
必須かどうかを指定できるが、必須でないならランダムにあったりなかったりにしよう。
ブロックの引数も生成する必要がある。Set
ではブロックを実行して中の要素を決定する。ブロックにも引数があるのかよ。。。
しかしご安心を。我々はこれまでに引数の生成方法について考えてきた。これをそのまま使えばよいだろう。
RBSの構造
ここでちょっとRBSの構造をおさらいしておこう。
RBSではブロックはRBS::Types::Block
classで表現されている。RBS::Types::Block
では、引数と返り値の型のセットであるRBS::Types::Function
や、必須かどうか、self
が何かという情報を持っている。
class RBS::Types::Block attr_reader type: RBS::Types::Function attr_reader required: bool attr_reader self_type: t? end
ではメソッドはというと、RBS::MethodType
で表現されていてRBS::MethodType
は、型引数、引数と返り値の型のセットであるRBS::Types::Function
、ブロック型を要素に持っている。
class RBS::MethodType attr_reader type_params: Array[AST::TypeParam] attr_reader type: RBS::Types::Function attr_reader block: RBS::Types::Block? end
ちなみにRBS::Types::Proc
もRBS::Types::Function
を持っている。Proc
型の生成にも使えそうだ。
class RBS::Types::Proc attr_reader :type attr_reader :block attr_reader :self_type end
つまり、RBS::Types::Function
という引数と返り値のセットを表す型は非常に重要であることがわかる。
というわけで、RBS::Types::Function
用にロジックを用意しておけば、メソッドでもブロックでも使えて便利というわけだ。
返り値
これまでで用意した引数を使ってメソッドを呼び出し、その返り値が返り値の型と一致していればRBS通りの挙動であると言えるし、逆なら言えない。
このチェックロジック自体はrbs本体にRBS::Test::TypeCheck
というclassがあるのでこれを利用すればよい。