0

I used to think that Ruby's

arr.inject(:+)

or JavaScript's

arr.reduce((a, b) => { return a + b })

// a cooler way
arr.reduce( (a, b) => a + b )  

are both the equivalent of summing the array entries up. But is it actually not correct because of this one case: what if the array is empty?

Ruby will return nil, and JavaScript will raise an exception. So besides using an initial value, are there also better ways to do it?

nonopolarity
  • 146,324
  • 131
  • 460
  • 740

3 Answers3

3

Ruby's inject can accept two parameters, the first one being the initial value. If there's no element in the Enumerable, this initial value becomes the returned value:

[].inject(:+)
# nil
[].inject(0,:+)
# 0

As mentioned by @Ursus in the comments, Ruby 2.4 defines Enumerable#sum with a default value of 0:

[].sum
# 0

As a bonus, it's also much faster for some objects (e.g. Ranges).

Note that returning nil could sometimes be the desired result, depending on which process comes after.

Finally, kudos to JS for raising an exception when the array is empty and no default value has been defined. It's probably the cleanest solution.

Community
  • 1
  • 1
Eric Duminil
  • 52,989
  • 9
  • 71
  • 124
  • 2
    And from ruby 2.4.0 you can use sum method, [].sum # 0 – Ursus Apr 24 '17 at 11:51
  • I never thought about the fact that the operation becomes the second argument if you want to add a starting value. This is kind of bad. :/ Why it is allowed to pass the accumulator in a non-block form is beyond me. – ndnenkov Apr 24 '17 at 11:56
  • @ndn: Indeed, it feels wrong to move the argument like this. The corresponding C code isn't pretty : `switch (rb_scan_args(argc, argv, "02", &init, &op)) {...` – Eric Duminil Apr 24 '17 at 12:02
  • @EricDuminil so you mean `arr.reduce( (a, b) => a + b, 0 )` is the cleanest solution? A bit surprised that summing array entries up appears complicated – nonopolarity Apr 24 '17 at 14:41
  • @太極者無極而生 If you want to accept empty arrays in JS, yes, I'd say that `arr.reduce( (a, b) => a + b, 0 )` is the way to go. If you want to detect empty arrays and fail as soon as possible when one is detected, you could leave `arr.reduce( (a, b) => a + b ) `. – Eric Duminil Apr 24 '17 at 14:46
0

To deal with the exception raised in case of empty array, declare an initial value.

var arr = [],
    res = arr.reduce((a,b) => { return a + b }, 0);
                                              //^^ initial value
    console.log(res);
kind user
  • 40,029
  • 7
  • 67
  • 77
0

Just because you use a dynamically-typed language with implicit types, doesn't mean that you don't have to think about types.

In other languages, these two operations have different names (e.g. in Haskell foldl and foldl1, in Scala foldLeft and reduceLeft), whereas in Ruby, they are overloaded, but the matter still remains that they are two distinct operations:

  1. the general fold operation

    • operates on a collection of As
    • has a zero element of type B
    • and a combining function which takes a B and an A and returns a B

    which means

    • it can return an object of a type other than the element type (it returns a B, not an A)
    • it can operate on an empty collection
  2. the constrained reduce operation

    • operates on a collection of As
    • has no zero element, it uses an element of the collection instead, which has type A
    • and a combining function which takes an A and an A and returns an A

    which means

    • it can only return an object of the same type as the element type (it returns an A)
    • it can only operate on a non-empty collection

That's also where the Haskell names come from, it is called foldl1 because it requires at least 1 element, and takes element number 1 as the initial element.

Note that the general fold operation is strictly more powerful than the constrained reduce operation. In fact, the general fold operation is a universal iterator operation, it can do anything that a foreach loop can do: map, groupBy, sort, uniq, reverse, … you name it. Every collection operation can be implemented as a fold.

Example:

module Enumerable
  def my_reverse
    inject([]) {|acc, el| [el] + acc }
  end

  # this is not exactly equivalent because it's not lazy
  def my_any?
    inject(false) {|acc, el| acc || yield el }
  end
end

reduce can not do everything. For example, you cannot implement reverse using reduce because reduce returns an A, but reverse returns a Collection<A>; likewise, you cannot implement any? because it returns a Boolean. You can only use reduce if these two properties hold:

  1. you want to produce a value of the same type as the element type
  2. there is at least one element

In your case, #1 is true, but #2 isn't, which is why you cannot use reduce, you must use fold. I.e. in Ruby, you need to use

ary.inject(0, :+) 

and in ECMAScript

arr.reduce((acc, el) => acc + el, 0)

As mentioned elsewhere, Ruby does have Enumerable#sum; however, that only works for this specific case of summing the elements, but for example not for the practically identical problem of multiplying them.

Jörg W Mittag
  • 363,080
  • 75
  • 446
  • 653
  • Extremely interesting, as always. If I understand your `fold` description correctly, it's covered by Ruby's `each_with_object`, right? – Eric Duminil Apr 24 '17 at 13:36
  • `fold` is `inject` with initial argument. `reduce` is `inject` without initial argument. `each_with_object` is different, it doesn't use the return value of the combiner function as the accumulator value for the next iteration. It only use one single accumulator value for the entire traversal. As such, it is inherently side-effecting and impure. It is more like `fold`, however, in that there *is* an initial value of a potentially different type. – Jörg W Mittag Apr 24 '17 at 19:25