RubiniusのArray#productをブロック付きで呼んだら…


自作Gemのテストをしている過程で、Rubiniusのバグを掘り出してしまうハメになりました。

Rubiniusって?

Rubiniusは、Rubyの実装の1種です。特徴としては「スピード志向」ということと、「大半の部分がRubyで書かれている」ということがあります1。標準実装のMRI(CRuby)、Java上での実装のJRubyと並んで、そこそこメジャーなRuby処理系の1つです。

Array#productとは

Array#productは、複数の配列について要素同士の組み合わせを全生成するメソッドです。ブロックを渡した場合、組み合わせの1つ1つについてブロックを実行する、というようになっています。

気づいたきっかけ

自作のGemが完成に近づいてきたため、もともと開発に使っていたRuby 2.2以外に、古いバージョンやJRuby、Rubiniusでもテストを回していました。そして、このGemは内部にいくつかのクラスを含んでいて、そしてそれら同士の演算についても定義していたので、組み合わせでテストしようとArray#productを使って組合わせを展開するようなテストコードを書いていました。

some_class_spec.rb
describe SomeClass do
  terms1 = [a, b, c, ...]
  terms2 = [d, e, f, ...]
  terms3 = [g, h, i, ...]

  terms1.product(terms2) do |term1, term2|
    context "#{term1} and #{term2}" do
      # 略
    end
  end

  terms1.product(terms3) do |term1, term3|
    context "#{term1} and #{term3}" do
      # 略
    end
  end
end

MRIでは問題なくテストを通過したので、Rubiniusで動かしてみたのですが、一気に赤字でエラーを連発しだしてしまいました。調べてみると、そもそもcontextの時点で余計なものが現れていることに気づきました。RSpecとRubiniusの組み合わせで起こる問題かと思ってさらに追いかけてみたのですが、試しにterms1freezeしてみたところ、MRIは何事も起こらなかったのに対して、RubiniusはArray#product内部でfrozenな配列を書き換えようとしてRuntimeErrorとなってしまいました。

最低限の再現コード

issueにも載せたコード
arr = [1,2,3].freeze
arr.product([4,5,6]){ 1 }
# MRIでは無問題、RubiniusではRuntimeError

原因の追求

スタックトレースにRubinius内のRubyのコードも出ていたので、追いかけるのはそこまで困難ではありませんでした。で、実際のコードを見てみると、なぜか返り値としてselfに書き込みを始めるし、ブロックを取るときにもいったん配列を生成するようになっている(動作はするのですが)など、これでいいのか感満載の実装となっていました。

そのままの勢いでIssueを立てて、とりあえず様子見中です。

まとめ

オープンソースプロダクトを使っていろいろ構築していくと、時折バグに出くわすことがあります。プルリクエストを作るにはハードルが高くても、Issueは事象をまとめればいいので、ぐっと立てやすいものです。せっかく万人に開かれているオープンソースの世界ですから、飛び込んでみたほうが、きっと新たな局面が開けます。


  1. このため、Rubyの標準メソッドについて「Ruby内で書くにはどうすればいいか」という事を調べるにも便利です。