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!