• Sales: (866) 518-YARD

Iteration Shouldn’t Spin Your Wheels!

By Evan Phoenix | January 27th, 2010 at 10:01AM

This article was originally included in the September issue of the Engine Yard Newsletter. To read more posts like this one, subscribe to the Engine Yard Newsletter.

In this series, Evan Phoenix, Rubinius creator and Ruby expert, presents tips and tricks to help you improve your knowledge of Ruby.


Ruby is a rich language that believes there should be more than one way to express yourself—the many ways of counting and iterating are no exception.

Most Ruby programmers are familiar with the most common one:

Integer#times
  100.times { |i| p i }

Integer#times counts from 0 up to 99, yielding the current number to the block. This a simple, expressive way to execute some code a number of times.

But there are cases where you want to start counting at a number other than 0, no problem:

Integer#upto
  10.upto(20) { |i| p i }

This prints out 10, 11, 12, until it hit 20. It increments by 1, and you'll notice it is inclusive, meaning that in this case we yield 11 items, not 10.

Going up is nice, but sometimes you need to go down, so use #upto's sister:

Integer#downto.
  20.downto(10) { |i| p i }

If you need a little more control over your iteration, you can use:

Range#step
  (10..20).step(2) { |i| p i }

This will print 10, 12, 14, 16, 18, 20.

Now, in this case, we've introduced a Range, which most Ruby programmers are familiar with. It is basically an object that expresses a beginning and an end — in this case, 10 and 20. Range has another trick up it's sleeve:

  (10...20).step(2) { |i| p i }

You'll notice the 3 dots instead of 2. This indicates that this range is exclusive of the end, not inclusive. So 20 is the terminator, but is not in the set of valid values itself.

Range also support #each:

  (10..20).each { |i| p i }

This works exactly the same as Integer#upto. I personally prefer Integer#upto, because I feel it expresses the operation better.

Another domain is counting on a collection. Before 1.8.7 and 1.9, there was pretty much only one method to help you with doing that: Array#each_with_index.

  [:foo, :bar, :baz].each_with_index { |sym, index| p [sym, index] }

This prints out [:foo, 0], [:bar, 1], and [:baz, 2].

This is nice, but it's pretty limiting because the only place you've got that index is with simple iteration. Say you wanted to map the Array and take the position into account — you'd have to do:

  ary = [1, 3, 5]
  i = 0
  ary.map { |element| x = element * i; i += 1; x }

It's kind of messy to just take the position into account. So with 1.8.7 and 1.9, Enumerator support was baked into most methods which makes this much simpler!

  ary = [1,3,5]
  ary.map.with_index { |element, index| element * index }

For those that haven't seen Enumerators yet, you're saying "Hey! Where did the block to map go!" Well there isn't one. Array#map, when passed no block, returns a Enumerator object. This object, when you call #each, calls the original method on the original object and passes the block along. To begin with, this provides external iteration, but it also gives Ruby a place to add iteration alteration methods, such as Enumerator#with_index. Now you never need to use a while loop again!

See you next time!

Update

1.8.7 is a bit inconsistent about when Enumerators are returned. You can instead do:

ary.dup.map!.with_index { |e,i| ... }

Or, as a commenter pointed out:

ary.to_enum(:map).with_index { |e,i| ... }

Share this post:
  • email
  • Digg
  • del.icio.us
  • Reddit
  • Slashdot
  • StumbleUpon
  • Technorati
  • Twitter
  • Google Bookmarks
  • Facebook
  • LinkedIn
Popularity: 29% |
Rate this post: 1 Star2 Stars3 Stars4 Stars5 Stars
Loading ... Loading ...

This website uses IntenseDebate comments, but they are not currently loaded because either your browser doesn't support JavaScript, or they didn't load fast enough.

5 Responses to “Iteration Shouldn’t Spin Your Wheels!”

  1. Ryan Briones Ryan Briones says:

    I sent this to a coworker because he thought he needed to write his own "map with index" a couple of weeks ago. On 1.8.7, at least the 1.8.7 provided by default with Snow Leopard, map always seems to return an array; not an Enumerator. *sad face*

    However I did get the following to work:

    [1,2,3].to_enum(:map).with_index { |f, i| [f, i] }

    Thanks for the article!

  2. I was feeling pretty smug about my Ruby knowledge until the last one about Enumerators which was brand new to me. Awesome!

  3. miky miky says:

    Thank you for the article.

    AFAIK what you've said about enumerators apply only to ruby >= 1.9

    $ irb

    irb(main):001:0> RUBY_VERSION

    => "1.9.2"

    irb(main):002:0> ary = [1,3,5]

    => [1, 3, 5]

    irb(main):003:0> ary.map

    => #<Enumerator: [1, 3, 5]:map>

    irb(main):004:0> exit

    $ rvm use ruby-1.8.7

    Now using ruby 1.8.7 p248

    $ irb

    irb(main):001:0> ary = [1,3,5]

    => [1, 3, 5]

    irb(main):002:0> ary.map

    => [1, 3, 5]

    irb(main):003:0> ary.map.class

    => Array

  4. Tom Tom says:

    Yeah, thanks for the misinformation.

  5. Evan Phoenix Evan Phoenix says:

    Oh geez! So sorry about the Array#map confusion. 1.8.7 is a bit inconsistent about when Enumerators are returned. You can instead do:

    ary.dup.map!.with_index { |e,i| … }

    Or as Ryan pointed out

    ary.to_enum(:map).with_index { |e,i| … }

    Sorry for the confusion.