A Post Entitled What’s in a name? Inject-ing your code
thoughts.inject(world)?
If you’re new to Ruby you may not realize that the title to the blog is an ode to one of my favorite Ruby idioms: inject. In a nutshell, inject is a customizable accumulator that Ruby mixes into collections through the Enumerable module (e.g., arrays and hashes).
Anatomy of an inject-ion
inject essentially has four parts: a collection to be iterated, a seed value, an accumulator, and a block used to ‘accumulate’ the members of the collection. In practice it looks something like this:
collection.inject( seed_value ) do |accumulated_value, collection_member|
accumulated_value.modify_by collection_member
end
Given that structure, here’s what happens
- accumulated_value is initialized with seed_value.
- accumulated_value and the first (second, third, …) value of the collection is passed to the block.
- The result of executing the block — the value returned by the last statement — is assigned to accumulated_value.
- Repeat the previous two steps for every element of the collection.
- Return the last value assigned to accumulated_value.
A simple example
The simplest, and probably canonical example, of using inject is to sum the values of an array.
[1, 2, 3, 4, 5].inject( 0 ) do | total, value|
total + value
end
Just to be thoroughly pedantic, let’s step through what’s going on here based on the explanation above. First, total is assigned the seed value of 0. Next, 0 and the first value of the array (1) is passed to the block and the block evaluates total + value (0 + 1) and assigns it to total. The next iteration passes that total (1) and the next value (2), performs the addition, and assigns 2+1=3 to total. And so on.
Now, if you’re using Ruby to build a database-backed application this might seem to be a bit of a waste. You can and should delegate simple sums like this to the database and avoid the cost of returning the values, instantiating classes, and then reading the attributes you want to sum. Databases live for that. Let them.
A more practical example
In one app that I support we were discussing how to bill customers. Stunning as it may be, that was a reasonably important issue for us so we wanted to get it right. The application primarily provides member management support and billing is tied to the population of members. The catch was that, at the current moment, both the member and the membership could be made inactive. I’m sure that there’s a sql guy out there who’s already got nested selects in mind but our app need only bill one time per month so we can afford to be a little less than optimal here. Inject to the rescue.
customer.memberships.inject(0) do |total, membership|
membership.expired? ? total : total + membership.members.active.count
end
So, what’s going on? It’s really not that much more complicated than the sum example above. As before we initialize the count with 0. Next, we check each membership. If the membership has expired then we just return the previously calculated total. If not, we add the number of active members on the membership to the previously calculated total.
But wait, there’s more!
There are two great things about inject that should not be overlooked. First, the accumulator can accumulate in any way you want. That means you can use it to transform data as well. For example, we could test to see how random random really is by checking to see how well it distributes numbers by using inject.
numbers = []
1000.times{ numbers.push rand(4) }
numbers.inject( Hash.new(0) ){|frequencies, num| frequencies.merge num => frequencies[num]+1}
=> {0=>254, 1=>249, 2=>248, 3=>249}
Furthermore, since inject is a method that gets mixed in from the Enumerable module that means you can use it far more widely than just arrays. Hashes, strings, ranges… you name it. Anything that uses Enumerable can take advantage of inject. Taking a variation on the number frequency above, what about finding the average length of a ‘line’ in a string?
str = "The quick\nbrown fox jumped\nover the lazy\ndog."
str.inject( Hash.new(0) ) do |lengths, line|
len = line.split(/\s+/).size
lengths.merge len=>lengths[len]+1
end
=> {1=>1, 2=>1, 3=>2}