This blog is part of our Ruby 2.4 series.
It is a common use case to calculate sum of the elements of an array or values from a hash.
1 2[1, 2, 3, 4] => 10 3 4{a: 1, b: 6, c: -3} => 4 5
Active Support already implements Enumerable#sum
1 2> [1, 2, 3, 4].sum 3 #=> 10 4 5> {a: 1, b: 6, c: -3}.sum{ |k, v| v**2 } 6 #=> 46 7 8> ['foo', 'bar'].sum # concatenation of strings 9 #=> "foobar" 10 11> [[1], ['abc'], [6, 'qwe']].sum # concatenation of arrays 12 #=> [1, "abc", 6, "qwe"] 13
Until Ruby 2.3, we had to use Active Support to use Enumerable#sum method or we could use #inject which is used by Active Support under the hood.
Ruby 2.4.0 implements Enumerable#sum as part of the language itself.
Let's take a look at how sum method fares on some of the enumerable objects in Ruby 2.4.
1 2> [1, 2, 3, 4].sum 3 #=> 10 4 5> {a: 1, b: 6, c: -3}.sum { |k, v| v**2 } 6 #=> 46 7 8> ['foo', 'bar'].sum 9 #=> TypeError: String can't be coerced into Integer 10 11> [[1], ['abc'], [6, 'qwe']].sum 12 #=> TypeError: Array can't be coerced into Integer 13
As we can see, the behavior of Enumerable#sum from Ruby 2.4 is same as that of Active Support in case of numbers but not the same in case of string or array concatenation. Let's see what is the difference and how we can make it work in Ruby 2.4 as well.
Understanding addition/concatenation identity
The Enumerable#sum method takes an optional argument which acts as an accumulator. Both Active Support and Ruby 2.4 accept this argument.
When identity argument is not passed, 0 is used as default accumulator in Ruby 2.4 whereas Active Support uses nil as default accumulator.
Hence in the cases of string and array concatenation, the error occurred in Ruby because the code attempts to add a string and array respectively to 0.
To overcome this, we need to pass proper addition/concatenation identity as an argument to the sum method.
The addition/concatenation identity of an object can be defined as the value with which calling + operation on an object returns the same object.
1 2> ['foo', 'bar'].sum('') 3 #=> "foobar" 4 5> [[1], ['abc'], [6, 'qwe']].sum([]) 6 #=> [1, "abc", 6, "qwe"] 7
What about Rails ?
As we have seen earlier, Ruby 2.4 implements Enumerable#sum favouring numeric operations whereas also supporting non-numeric callers with the identity element. This behavior is not entirely same as that of Active Support. But still Active Support can make use of the native sum method whenever possible. There is already a pull request open which uses Enumerable#sum from Ruby whenever possible. This will help gain some performance boost as the Ruby's method is implemented natively in C whereas that in Active Support is implemented in Ruby.