Just yesterday I wrote about Enumerable and some of its all-stars. Well, here’s another baller: inject. It took me a bit to truly grok and appreciate this method, but once I did it really cleaned up my code and mind. Zen-like.
Injecting the Comic Book Example
Previously I wanted to find all objects in an array with an item_type equal to Comic (as opposed to Poems).
Here was my first stab:
books = Book.find(:all, :order => 'id desc', :limit => 20) comics = [] books.each do |book| comics << book if book.item_type == 'Comic' end
I cleaned this up with select, but here’s how someone might accomplish this task using inject:
books = Book.find(:all, :order => 'id desc', :limit => 20) comics = books.inject([]) do |array, book| if book.item_type == 'Comic' array + [book] else array end end
This code’ll give me a new array, comics, consisting only of Book objects whose item_type is Comic. How?
In Ruby
As the Apple Rails page says, “The inject method is a Ruby idiom for accumulating while stepping through a collection of objects.” Correct. Here’s how I’d write inject:
# inject's parameter is optional, but let's keep things simple def inject(target) each do |element| target = yield(target, element) end target end
You pass inject a target and a block. The block is passed two elements: the target (which you are injecting) and the current element in your collection. The target is replaced each step of the way by what the block returns as it iterates over your collection. What you’re left with is a beefed up, injected target.
Summing Numbers
The canonical inject example seems to be summing up a range of numbers. Not one to break convention, here you go:
>> (0..10).inject(0) { |sum, number| sum + number } => 55
For fun and profit, concoct your very own sum method:
module Enumerable def sum inject { |sum, number| sum + number } end end >> (0..10).sum => 55 >> [1,2,3,4].sum => 10
If you don’t pass it a parameter, inject will fill its target with the first element of the collection, skipping that element.
Hey, that factorial function everyone’s always throwing around? Trivial.
def factorial(x) return 1 if x.zero? (1..x).inject { |a, b| a * b } end
Even and Odd
Here’s another example: we want all the even numbers from 0 to 10:
evens = (0..10).inject([]) do |array, number| if number % 2 == 0 array + [number] else array end end
Again, our array local variable gets replaced each step of the way by the result of the block. If it’s not an even number, we just return the array as it stands. Be warned: if we returned nil inside of the block we’d run into problems as within the next iteration array would be nil.
Hashes, too
All this haxin’ makes me hungry. I need to find places to eat that aren’t any more than 10 minutes away. Because inject is a method on Enumerable, we can use it with hashes as well:
>> distance_of_food = { :pizza => 10, :thai => 21, :indian => 11, :mexican => 9, :burgers => 15 } => {:pizza=>10, :thai=>21, :indian=>11, :mexican=>9, :burgers=>15} >> distance_of_food.inject({}) do |hash, (place, distance)| key = distance <= 10 ? :near : :far hash[key] ||= [] hash[key] << place hash end => {:near=>[:pizza, :mexican], :far=>[:thai, :indian, :burgers]}
Remember to always end the block with the new value of your target (hash in this case) and also make sure to break out the key and value parameters with parenthesis in your block if iterating over a hash.
Instance Variables to_hash
Want to get a hash of instance variables from an object you’re coding? inject makes it easy:
class Restaurant def initialize(type, minutes_away) @type, @minutes_away = type, minutes_away end def to_hash instance_variables.inject({}) do |hash, ivar| hash.merge(ivar.gsub('@','') => instance_variable_get(ivar)) end end end >> cybelles_pizza = Restaurant.new('pizza', 10) => #<Restaurant:0x4d56c0 @minutes_away=10, @type="pizza"> >> cybelles_pizza.to_hash => {"type"=>"pizza", "minutes_away"=>10}
Glorious.
A More Complex Example
This one uses some Rails code, group_by. If you’re unfamiliar you can get the scoop in this post.
comments = Comment.find(:all, :order => 'id desc', :limit => 30) comments.group_by(&:item_type).inject({}) do |hash, (type, items)| hash.merge(type => type.constantize.find(items.map(&:item_id))) end
What we end up with is a hash keyed by item_type with each member containing an array of items which correspond to that item_type. Not so bad.
injecting
Here’s a little tidbit Ezra was cool enough to share with me: injecting. Try it out:
module Enumerable def injecting(s) inject(s) do |k, i| yield(k, i); k end end end
As he explains, here’s one way to collect a hash:
>> [1,2,3,4,5].inject({}) {|m, el| m[el] = el; m } => {5=>5, 1=>1, 2=>2, 3=>3, 4=>4}
With injecting you needn’t worry about explicitly returning the target—it’s done for you:
>> [1,2,3,4,5].injecting({}) {|m, el| m[el] = el } => {5=>5, 1=>1, 2=>2, 3=>3, 4=>4}
Pretty handy. (By the way, why m? Because the target I keep referring to is commonly known as the memo. Take note.)
Finally, const_grabbin’
One last thing to share. This is pretty neat, from Dave Thomas’ annotate_models plugin:
class_name.split('::').inject(Object) do |klass, part| klass.const_get(part) end
This code is turning a string like Blogs::Grinder into a constant. Think about it. Real slick.
More More More
For more uses of inject, check the ever handy RDoc. Gregory Brown blogged about it on the O’Reilly blog back in July, and then there’s the minimal ruby+inject tag on delicious (which could use some love). Check that page and you’ll see that, as always, Projectionist does not disappoint.
Got any more crazy uses of inject? Hit me up. Pastie for best results. Thanks, yes.
Update: Greggory Bluvshteyn teaches me maths and fixes my factorial method.
As usual, great post, thanks!
Excellent write up. With all the Rails loving going on, people often forget about the loveliness that is Ruby. It’s great seeing people injecting (heh, get the pun…) some enthusiasm back into the mother tongue :-)
Big fan of yours man, keep rolling. Just the special case for factorial though (0)
Nevertheless this inject method is pretty awsome def factorial(x) return (1..x).inject { |a, b| a * b } if x != 0 return 1 if x == 0 end
Great post. I’m learning so much from your posts, I can’t thank you enough!
While I was reading the code snippet using inject for getting the odd numbers out of an array, I was concerned by creating a new array for each iteration with an odd number (array + [number]). So I wrote a small bench using inject, select and a combination of map and compact. It turns out that using inject is much slower.
ar = (1..1000000).to_a
For select, I am using: evens = ar.select {|number| number % 2 0 }
For map/compact I am using: evens = ar.map {|number| (number % 2 0) ? number : nil } evens.compact!
I just wanted to share those results with you (array contains numbers from 1 to 1,000,000): - inject: 10.621 seconds - select: 0.056 seconds - map/compact: 0.0606 seconds
The results are also exponentials: it will take 2ms for 1,000 numbers, 116ms for 10,000, 10.621 secs for 100,000, 41 secs for 1,000,000. Whereas for select and map/compact methods, the results are pretty linear (from 0.5 ms for 1,000 to 112 ms for 1,000,000).
Thanks for the benchmarks, Zlaj! Useful to know that inject is not the answer if speed is an issue.
Here’s my example – using inject with logic operations: http://offtheline.net/2007/1/27/ruby-inject-and-logic-operations For example, if you have an array and you want to know if each item in the array passes a given test, collectively, or check to see if any of those items passes the test (e.g. is this user a member of any of these groups?)
Nice post Chris.
Let me make a small remark on your way to mimic “select” using “inject”: you could make it faster by pushing each element matching the condition into the injected array instead of adding it to an array composed of a sole matching element. Like this:
Fold is a better name than inject. it is the same thing execpt it’s shorter and it is more functional sounding.
I agree that inject is slow, at least for a hash to be the product. After switching to “a = {}; b.each…”-solution from a inject I got a speed down to 1% of the inject-version.
Some other speed increases (not related to inject):
I tried to use the built in solution for escaping strings for mysql but got a huge slowdown with my page, it took 3 sec for it to escape 6000 strings!books = Book.find(:all, :order => ‘id desc’, :limit => 20) comics = books.inject([]) do |array, book| if book.item_type == ‘Comic’ array + [book] else array end end
Why not
... comics = books.reject { |x| x.item_type != ‘Comic’ }
?
Chime in.