Jedi or Sith Lord: Ruby's inject method

/ enumerables / ruby

"Remember, a Jedi's strength flows from the Force."

The ruby inject method is a powerful ally in a jedi (ruby) master's arsenal. It is wielded easily to those who train with it and mastered by a jedi with both knowledge of the light and dark side of the force. The light side of inject involves the different usages for the method as well as the basic format of it. The dark side involves inject's power using symbol#to_proc shortcut methods.

Inject a.k.a. Reduce

The inject method in ruby is a powerful method for reducing (or folding) all the elements of a collection, into a single value. It is commonly known as the reduce or fold method in other programming languages. Ruby provides an alias to reduce via inject.

Let's walk through a typical use case for the inject method in ruby. I originally thought of this idea after reading this awesome post, by Joey Devilla. He uses folding paper to demostrate how inject works. Rather than recreate his post, I've decided to use playing cards to demonstrate another type of explanation.

The darkside collection

Thought I was done with the Star Wars theme, eh? Not quite.

If you look at the above picture, I've grabbed five cards out of the deck. For now just assign them number values: 1 for ace, 2 for 2, and so on. Our goal is to calculate the total number of points that we hold in our hand. The cards directly represent our ruby collection that is passed into the block.

The inject method works by taking values and injecting, or folding, them into themselves. For instance the following code is an example of calculating the sum for the above collection using the inject method.

[1, 2, 3, 4, 5].inject { |sum, n| sum + n }
# => 15

Using our smaller deck of cards above, I'll show you exactly what is happening step-by-step. First we take our first card (Ace or 1) and combine with the second card that we drew (2) from the deck.

Drawn

2

In Hand

Ace
# Inject, or fold, 2 into the starting sum of 1
[1, 2, 3, 4, 5].inject { |1, 2| 1 + 2 } # => 3

The inject method takes the first two indexes from the cards in our hand, or the collection, and applies them to the block. Essentially, 1 becomes the sum and 2 becomes n. This is the typical implementation of inject.

Next, drawing a new card from the deck we recieve 3.

Drawn

2

In Hand

Ace 2
# Inject 3 into the sum
[1, 2, 3, 4, 5].inject { |3, 3| 3 + 3 } # => 6

Note that on this iteration of the inject method we already have the cards, ace and 2, in our hand. Therefore, the current sum is 3 and n, or the current drawn card is also 3. This gives us a resulting sum of 6.

Let's draw another card from the deck.

Drawn

4

In Hand

Ace 2 3
# Inject 4 into the sum
[1, 2, 3, 4, 5].inject { |6, 4| 6 + 4 } # => 10

Sensing a pattern here? The previous sum is 6 and the new card is 4 giving us a current sum of 10.

Draw the last card

Drawn

5

In Hand

Ace 2 3 4
# Inject 4 into the sum
[1, 2, 3, 4, 5].inject { |10, 5| 10 + 5 } # => 15

Finally, our last card (5) gets added to the total for a grand total of 15 points for our hand.

In Hand

Ace 2 3 4 5

This is the power of inject in that it can reduce a collection down to a single value. All in a single line of code no less!

Here are some alternative formats for accomplishing the same thing.

(0..5).inject(:+) # => 15, Whoa! We'll talk about (:+) a little later.
(0..5).reduce { |sum, n| sum + n } # => 15, reduce is an alias to inject

If you only knew the power of Ruby's inject method!

You dont know the power of the darkside - From http://heavy-metal-aesthetic.tumblr.com/

The inject method has a couple of tricks up its sleeve (card pun). Here are several of the formats that you can utilize. These are taken straight from the ruby docs.

inject(initial, sym)  obj
inject(sym)  obj
inject(initial) { |memo, obj| block }  obj
inject { |memo, obj| block }  obj

inject(initial) { |memo, obj| block } → obj

This format of inject allows the developer to pass in an optional initial value to be used as the first element. Other than that this format is exactly the same as inject { |memo, obj| block } → obj.

Looking at our above example we essentially could say the initial value of the block is the ace (1), or our first card. Inject assumes the first index as your initial value unless you specify it using the inject(initial) optional format. This is what would happen if instead of using the ace we had drawn another 5 before the other cards.

[1, 2, 3, 4, 5].inject(5) { |sum, n| sum + n } # => 20

Now our first iteration of the loop yields { |5, 1| 5 + 1 } and increases the ending sum by 5 making it 20. This can come in handy if you have an existing collection and need to use a different starting point.

Staying on target in the real world with an example

From giphy.com

Let's take a look at a real world example. Say we had a forum for asking questions on our website between buyers and sellers. The buyer creates questions that can be shared between multiple projects. On these projects, the seller answers the required questions.

Now the project requirement is to lock the buyer out of editing a question, if that particular question is used on more than one project or location in the app. We need to make sure that we have a starting value of 0 in this example which is where our initial value comes into play.

[seller.projects.questions, seller.other_locations.questions].inject(0) do |sum, n|
  break sum if sum > 1
  sum + n.size
end
"...lazy evaluation, or call-by-need is an evaluation strategy which delays the evaluation of an expression until its value is needed (non-strict evaluation) and which also avoids repeated evaluations..."

- Wikipedia

The above code will check the size of the collection within the array (e.g. seller.projects.questions), and add its size to the sum.

Then by adding a stop condition to the inject statement break sum if sum > 1;, the inject iteration will stop once we determine that a question is in use elsewhere in the app. This utilizes a technique called lazy evaluation to essentially not do any more work than is necessary.

Now that we have an understanding into the inner workings of inject, we'll now take look at the darkside of the force with Symbol#to_proc.

It’s true. All of it. Enumerables, Symbol#to_proc. They’re real.

From polygon.com

Do you remember seeing this example above? (0..5).inject(:+) #=> 15. This format is known as Symbol#to_proc or inject(sym) → obj. This is also known as using some of Ruby's built-in syntactical sugar.

inject(sym) → obj

In more simple terms, inject(sym), allows you to pass in a symbol which is then executed against the collection. For instance :+, would take the elements from the collection and add them to each other one at a time.

Some addtional examples of valid symbols include: :+, :-, :*, and :/.

Let's take a look at the two bits of code that accomplish the exact same thing.

[1, 2, 3, 4, 5].inject { |sum, n| sum + n } #=> 15
[1, 2, 3, 4, 5].inject(:+) => 15
(0..5).reduce(:+) => 15, this is the same thing again just different format

This can be quite handy for quickly implementing basic logic against a collection. Additionally, just like above the inject(sym) → obj format can also accept an initial value in the following format inject(initial, sym) → obj Yoda inspirational quote

Pretty awesome, no?

Symbol confusion

There two different symbol formats you may encounter online. Up until Ruby 1.8.7, the inject method required that you prepend symbols with an ampersand (&:+) to convert them to a Proc so that inject could understand them. With 1.8.7 the inject method will accept both :+ and &:+ as valid symbols.

Now before you go applying :+ and :* to other enumerable methods such as (0..5).map, keep in mind that these require the usage of the ampersand to convert them to proc. Only inject and reduce have this functionality of current.

In the next Enumerables series post, we'll take a look at the map method. So keep reading to learn more about how powerful Ruby's enumerable methods really are.

Got a method you'd like to see analyzed? Leave me a comment below.

Join the conversation

comments powered by Disqus