injectの甘い罠

 Rubyの中でもinjectは個人的にキメると最高に気持ちの良いメソッドの一つなのですが、今回はinjectの罠にハマってしまったため、備忘録を兼ねて記録しておきます。

 まず、injectのチュートリアル的な使い方から。

[1,2,3,4,5].inject do |a, b|
  a + b
end
=> 15

 
 よく見る一般的な使い方です。1,2,3,4,5の値がbに代入され、aの値が次のループに引き継がれます。
 最後まで回るとaが返却されるため、この結果は15になります。
 
 また、injectは引数に初期値を設定することが出来、初期値を設定するとaに初期値が代入された状態でループが回ります。

[1,2,3,4,5].inject(10) do |a, b|
  a + b
end
=> 25

 aの初期値が10となるため、この結果は25となります。

 では、以下の場合はどうなるでしょうか。

[1,2,3,4,5].inject do |a, b|
  bb = b + 1
  a + bb
end
=>19

 15+5で20と思うかもしれませんが、injectの引数である初期値を省略した場合は、1番目の要素がaに、2番目の要素がbに代入されます。そのため、1はbの要素に数えられないため、結果は19となります。これは仕様通りの動作となります。

 http://www.ruby-lang.org/ja/man/html/Enumerable.html

 20にしたい場合は初期値を設定すればOKです。

[1,2,3,4,5].inject(0) do |a, b|
  bb = b + 1
  a + bb
end
=>20

 では、次は初期値を設定した上で、ブロックの中を空にするとどうなるでしょうか。

[1,2,3,4,5].inject(10) do |a, b|
end
=> nil

 この結果はnilになります。前のブロックの実行結果を次のブロックに引き継ぐため、初期値が入っていても、何も返さないので、以降はnilとなります。

[1,2,3,4,5].inject(10) do |a, b|
  puts "a=#{a},b=#{b}"
end

a= 10,b=1
a= ,b=2
a= ,b=3
a= ,b=4
a= ,b=5
=> nil

 ちなみに、if文で評価式を通過しないようにしても同様の現象が発生します。

[1,2,3,4,5].inject(10) do |a, b|
  a + b if b > 10
end
=> nil

 キメると最高に気持のいいinjectですが、よく分からない挙動も多いので注意して使いましょう、という話でした。

 ちなみに自分は文字列の連結や配列の展開をinjectを使って行うことが多いです。