Igor Šarčević wrote this on February 18, 2016

Inject is a fundamental building block

Inject is one of the fundamental, and most versatile constructs available in functional languages. It can be used to implement map, select, max, all? and a bunch of other iteration related methods. Unfortunately, many programmers are not aware of its awesome powers. This article is here to improve this fact.

Let’s dig in.

Simple array iteration

First, let’s start with an example problem — finding the sum of numbers in an array:

def sum(numbers)
  result = 0

  numbers.each do |number|
    sum += number
  end

  result
end

sum([1, 3, 5, 7, 9]) # => 25

Now, let’s try to solve the same problem with a simple limitation. Let’s calculate the result without changing the values of the variables. In other words, we will try to avoid explicit state changes like in the following example:

sum += number

This is a very important step to make our code more functional. Changing the values of variables, in other words making side effects, is preventing our code to run on multiple processors effortlessly. Side effects are also a common thing that can introduce hard to find bugs.

In the functional world, instead of a for-each iteration, programmers use recursion. Let’s rewrite the above code segment with a recursive implementation:

def sum(numbers)
  if numbers.empty?
    0
  else
    first, *rest = numbers

    first + sum(rest)
  end
end

sum([1, 3, 5, 7, 9]) # => 25

It can be a little hard to grasp the idea if this is the first time you are encountering recursion in your life. The idea behind it is, however, very simple. After several tries and errors it can be mastered easily.

Accumulators

In order to create a more general purpose algorithm, and to optimize it for tail calls we will allow our sum method to accept a starting value. We will call it accumulator.

def sum(accumulator, numbers)
  if numbers.empty?
    accumulator
  else
    first, *rest = numbers

    sum(accumulator + first, rest)
  end
end

sum(20, [1, 3, 5, 7, 9]) # => 45

The name ‘accumulator’ can be confusing, but you can think of it as a variable that accumulates the result. Its purpose is equivalent to the result variable in our original imperative implementation.

Introducing inject

Now it is safe to introduce the inject method — an abstraction for the above recursive construct. Let’s use it to sum numbers:

[1, 3, 5, 7, 9].inject(20) { |accumulator, number| accumulator + value } # => 45

By renaming the variables we can make the above line more straightforward:

[1, 3, 5, 7, 9].inject(20) { |result, number| result + value } # => 45

Let’s use it to calculate the product of an array:

[1, 3, 5, 7, 9].inject(2) { |result, number| result * number } # => 805

The above patters are very handy when we want to convert an array of values into one value. This is one of the main strengths of the inject method. In this example we are reducing the array into a single value. This is why the inject method is commonly also named reduce.

Less verbose injecting

If you think about it, the { |result, number| result + value } is repeated for both of the above examples. Luckily, Ruby is a powerful language that enables us to write the above lines even shorter. The :* is a shorthand value for a block that multiplies its arguments. Let’s use it:

[1, 3, 5, 7, 9].inject(1, :*) # => 945

This representation can give us a deeper insight into the name of the inject method. We can think of inject as a mechanism that injects a * operator between the elements of the array.

1 * 3 * 5 * 7 * 9

Implementing sum and product

Let’s implement the sum and product methods using inject:

def sum(elements, from = 0)
  elements.inject(from, :+)
end
def product(elements, from = 1)
  elements.inject(from, :*)
end

Implementing map

The above example is nice, but it is not nice enough to be called a fundamental iteration block for functional programmers. Luckily, inject can do much more. The map method can be tough about as a special kind of inject. The following two code block are equivalent:

[1, 2, 3].map { |el| el * el }
[1, 2, 3].inject([]) { |result, el| result + [el * el] }

We can even implement a map method by using inject:

def map(elements, &block)
  elements.inject([]) { |result, el| result + [block.call(el)] }
end

Implementing select

We can even implement a select method using inject:

def select(elements, &block)
  elements.inject([]) { |result, el| result + (block.call(el) ? [el] : []) }
end

We can reuse the previous definition of map and sum to make it shorter:

def select(elements, &block)
  sum(map(elements) { block.call(el) ? [el] : [] })
end

Even the reject method is simple:

def reject(elements, &block)
  elements - select(elements, &block)
end

Implementing min and max

We can implement a min method that returns the smallest element in the array. The trick is to store the current minimum as the accumulated value:

def min(elements, &block)
  elements.inject(Float::INFINITY) do |minimum, el|
    el < minimum ? el : minimum
  end
end

Symmetrically, the maximum value can be calculated:

def max(elements, &block)
  elements.inject(-Float::INFINITY) do |maximum, el|
    el > minimum ? el : maximum
  end
end

Implementing all?

This is my last example, and hopefully you will be convinced that almost every method in Ruby’s Enumerable module can be implemented as an special case of the inject function.

Let’s construct the all? method that checks if every element in the array satisfies a given check block:

def all?(elements, &block)
  elements.inject(true) { |result, el| result && block.call(el) }
end

Similarly, the any? method can be implemented:

def any?(elements, &block)
  elements.inject(false) { |result, el| result || block.call(el) }
end

Should I use inject to write code?

No.

Many programmers that learn the inject method start to use it all over the place. While I admit that inject is a truly powerful construct, it is not something that should be used everywhere, especially not in business level logic.

Instead of using inject everywhere, use it to construct new, domain level functions that you can use in you code. For example, instead of writing:

usernames = ["tim", "jake", "jennifer", "marcus"]

usernames.inject({}) do |result, username|
  result.merge(username => username.length)
end

# => { "tim" => 3, "jake" => 4, "jennifer" => 8, "marcus" => 6 }

I encourage you to write a specific method that creates a hash from the input and output array. Let’s call this method hashmap:

def hash_map(elements, &block)
  elements.inject({}) do |map, el|
    map.merge(el => block.call(el))
  end
end

Then use it to calculate the same value simpler:

usernames = ["tim", "jake", "jennifer", "marcus"]

hash_map(usernames) { |username| username.length }

# => { "tim" => 3, "jake" => 4, "jennifer" => 8, "marcus" => 6 }

Or even shorter:

hash_map(usernames, &:length)

If you feel ambitious, you can even add it as a method to Array:

def Array
  def hash_map(&block)
    self.inject({}) do |map, el|
      map.merge(el => block.call(el))
    end
  end
end

Final words

Inject is super awesome, and powerful enough to express most of the iteration logic in functional languages. However, never forget the Spiderman hypothesis:

With great power comes great responsibility.

Happy injecting!

comments powered by Disqus

About Igor Šarčević

Igor is a programming enthusiast with a passion for mathematics and intelligent systems. He gets lots of joy while solving difficult problems with a handful of dedicated people. He likes to be helpful to others and tries to improve himself on a daily basis. When he is not coding you can probably find him running, reading some Japanese poetry or improving his ninja skills.

Suggested Reads

Rails Testing Handbook

A new ebook on building test-driven Rails apps with RSpec and Cucumber.

At Rendered Text, we have a long history with Ruby on Rails. Checking the blog archive reminds me that we published first posts about working with Rails way back in 2009.

———

Rendered Text is a software company. For questions regarding Semaphore, please visit semaphoreci.com. Otherwise, feel free to get in touch any time by sending us an email.