Take me home

Result equals new array

Written by August Lilleaas, published September 12, 2008

If you ever find yourself doing result = [], follewed by modifying the result, and and then returning it when you've finished modifying it, you're letting everyone know you don't know how to program in Ruby.

detect

def accepted_users
  result = []
  users.each {|u| result << u if u.accepted? }
  return result
end

No. each is for looping, not selecting certain items.

def accepted_users
  users.select {|u| u.accepted? }
end

map

def for_select(items)
  result = []
  items.each {|i| result << "<option>#{i}</option>" }
  return result
end

No again. each is for looping, not mapping.

def for_select(items)
  items.map {|i| "<option>#{i}</option>" }
end

Magic mofo - inject

What exactly is happening when one does result = [] stuff?

Create
result = []
Loop and alter
(1..10).each {|i| result += i }
Return
result

This is exactly what inject does, too. Initial object, loop through the items, add stuff on each iteration, and return the initial object. But it lets you one-line the process.

(1..10).inject(0) {|result, i| result + i }
# => 55

Here's another example. First, the evil result = {} (it's not an array, but the concept is the same):

def filenames_with_path(filenames)
  result = {}
  filenames.each {|d| result.merge!("/path/to/#{d}" => d) }
  result
end

filenames_with_path(['foo.txt', 'bar.pdf', 'hello.jpg'])
# => {"/path/to/bar.pdf"=>"bar.pdf", "/path/to/foo.txt"=>"foo.txt", "/path/to/hello.jpg"=>"hello.jpg"}

Then, an inject version. A sensible one-liner.

def filenames_with_path(filenames)
  filenames.inject({}) {|result, d| result.merge!("/path/to/#{d}" => d) }
end

filenames_with_path(['foo.txt', 'bar.pdf', 'hello.jpg'])
# => {"/path/to/bar.pdf"=>"bar.pdf", "/path/to/foo.txt"=>"foo.txt", "/path/to/hello.jpg"=>"hello.jpg"}

Inject can do anything

Don't go too wild, though. inject can do everything map, detect etc. can. Both of those methods returns a new object, you know. Exactly as inject.

The result of these two calls are identical, as you can see:

data = ["foo", "bar", "baz"]

data.inject([]) {|result, i| result.push("#{i} is nice") }
# => ["foo is nice", "bar is nice", "baz is nice"]

data.map {|i| "#{i} is nice" }  
# => ["foo is nice", "bar is nice", "baz is nice"]

Important to know about inject

This fails:

["foo", "bar"].inject({}) {|data, i| data["#{i} is cool"] = i }
# => IndexError: string not matched

This does not:

["foo", "bar"].inject({}) {|data, i| data.merge("#{i} is cool" => i) }
# => {"bar is cool"=>"bar", "foo is cool"=>"foo"}

Here's how inject works:

Initial object
Call inject on an array or a hash, and give it an object ({}, an empty hash) it should work on.
Start looping
Loop through all the items in the array or hash we called inject on.
First iteration
Yield the object passed to inject (data) and the first object in the collection we called inject on ("foo").
Subsequent iterations
Yield whatever the previous iteration returned and the subsequent object in the collection we called inject on.

This means that we have to be careful what we do in our block. In the example above, this happened:

Did not work
data["#{i} is cool"] = i
Worked
data.merge("#{i} is cool" => i)

As mentioned, when inject loops through the items, the data part is whatever the previous iteration returned. Let's have a look at what the two calls returns.

data["#{i} is cool"] = i
# => "foo"

data.merge("#{i} is cool" => i)
# => {"foo is cool" => "foo"}

The reason data["#{i} is cool"] = i did not work, is that on the second iteration, data was the string "foo". And obviously, "foo"["#{i} is cool"] = i is not what we're looking for.

The reason data.merge("#{i} is cool" => i) worked, is that on the second iteration, data was the hash {"foo is cool" => "foo"}. And, {"foo is cool" => "foo"}.merge("#{i} is cool" => i) is exactly what we're looking for.

You should probably read this section a couple of times and make sure you grok all of it.

You rock!

Congratulations, you're now an awesome Ruby programmer that knows how to use powerul enumerable methods such as detect, map and last but not least inject!


Questions or comments?

Feel free to contact me on Twitter, @augustl, or e-mail me at august@augustl.com.