davideisinger.com

My personal website
Log | Files | Refs | README

index.md (3084B)


      1 ---
      2 title: "Get Lazy with Custom Enumerators"
      3 date: 2015-09-28T00:00:00+00:00
      4 draft: false
      5 canonical_url: https://www.viget.com/articles/get-lazy-with-custom-enumerators/
      6 ---
      7 
      8 Ruby 2.0 added the ability to create [custom
      9 enumerators](http://ruby-doc.org/core-2.2.0/Enumerator.html#method-c-new)
     10 and they are
     11 [bad](https://themoviegourmet.files.wordpress.com/2010/07/machete1.jpg)
     12 [ass](https://lifevsfilm.files.wordpress.com/2013/11/grindhouse.jpg). I
     13 tend to group [lazy
     14 evaluation](https://en.wikipedia.org/wiki/Lazy_evaluation) with things
     15 like [pattern matching](https://en.wikipedia.org/wiki/Pattern_matching)
     16 and [currying](https://en.wikipedia.org/wiki/Currying) -- super cool but
     17 not directly applicable to our day-to-day work. I recently had the
     18 chance to use a custom enumerator to clean up some hairy business logic,
     19 though, and I thought I'd share.
     20 
     21 **Some background:** our client had originally requested the ability to
     22 select two related places to display at the bottom of a given place
     23 detail page, one of the primary pages in our app. Over time, they found
     24 that content editors were not always diligent about selecting these
     25 related places, often choosing only one or none. They requested that two
     26 related places always display, using the following logic:
     27 
     28 1.  If the place has published, associated places, use those;
     29 2.  Otherwise, if there are nearby places, use those;
     30 3.  Otherwise, use the most recently updated places.
     31 
     32 Straightforward enough. An early, naïve approach:
     33 
     34 ```ruby
     35 def associated_places
     36   [
     37     (associated_place_1 if associated_place_1.try(:published?)),
     38     (associated_place_2 if associated_place_2.try(:published?)),
     39     *nearby_places,
     40     *recently_updated_places
     41   ].compact.first(2)
     42 end
     43 ```
     44 
     45 But if a place *does* have two associated places, we don't want to
     46 perform the expensive call to `nearby_places`, and similarly, if it has
     47 nearby places, we'd like to avoid calling `recently_updated_places`. We
     48 also don't want to litter the method with conditional logic. This is a
     49 perfect opportunity to build a custom enumerator:
     50 
     51 ```ruby
     52 def associated_places
     53   Enumerator.new do |y|
     54     y << associated_place_1 if associated_place_1.try(:published?)
     55     y << associated_place_2 if associated_place_2.try(:published?)
     56     nearby_places.each { |place| y << place }
     57     recently_updated_places.each { |place| y << place }
     58   end
     59 end
     60 ```
     61 
     62 `Enumerator.new` takes a block with "yielder" argument. We call the
     63 yielder's `yield` method[^1],
     64 aliased as `<<`, to return the next enumerable value. Now, we can just
     65 say `@place.associated_places.take(2)` and we'll always get back two
     66 places with minimum effort.
     67 
     68 This code ticks all the boxes: fast, clean, and nerdy as hell. If you're
     69 interested in learning more about Ruby's lazy enumerators, I recommend
     70 [*Ruby 2.0 Works Hard So You Can Be
     71 Lazy*](http://patshaughnessy.net/2013/4/3/ruby-2-0-works-hard-so-you-can-be-lazy)
     72 by Pat Shaughnessy and [*Lazy
     73 Refactoring*](https://robots.thoughtbot.com/lazy-refactoring) on the
     74 Thoughtbot blog.
     75 
     76 [^1]: Confusing name -- not the same as the `yield` keyword.