Err the Blog Atom Feed Icon
Err the Blog
Rubyisms and Railities
  • “Allow Me to Inject”
    – Chris on January 16, 2007

    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.

    inject.

    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.

  • Piotr Usewicz, about 2 hours later:

    As usual, great post, thanks!

  • Sam Aaron, about 16 hours later:

    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 :-)

  • Greggory Bluvshteyn, 1 day later:

    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

    factorial 3 => 6

    factorial 0 => 1

  • Zlaj, 2 days later:

    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).

  • Chris, 3 days later:

    Thanks for the benchmarks, Zlaj! Useful to know that inject is not the answer if speed is an issue.

  • Jason L., 10 days later:

    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?)

  • Roman LN, 12 days later:

    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:

    # same as: evens = (0..10).select { |number| number % 2 == 0 }
    evens = (0..10).inject([]) do |array, number|
      if number % 2 == 0
        array << number   # instead of: array + [number]
      else
        array
      end
    end
    
  • George, 12 days later:

    Fold is a better name than inject. it is the same thing execpt it’s shorter and it is more functional sounding.

  • Moofius, 21 days later:

    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!
    "'"+str.gsub(/\0/, "\\0").gsub(/\n/, "\\n").gsub(/\r/, "\\r").gsub(/\032/, "\\Z").gsub(/\'/, "\\\'").gsub(/\"/, "\\\"").gsub(/\\/, "\\\\")+"'"
    </pre>
    <- that code does it in much shorter time, 0,1 sec I think

    the .chars built in into rails 1.2 is also slow, took 6 sec for 3000 strings to be converted to downcase (4 with the ”???”-gem someone made me test), but with the “unicode”-gem It was so fast that I could not measure it.

    rexml is also slow (but you probable knew that right ;) ), It took 3 sec to parse a 500 kb xml-file. Then I tested hpricot, and it did it in 4 !!!! I finaly settled with libxml and got the time down to 0.5 sec.

  • UserASD, 7 months later:

    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’ }

    ?

  • Ten people have commented.
    Chime in.
    Sorry, no more comments :(
This is Err, the weblog of PJ Hyett and Chris Wanstrath.
All original content copyright ©2006-2008 the aforementioned.