Get Lazy with Custom Enumerators

Ruby 2.0 added the ability to create custom enumerators and they are bad ass. I tend to group lazy evaluation with things like pattern matching and currying – super cool but not directly applicable to our day-to-day work. I recently had the chance to use a custom enumerator to clean up some hairy business logic, though, and I thought I’d share.

Some background: our client had originally requested the ability to select two related places to display at the bottom of a given place detail page, one of the primary pages in our app. Over time, they found that content editors were not always diligent about selecting these related places, often choosing only one or none. They requested that two related places always display, using the following logic:

  1. If the place has published, associated places, use those;
  2. Otherwise, if there are nearby places, use those;
  3. Otherwise, use the most recently updated places.

Straightforward enough. An early, naïve approach:

 def associated_places
 [
 (associated_place_1 if associated_place_1.try(:published?)),
 (associated_place_2 if associated_place_2.try(:published?)),
 *nearby_places,
 *recently_updated_places
 ].compact.first(2)
end

But if a place does have two associated places, we don’t want to perform the expensive call to nearby_places, and similarly, if it has nearby places, we’d like to avoid calling recently_updated_places. We also don’t want to litter the method with conditional logic. This is a perfect opportunity to build a custom enumerator:

 def associated_places
 Enumerator.new do |y|
 y << associated_place_1 if associated_place_1.try(:published?)
 y << associated_place_2 if associated_place_2.try(:published?)
 nearby_places.each { |place| y << place }
 recently_updated_places.each { |place| y << place }
 end
end

Enumerator.new takes a block with “yielder” argument. We call the yielder’s yield method1, aliased as <<, to return the next enumerable value. Now, we can just say @place.associated_places.take(2) and we’ll always get back two places with minimum effort.

This code ticks all the boxes: fast, clean, and nerdy as hell. If you’re interested in learning more about Ruby’s lazy enumerators, I recommend Ruby 2.0 Works Hard So You Can Be Lazy by Pat Shaughnessy and Lazy Refactoring on the Thoughtbot blog.

* * *

  1. Confusing name – not the same as the yield keyword.  ↩

David is Viget's managing development director. From our Durham, NC, office, he builds high-quality, forward-thinking software for PUMA, the World Wildlife Fund, ID.me, and many others.

More posts by David