15
3

More than 1 year has passed since last update.

Rubyの総和を求めるinject(:+)は罠があるからsumにして空のケースもテストしよう

Last updated at Posted at 2022-11-13

最初に

コミット権限をもらったRubyライブラリが古く、
sumのない時代に書かれたライブラリでinject(:+)で総和が求められていました。1

a = [1, 2, 3]
p a.inject(:+) # => 6

このように要素が数値の配列であれば、総和を求めることができます。2

問題点

しかし、空の配列でinject(:+)を使うと、0ではなくnilを返してきます。

a = []
p a.inject(:+) # => nil

injectは総和を求めるのに限られたメソッドではないので、「要素に何もないから数値の総和を求めたいのだろう」と勝手に決めつけたりせず、:+を使うことなく淡々とnilを返してきます。

この挙動を知らずに書かれているコードであったため、そのライブラリはバグっていました。

解決策

空の配列に対する総和を0にしたければ、単純にsumに置き換えると良いでしょう。

a = []
p a.sum # => 0

なお、小数計算ではinjectよりもsumの方が誤差が少ないらしいです。
今のバージョンで、数値の総和を計算をするならsum一択でしょう。

備考: もしも古いバージョンに対応するなら

メンテナンス期間の終わっている古いv2.4未満の話になりますが、
もしsumの使えないバージョンに対応する必要があるなら、
「引数を2個にして、最初の引数を0を指定」する方法があります。

a = []
p a.inject(0, :+) # => 0

簡単に言えば、最初の引数から演算します。
最初の引数は空配列のときに返される値になるだけでなく、
要素があれば第1引数も他の数と同じように最初に足されます。

詳しくは、Rubyリファレンスマニュアルを見たり、別途調べたください。

備考: 総和以外のケースでも

inject(:+)という総和のケースではsumに置き換えられますが、
その他の演算に関しては特別なメソッドは用意されていないです。

その他の場合でも、引数は2個とる必要がないか検討した方が良いでしょう。

例えば、要素の全ての積をとる総乗のケースです。

p [2, 4].inject(1, :*) # => 8  (= 1 x 2 x 4)
p [].inject(1, :*) # => 1  (= 1)

総乗のケースでは、最初の引数を1にします。
もし、最初の引数を0にすると0になにかけても0なので、
返り値が必ず0になってしまいます……。

p [2, 3].inject(0, :*) # => 0 (= 0 * 2 * 3)

最後に

本当に言いたい大事なこと

総和を求めるだけに限らない話ですが、要素が0のような何もないケースは一般的でない処理で、意識してないと試し忘れたり、テストを書かなかったりするかもしれません。

しかし、0のようなケースこそ、エッジケースなのでバグりやすいです。

要素数が0個、1個、複数個のパターンで、テストは書くなり試した方が良いでしょう。

  1. reduceinjectはエイリアスなので、reduceでも全く一緒です。

  2. 要素が整数の場合、inject(:+)の内部ではInteger#+が使われます。

15
3
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
15
3