Ruby - reduce / inject function

Reduce function

The reduce function reduce an array into a single value. It iterates through the array while having a accumulator variable (memo). This variable contain the return value of the block given as parameter for each elem of the array. The block contain as param the element itself and the accumulator value.

[1] pry(main)> a = [1, 2, 3, 4, 5]
=> [1, 2, 3, 4, 5]
[2] pry(main)> a.reduce {|elem, accumulator| accumulator += elem}
=> 15

Here we simply add all the element of the array into the accumulator variable. So on the first loop we have :

  • accumulator = 1 (It takes the first value of the array if we don’t specify one)
  • elem = 2
  • 2 + 1

Then for the second round of loop :

  • accumulator = 3
  • elem = 3
  • 3 + 3 = 6

And so on.

The true idea with reduce is that the accumulator variable (memo) is reassigned each time in the loop by the operation we specify in the block.

a = [2, 3, 4]
=> [2, 3, 4]
[4] pry(main)> a.reduce {|elem, accumulator| accumulator * elem}
=> 24
  • accumulator = 2
  • elem = 3
  • 2 * 3 = 6
  • accumulator = 6

Then

  • accumulator = 6
  • elem = 4
  • 6 * 4 = 24
  • accumulator = 24

Then it return the accumulator (memo) variable.

One interesting thing is that the reduce function can take as an argument a default value for the accumulator variable. If not specified then, the function will take the first value of the array as the accumulator variable and directly begin with the second value of the array to execute the block.

We can pass a default accumulator variable, here 0, like this :

a = [2,3,4]
a.reduce(0) {|elem, sum| sum * elem} # 24

The reduce function with symbol

The reduce function can also take a symbol. It will iterates over the array and apply an operation.

[5] pry(main)> a = [2, 3, 4]
=> [2, 3, 4]
[6] pry(main)> a.reduce(:+)
=> 9

It’s exactly the same as writing a.reduce {|elem, sum| sum += elem}. But I guess it’s more readable to use a symbol.

Recoding reduce 🧙‍

If you read my previous article about map, there’s not really any magic here : we call a proc on each elem and pass as argument the elem itself and the accumulator variable.

A basic implementation of reduce look like this :

def reduce(array, &block)
  accumulator = 0
  array.each do |elem|
    accumulator = block.call(accumulator, elem)
  end
  accumulator
end

Again the only thing that change from the each method is that we pass a second variable to the proc (accumulator) which is then reassigned as the return value of the proc.

Let’s now look at a more robust version of reduce, which can handle symbol using the open class technique on the Array class.

class Array

  def reduce(accumulator = nil, operation = nil, &block)
    if operation && block
      raise ArgumentError
    end

    if operation.nil? && block.nil?
      operation = accumulator
      accumulator = nil
    end

    block = begin
      case operation
      when Symbol
        lambda { |acc, value| acc.send(operation, value) }
      when nil
        block
      end
    end

    if accumulator.nil?
      accumulator = first
      skip_first = true
    end

    index = 0

    self.each do |elem|
      unless skip_first && index == 0
        accumulator = block.call(accumulator, elem)
      end
      index += 1
    end
    accumulator
  end
end

The operation variable contain a symbol, it is optionnal. The accumulator is the memo value which stock the return value of the proc at each round of the loop. This is this value that the function will return.

Two important things : the behavior of the function make a case on the operation variable to either execute a the given block or the symbol.

acc.send(operation, value) is where the magic happen with the symbol as parameter. This snippets will be called on each iteration of the loop to increment the accumulator value.

For example with [1, 2, 3, 4, 5].reduce(:+) This is what going on on the first round of loop :

  • accumulator = 1

  • operation = :+
  • value = 2
  • 1.send(:+, 2) will return 3

Then on the second round of loop :

  • accumulator = 3
  • operation = :+
  • value = 3
  • 3.send(:+, 3) will return 6

And so on.

The second important thing is the skip_first and index variable. Basically, it sum up the fact that if you don’t specify any default accumulator value, then accumulator will take the first value of the array (accumulator = first) and we begin the loop with the second value of the array. We skip the first round of the loop.

We reach the end of this articles about the reduce function.

As a conclusion, the reduce function and the inject are aliases according to the ruby doc :

The inject and reduce methods are aliases. There is no performance benefit to either.

Sources :

Search