In Progress
Unit 1, Lesson 1
In Progress

Sum

Video transcript & code

Chances are, sooner or later you'll find yourself wanting to sum a list of numbers in Ruby. If you're using Rails, you'll simply use the #sum method added by ActiveSupport.

require "active_support/core_ext/enumerable"
data = [1,2,3,4,5,6,7,8,9,10]
data.sum                        # => 55

As you can see from this code, if we just want to pull in the Enumerable extensions such as #sum from ActiveRecord, we can require active_support/core_ext/enumerable.

But what about using pure Ruby? If your introduction to Ruby was through Rails, you might have been surprised the first time you tried to use #sum outside of a Rails project, and found that it wasn't there.

data = [1,2,3,4,5,6,7,8,9,10]
data.sum                        # => 
# ~> -:2:in `<main>': undefined method `sum' for [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]:Array (NoMethodError)

Summing a list of numbers is fundamentally a reduction operation: we want to reduce the list into a single number which is derived from all of them put together. Ruby Enumerable objects give us a method for this class of operations, unsurprisingly called #reduce. You may also know it as #inject, which is its older, Smalltalk-inspired alias.

data.inject

Fundamentally, #reduce iterates over the elements of the collection, combining one number with the next one, then combining that combination of two numbers with the next one, and so on. It takes a block which tells it how to combine one number with the next number in the list. The first argument the block receives is called the accumulator. It contains the cumulative result of all the previous combinations. The second block argument is the next number to be combined in.

Let's make this more concrete by using #reduce to sum up our list of numbers. Our block is simple: it takes the accumulator, and adds to it the next number. That's it. However, we need one more thing. #reduce combines each element with the ones before it, but when it first starts it needs something to combine with the very first element. We provide this "seed" value as an argument. In this case, we'll use 0.

When we run this code, we can see it gives us the sum of our list.

data = [1,2,3,4,5,6,7,8,9,10]
sum = data.reduce(0) {|acc, n|
  acc + n
}
sum                             # => 55

Let's take a closer look at how this works by printing the accumulator and next number at each step of the operation.

data = [1,2,3,4,5,6,7,8,9,10]
sum = data.reduce(0) {|acc, n|
  p [acc, n]
  acc + n
}
sum                             # => 55
# >> [0, 1]
# >> [1, 2]
# >> [3, 3]
# >> [6, 4]
# >> [10, 5]
# >> [15, 6]
# >> [21, 7]
# >> [28, 8]
# >> [36, 9]
# >> [45, 10]

On the first iteration, the seed value of 0 is combined with 1, yielding 1. On the second go-round, 1 is added to the next value of 2, yielding 3. Next time, that 3 is combined with 4. And so on, until a cumulative 45 is combined with the last value of 10, for a total of 55.

Now that we understand how #reduce works, let's see if we can golf this code down a bit. In Ruby, the + operator is simply a method defined on numeric objects, like any other method. We can see this if we send it using method call notation. We can also explicitly send it using the #public_send method and the symbol #+.

2 + 2                           # => 4
2.+(2)                          # => 4
2.public_send(:+, 2)            # => 4

Speaking of symbols, when a symbol is sent the #to_proc message, it returns an executable Proc object which will send that symbol as a message to the first argument of the Proc. That probably made no sense, so let's just do it. We assign the result of the #to_proc send to a variable named add. Then we invoke this proc by sending it the #call message, with two numbers as arguments. The result is the sum of the two numbers.

add = :+.to_proc               # => #<Proc:0x000000019697d0>
add.call(2, 2)                 # => 4

The proc we call add here is more or less equivalent to a proc that takes two arguments, and sends the #+ message to the first argument, passing the second as an argument.

add = :+.to_proc               # => #<Proc:0x00000001be0860>
add.call(2, 2)                 # => 4
myadd = ->(a,b) { a.public_send(:+, b) }
myadd.call(2,2)                # => 4

Knowing this, we can remove the block from our reduction. In its place, we add a second argument. We'll start this argument with an ampersand, signaling to Ruby that it should convert the argument to a Proc and use it in place of a block passed to the method. Then we pass the :+ symbol. Ruby will implicitly send this symbol the #to_proc message, and use the resulting Proc as the block. As we've just seen, the proc form of the :+ symbol does exactly what we want: add the two numbers together.

data = [1,2,3,4,5,6,7,8,9,10]
sum = data.reduce(0, &:+)
sum                             # => 55

But we don't stop here. As it happens, #reduce has special handling for symbols passed as arguments. We can remove the ampersand, making the symbol into an ordinary argument. The #reduce method, seeing that the argument is a symbol, will assume that we want to use it as if it were an ampersand block argument, and this code will continue to work as before.

data = [1,2,3,4,5,6,7,8,9,10]
sum = data.reduce(0, :+)
sum                             # => 55

One more thing. Remember earlier when I said that we needed to supply a "seed" argument to get the accumulation started? I lied. If we leave the argument off, #reduce uses the first element as the seed, combines that element with the second, and so on.

data = [1,2,3,4,5,6,7,8,9,10]
sum = data.reduce(:+)
sum                             # => 55

We have now arrived, via a roundabout journey, at the idiomatic way to sum up a list of numbers in Ruby. We can trivially modify this code to perform other operations on our list—for instance, to XOR a list of numbers together, we could simply change the + to the numeric binary XOR operator:

data = [1,2,3,4,5,6,7,8,9,10]
checksum = data.reduce(:^)      # => 11

Alright, that's enough for today. Happy hacking!

Responses