Enumerable#inject, that is.
I know that there are a lot of Budding Rubyists out there. Are you a former (or current) Java developer? That was me, but I saw the light and have moved on to greener pastures.
A big step in the transition from Java to Ruby was the adoption of inject as a first-class weapon in my iteration arsenal. So, what’s the big deal, and how does it work? Let’s break it down.
You’re going to use inject to iterate through some values and use each of them to manipulate some other object. Let’s say that we have an array of two-element arrays (a la [['a', 1], ['b', 2], ['c', 3]]) and want to construct a hash with the first element as the key and the second element as the value ({ ‘a’ => 1, ‘b’ => 2, ‘c’ => 3}). Someone just transitioning to Ruby might do this:
hash = {}
array.each do |current|
hash[current[0]] = current[1]
end
You can do this with inject like so:
hash = array.inject({}) do |results, current|
results[current[0]] = current[1]
results
end
Ok, to be fair…this code is not simpler. Not as pretty, imho. More Ruby-esque? Sure. Patience, Grasshopper.
You can think of inject as a replacement for each that takes an argument representing the starting state of the object you want to manipulate. We want to construct a hash, so this starting state is {}. The block, then, receives another argument (I call it ‘results’ above): the value of your object passed in from the previous iteration.
One key thing to remember when using inject: you must return the ‘results’ from the iteration at the end of the block. Not doing so is a very common mistake that will totally break you.
Let’s pick a different example: we want to sum the values in an array. Old way:
total = 0
array.each do |current|
total += current
end
total
With inject:
total = array.inject(0) do |total, current|
total + current
end
Again, you mentally translate the above to:
- start with 0
- iterate through each value, adding the current value to the total, and
- pass the total on to the next iteration
Easy peasy. (What, you just want to use ActiveSupport’s Enumerable#sum? Bleh. Fair enough.) Ok, one more. This one actually shows it used in a realistic scenario.
Array.class_eval do
def hash_by(attribute)
inject({}) do |results, obj|
results[obj.send(attribute)] = obj
results
end
end
end
Here, I have reopened the Array class to add a new method, hash_by(). I use it to construct a cache of objects so that I can quickly look up a specific object using a unique object attribute. Here it is in action:
class User
attr_accessor :name, :age, :email
def initialize(name, age, email)
@name = name
@age = age
@email = email
end
end
users = [User.new('Brian', 32, 'brian@foo.bar.com'),
User.new('Jim', 46, 'jim@foo.bar.com'),
User.new('Scott', 33, 'scott@foo.bar.com'),
User.new('Kenton', 32, 'kenton@foo.bar.com'),
User.new('Chris', 34, 'chris@foo.bar.com')]
user_cache = users.hash_by(:name)
puts user_cache['Brian'].inspect
puts user_cache['Scott'].inspect
With the constructed User cache, we can now get to the object of our choice in constant time. Yay, inject!