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

前回までのあらすじ

RBSがテストになるとおもしろいんじゃないか日記1 - スペクトラム

RBSがテストになるとおもしろいんじゃないか日記2 - スペクトラム

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

RBSがテストになるとおもしろいんじゃないか日記4 - スペクトラム

続シンボリックコール

シンボリックコールは、前回紹介したようにメソッドの呼び出しを遅延評価するデータ構造である。

ザックリとしか紹介できていなかったので、ここで深掘りしておく。

シンボリックコールは次のようなシンプルなデータ構造だ。

[:call, Integer, :sqrt, [4], {}, nil]

この構造は次のRubyコードをシンボリックコールで表現したものだ。

Integer.sqrt(4)

最初に:callがあることで、ありえそうなArrayデータと区別しやすくしている。

次のIntegerはレシーバーだ。

次の:sqrtはレシーバーから呼び出すメソッド名。

次の[4]は位置引数。

次の{}はキーワード引数。

次のnilはブロック引数を表した実際の値となっている。

Rubyにおけるオブジェクト指向的にはclassにしたほうがいいのだろうが、シンボリックコールの発祥であろうQuickCheckをリスペクトした形だ。また、単純なタプルだとinspectで単純に表示できるし、パターンマッチでマッチするように書いても自然だ。

余談だが、raapを作るにあたってerlangとelixirとhaskellを少し学んだので、raapではこれらをリスペクトしてパターンマッチやメソッドの再起呼び出しを多用している。

ネスト

このシンボリックコールはネストできる。例えば前の例を拡張してみよう。

i = "4".to_i
Integer.sqrt(i)

上のコードをシンボリックコードで表現するとこうなる。

[:call,
  Integer, :sqrt, [
    [:call, "4", :to_i, [], {}, nil]
  ],
{}, nil]

位置引数部分でネストしていることがわかる。

さらに複雑な例をraapのテストコードから引用する。

test_a = Test::A.new()
test_b = Test::B.new()
test_c = Test::C.new(a: test_a, b: test_b)
test_c.run()

を表現すると次のようになる。

[:call,
  [:call, Test::C, :new, [], {
    a: [:call, Test::A, :new, [], {}, nil],
    b: [:call, Test::B, :new, [], {}, nil],
  }, nil],
:run, [], {}, nil]

このように、メソッド呼び出しの結果を利用しなければならないという制限はあるものの、raapでイメージしている型からの値生成では十分に柔軟なものだと思われる。

なににつかうの

さて、このシンボリックコールが何の役に立つかというと、"ランダムな値"の生成時にこの値を生成してしまうのではなく、シンボリックコールで生成するのだ。

単純な例えだと、あるFloatの値が欲しいとしたら、

Random.rand

とするのではなく、

[:call, Random, :rand, [], {}, nil]

としておくのだ。

"ランダムな値"を取得すると、このシンボリックコールが手に入り、このシンボリックコールを実際に実行することで"ランダムな値"を取得する。

なんでこんな回りくどいことを?

シンボリックコールはinspectするだけでも、なんとなく何がしたいのかわかるが、そこまでフレンドリーでもない。 そこで、このシンボリックコールからRubyのコードに戻すことを考えた。これはQuickCheckでもproperでもやっていない。

つまり、これを

[:call, Random, :rand, [], {}, nil]

こうする。

Random.rand

これを、

[:call,
  [:call, Test::C, :new, [], {
    a: [:call, Test::A, :new, [], {}, nil],
    b: [:call, Test::B, :new, [], {}, nil],
  }, nil],
:run, [], {}, nil]

こうするのだ。

test_a = Test::A.new()
test_b = Test::B.new()
test_c = Test::C.new(a: test_a, b: test_b)
test_c.run()

こうすることによって、もしテストが失敗した場合に、かなり実際に近い"再現コード"を手に入れることができる。

"ランダムな値"を手に入れるために、さまざまな選択肢が存在する。

例えば Integer? なら Integernil かをランダムに選択している。

Integer | Stringだったら、 IntegerString かでランダムに選択している。

選択肢があると、引数に使われた42はどうやって作られた42なのかわからなくなる。

再現コードがあれば、問題の再現はもちろん、raapがどういう選択をして"ランダムな値"を選択したのかもわかる。

これはさすがに便利なんじゃない?

どうやってRubyコードにするの?

"実装を見てくれ"の一言だが、軽く説明する。

まずこのシンボリックコールを再起的に渡り歩いていく。

トップレベルがシンボリックコールでないならその値が最終的な値。

シンボリックコールなら、レシーバー、位置引数、キーワード引数の順に見ていき、これがシンボリックコールなら、またレシーバー、位置引数、キーワード引数と見ていく。

再起的に見ていくと、もうこれ以上深ぼれないシンボリックコールにたどり着くので、これから文字列化していく。 文字列化したら最終結果にためておき、"変数名"を戻す。

変数

この変数は現状かなりザックリつくっており、module/class名をdowncaseしただけというものになっている。名前が被ってしまうことがあるが、あんまりいい解決策は見つかっていない(なんとかしたい)。

変数が返ってきたらこの変数をRubyコードに文字列として組み込んでやる。再起が戻っていけば、自然と上からRubyコードを実行したかのように文字列が組み上がるという寸法だ。

つなみに"ランダムな値"は、変数を返すのではなく、シンボリックコードを実行した結果を返す。

[:call, Random, :rand, [], {}, nil]

だったら、

Random.rand

にして、

0.2585089800916408

を得るのだ。これを再起的に戻していって、レシーバーなり引数に利用して、コードを実行していく。

つまり?

シンボリックコールを使えば使うほど、再現コードが手に入る確率が上がり、便利になっていくというわけだ。

まとめ

シンボリックコールはいいぞ。

あんまり読み返してなくて眠いので読みづらいと思うが、書いて出しが長続きするのでここで一旦publishする。