RBSがテストになるとおもしろいんじゃないか日記3

前回までのあらすじ

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が返るという挙動をこれで表せる。

今回の問題は、この型引数のランダムな値は何か?というものになる。

名前解決できる場合

例えばArraydef first: () -> Elem?を見てみる。このElemArrayclassの定義にあるclass Array[unchecked out Elem] < ObjectのElemである。ようするにArray[Integer]Integerだ。

このようにclass定義に存在する名前がメソッド定義に出てきた場合は解決可能に見える。Array[Integer]#firstでは、ElemIntegerに置き換えて() -> 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とし、valueRBSの型指定から生成すればよさそう。

ブロック

RBSではブロックの型定義も書ける。() { () -> void } -> voidのように{ (ARG) -> RET }で表現する。

今回はRBSからランダムな値を生成してメソッドを呼び出したい。

よって、このブロックは一旦Procオブジェクトにしてブロック引数として渡してあげる必要がある。

必須かどうかを指定できるが、必須でないならランダムにあったりなかったりにしよう。

ブロックの引数も生成する必要がある。Setではブロックを実行して中の要素を決定する。ブロックにも引数があるのかよ。。。

しかしご安心を。我々はこれまでに引数の生成方法について考えてきた。これをそのまま使えばよいだろう。

RBSの構造

ここでちょっとRBSの構造をおさらいしておこう。

RBSではブロックはRBS::Types::Blockclassで表現されている。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::ProcRBS::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があるのでこれを利用すればよい。