RubyのEnumerable#injectやEnumerable#reduceで結果オブジェクトを書き換えてはいけない

2 min read読了の目安(約1900字

言いたいことはタイトルが全てなのですが、少し解説します。

どういうコードの話なのか

コードレビューなどでRubyの Enumerable#injectEnumerable#reduce で結果オブジェクトを書き換えているコードをたまに目にします。

values = [10, 20]
ary = [1, 2, 3, 4]
ret =
  ary.inject(values) do |result, value|
    result << value * 2
    result
  end
p ret # => [10, 20, 2, 4, 6, 8]

こういうコードです。

一見普通のコードに見えますよね。
でもこのコードには問題があります。

何が問題か?

ret は期待した値になるし問題ないのではと思いますか?
では、以下を実行したらどうなるでしょうか。

p values # => [10, 20, 2, 4, 6, 8]

そうです、初期値として渡した values までもが書き換わってしまっています。
これは果たして期待した動作でしょうか?

inject / reduce の引数に渡すのは初期値です。書き換えるためのものではありません。
inject / reduce は引数で受け取った値を結果オブジェクトの初期値として使用し、ブロックの戻り値を次の結果オブジェクトにします。
初期値として渡しただけなのにそのオブジェクトが書き換わるのはビックリです。おかしいですよね。

どう書くのが良いのか?

inject / reduce を使って書くならこんな感じになります。

values = [10, 20]
ary = [1, 2, 3, 4]
ret =
  ary.inject(values) do |result, value|
    result + [value * 2]
  end
p ret # => [10, 20, 2, 4, 6, 8]
p values #=> [10, 20]

これならば values は書き換わらず期待した通りの動作になります。
ブロックが毎回新しい Array を作って返すので初期値のオブジェクトを破壊しません。

もっとちゃんと書く

前述のコードだと何度も Array を作り直していていまいちだと思いませんか?
そもそもこういうのシーンでは Enumerable#each_with_objectEnumerator#with_object を使います。

values = [10, 20]
ary = [1, 2, 3, 4]
ret =
  ary.each_with_object(values) do |value, result|
    result << value * 2
  end
p ret # => [10, 20, 2, 4, 6, 8]
p values # => [10, 20, 2, 4, 6, 8]

each_with_object / with_object の引数に渡すのは結果オブジェクトです。書き換えるためのものです。
each_with_object / with_object は最終的に引数で渡した結果オブジェクトを返します。

結局同じ結果になるし良くない?

全然良くないです。

Enumerable#injectEnumerable#reduce の引数に渡すのは初期値です。書き換わることを期待していません。
一方で Enumerable#each_with_objectEnumerator#with_object の引数に渡すのは結果オブジェクトです。書き換わることを期待しています。

こういうメソッドの本来の用途と違う使い方をすることがコードの読みにくさにつながります。
用途を知っている人は inject / reduce で副作用が生まれるとは思いません。

inject/ reduceeach_with_object / with_object は用途が違います。
ちゃんと使い分けましょう。