This post originated from an RSS feed registered with Ruby Buzz
by Matt Williams.
Original Post: The trouble with injection
Feed Title: Ramblings
Feed URL: http://feeds.feedburner.com/matthewkwilliams
Feed Description: Musings of Matt Williams
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?
# 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 .. 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 .. 100).inject(){|sum, n|(n %2)==0 ? sum + n : sum }=>2551
Much better.
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):
>>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:
>> s=[]=>[]>> s[1]=1=>1>> s=(s[1]=1)=>1>> s[2]=2NoMethodError: 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:
>> week = (0 ... 7).inject([])do|s,n|
?> s.push((Time.now+ n.day))>>end=>[Wed Aug 1315:03:47-04002008, Thu Aug 1415:03:47-04002008, Fri Aug 1515:03:47-04002008, Sat Aug 1615:03:47-04002008, Sun Aug 1715:03:47-04002008, Mon Aug 1815:03:47-04002008, Tue Aug 1915:03:47-04002008]
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 .. 7).mapdo|n|Time.now+ n.day;end
=>[Thu Aug 1415:04:20-04002008, Fri Aug 1515:04:20-04002008, Sat Aug 1615:04:20-04002008, Sun Aug 1715:04:20-04002008, Mon Aug 1815:04:20-04002008, Tue Aug 1915:04:20-04002008, Wed Aug 2015:04:20-04002008]