|
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
|
Latest Ruby Buzz Posts
Latest Ruby Buzz Posts by Matt Williams
Latest Posts From Ramblings
|
|
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]=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:
>> 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 .. 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]
Read: The trouble with injection