ブロック引数と返り値は似ている

RaaPを開発していて、ブロック引数の扱いについて解像度が上がったのでメモとして書いておく。

これまでの考え方

Array#mapを例に見てみる。

[1, 2, 3].map do |i|
  i.to_s
end

これまではこう考えていた。 メソッドの最終的な返り値のためには、iはぶっちゃけなんでもよく、ブロックの返り値さえ想定された型なら良いだろうと。

実装イメージとしてはこうしていた。

array = Array.new(3) { random_integer }
return_value = random_string
block = Proc.new
  return_value
end

array.times(&block)

しかしながら、これでは動作検証とは言えないケースが出てきた。

例えばSet#divideはブロック引数の数で挙動が変わるという面白いメソッドで、実装も明確に

if func.arity == 2

とブロック引数の数で挙動が分岐している。

set/lib/set.rb at 4e95d55294f0e577529ca8702c4d2c3ae90247e7 · ruby/set · GitHub

この挙動もサポートしたい。

Procを動的に作る

func.arity == 2を達成するにはブロック用のProcの引数部分を動的に生成しなければならない。

Rubyでは現状Procを動的に組み立てるメソッドはない。evalするしかない。

というわけで、型からブロック用のProcをevalで生成するコードを書いた。もっといい方法があったら知りたい……。

与えるか、与えられるか

さらにInteger#timesの例を考えてみる。

10.times do |i|
  # ...
end

このiは型から生成しているわけではなく、メソッドの挙動として勝手に生成されている。

これまでの考え方としては、返り値以外は全て自動的に値を生成するもので、メソッドの返り値だけが唯一の型チェック対象だとしていた。

ランダムな食材をメソッドクックに与えると、できあがった料理となって出てくる。こう考えていた。

しかしながら、ブロック引数も実はメソッドが生成している。返り値のように。ならば、その値が本当に型通りなのか確かめたい。なぜならRaaPはテスティングツールだから。

ブロック引数は、実は返り値に似ていて、型チェック対象なのだ。

まとめると、

  • メソッド引数 -> 型から値を生成する
  • メソッド返り値 -> 値を型チェックする
  • ブロック引数 -> 値を型チェックする
  • ブロック返り値 -> 型から値を生成する

メソッドとブロックは反対の関係にあるように見えておもしろい。

実装する

Integer#timesの型が() { (String) -> void } -> selfだったら、間違っていると教えてあげるべきなのだ。

わかってしまえば、なんてことのない当たり前のことだ。

というわけで、ブロック引数の型チェック機能も実装した。

試しにわざとInteger#timesのブロック引数をStringにしてみると、TypeErrorが発生してテストは失敗するようになった。

[RaaP] INFO: # Integer
[RaaP] INFO: ## def times: () { (::String) -> void } -> self
....F
[RaaP] INFO: Failure: [TypeError] block argument type mismatch: expected `(::String)`, got [0]

(::String)をブロック引数に期待していたが、実際は[0]が来たのでおかしいよとRaaPが教えてくれている。

さらに、わざとブロック引数を空にしてみる。

[RaaP] INFO: # Integer
[RaaP] INFO: ## def times: () { () -> void } -> self
......E....EE....EE.E.EE.EE..E...E..E.E.E.....E.EE...E...E....E..EEE..EE....E.E.E.EEE.E.EEE..EE..E.E.
() { () -> void } -> self

[RaaP] INFO: success: 61, skip: 0, exception: 40, time: 17ms

Integer#timesはレシーバーがマイナスのときはブロックを全く実行しないので、マイナスの場合は成功する。 プラスの時はdebug出力するとわかるがArgumentErrorを出しているので、exceptionとして計上されている。

あながち間違いとも言いにくいのだが、これは失敗でもいい気がするなあ。。。

ともかく検知はできたのでいい感じ。