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 calledinject
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
!