Ruby’s injection is very useful, but if you don’t remember one key fact, you’ll shoot yourself in the foot.
The inject
method allows you to perform an operation over all the members of an Enumerable, keeping track of a value throughout. However, the caveat is that you must return the value at each step.
Suppose we wanted to obtain the sum of the numbers from 1 to 100?
1 2 3 |
# Gauss would be jealous!!! >> (1 .. 100).inject() {|sum, n| sum + n} => 5050 |
Ok, that works. But what about the sum of the even numbers?
1 2 3 4 5 6 7 |
>> (1 .. 100).inject() {|sum, n| (sum + n) if (n%2) == 0 } NoMethodError: undefined method `+' for nil:NilClass from (irb):38 from (irb):38:in `inject' from (irb):38:in `each' from (irb):38:in `inject' from (irb):38 |
That’s not at all what we expected. Here’s why:
If you recall from before, the value needs to be returned at each pass through the block. However, here if the number is not even, the block returns an implicit nil
. Then things get weird when we attempt to add a number to nil
. Let’s try this again:
1 2 |
>> (1 .. 100).inject(0) {|sum, n| (n % 2)==0 ? sum + n : sum } => 2550 |
Much better. (edited per comment below by Jeremy Henty)
Another place where errors occur are with arrays and hashes. Suppose I wanted (contrived) to collect objects representing the next seven days in an array. Here’s an attempt (which fails):
1 2 3 4 5 6 7 8 9 |
>> require 'rubygems' >> require 'activesupport' >> (1 .. 7).inject([]){|s, n| s[n] = Time.now + 1.day} NoMethodError: undefined method `[]=' for Thu Aug 14 15:32:58 -0400 2008:Time from (irb):40 from (irb):40:in `inject' from (irb):40:in `each' from (irb):40:in `inject' from (irb):40 |
What’s happening can be illustrated below:
1 2 3 4 5 6 7 8 9 |
>> s=[] => [] >> s[1]=1 => 1 >> s=(s[1]=1) => 1 >> s[2]=2 NoMethodError: undefined method `[]=' for 1:Fixnum from (irb):45 |
The assignment returns the value assigned. Since it is not an array, when we attempt to use an index, we get an error. Here’s a version which works:
1 2 3 4 |
>> week = (0 ... 7).inject([]) do |s,n| ?> s.push((Time.now + n.day)) >> end => [Wed Aug 13 15:03:47 -0400 2008, Thu Aug 14 15:03:47 -0400 2008, Fri Aug 15 15:03:47 -0400 2008, Sat Aug 16 15:03:47 -0400 2008, Sun Aug 17 15:03:47 -0400 2008, Mon Aug 18 15:03:47 -0400 2008, Tue Aug 19 15:03:47 -0400 2008] |
Note the use of Array#push
which returns the entire array. Admittedly, it’s very contrived. A better way would be to use map
, but that’s a discussion for another day.
1 2 |
>> (1 .. 7).map do |n| Time.now + n.day;end => [Thu Aug 14 15:04:20 -0400 2008, Fri Aug 15 15:04:20 -0400 2008, Sat Aug 16 15:04:20 -0400 2008, Sun Aug 17 15:04:20 -0400 2008, Mon Aug 18 15:04:20 -0400 2008, Tue Aug 19 15:04:20 -0400 2008, Wed Aug 20 15:04:20 -0400 2008] |
2 comments
Jeremy Henty
August 13, 2008 at 10:53 pm (UTC -5) Link to this comment
“=> 2551 Much better.” D’ya think? What are the chances of
the sum of even numbers being odd? How about
irb(main):002:0> (1 .. 100).inject(0) {|sum, n| (n % 2)==0 ? sum + n : sum }
=> 2550
That’s better!
Matt Williams
August 13, 2008 at 11:00 pm (UTC -5) Link to this comment
D’oh! Yeah, you’re right; without the 0 as the argument to inject, it uses the first element as the seed.