RubyのEnumerable#injectやEnumerable#reduceで結果オブジェクトを書き換えてはいけない
言いたいことはタイトルが全てなのですが、少し解説します。
どういうコードの話なのか
コードレビューなどでRubyの Enumerable#inject
や Enumerable#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_object
や Enumerator#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#inject
や Enumerable#reduce
の引数に渡すのは初期値です。書き換わることを期待していません。
一方で Enumerable#each_with_object
や Enumerator#with_object
の引数に渡すのは結果オブジェクトです。書き換わることを期待しています。
こういうメソッドの本来の用途と違う使い方をすることがコードの読みにくさにつながります。
用途を知っている人は inject
/ reduce
で副作用が生まれるとは思いません。
inject
/ reduce
と each_with_object
/ with_object
は用途が違います。
ちゃんと使い分けましょう。
Discussion