Beware of Ruby Sugar

Yesterday, I posted a tip about iterating through time with Ruby and Rails/ActiveSupport.

Originally, I posted this code. This code is actually not valid in Ruby 1.9 anymore.

# Steps day-by-day from today until 5 days from now, 5.days.from_now).step( do |time|
  puts time

This code looks amazingly compact and incredibly idiomatic … right?

Well, not so “fast”. Take a look at the API documentation for Range#step:

Iterates over rng, passing each nth element to the block. If the range contains numbers, n is added for each iteration. Otherwise step invokes succ to iterate through range elements.

A Time instance is not a number, so Range#step is stuck calling Time#succ 86400 times (the number of seconds in a day) between steps. To add insult to injury, Time#succ creates a new Time instance: we create 86399 useless objects between steps!


While it’s often reasonable to sacrifice a bit of performance for idiomatic, readable code, in this case, the price is certainly too high.

Lesson learned: Peel back at least one level of abstraction when writing any code that looks a bit magical.

In the end, I recommend this code:

now =

# Second argument defaults to 1, but shown for clarity
now.step(now + 5, 1) do |time|
  puts time

DateTime has its own #step instance method which can be much more specific about its implementation than Range#step which must operate correctly on nearly every type of enumerable object.

Final thought: Sometimes abstractions and duck typing are great things, but be especially careful with methods that accept any object that implements a very limited public API (in this case, Range with objects that respond to #succ). In many cases, using only this limited API results in very bad inefficiencies.

— Jan 22, 2011
blog comments powered by Disqus