Enumerating Ruby: Lazy Chains

This is the third post in a series on enumeration in Ruby. In the previous posts, we’ve remained within the shelter of the standard library, using the built-in enumeration mechanisms, Object#enum_for and Enumerator.new. In this post, we’re going to go into the deep end and talk about some more advanced uses of enumerators, as well as creating our own lazy versions of a few standard methods.

I really like the functional style of our original solution, where we took a method and turned it into a collection, but the fact that #select and #map don’t have lazy equivalents kind of ruins the magic of the #enum_for.

Given:

1
2
3
4
5
6
7
8
9
10
11
12
13
class RedWingsFan
  def initialize(hockey_client)
    @hockey_client = hockey_client
  end

  def red_wings_goal_scorers
    @hockey_client.enum_for(:listen_for_hockey_scores).select do |goal|
      goal.team == "Red Wings"
    end.map do |goal|
      goal.scorer
    end
  end
end

Problem:

We want our method to return immediately, and give us an object we can enumerate on-demand… while maintaining our functional style and elegant code.

Solution:

There are two things I want to do to this implementation. First, I want to get rid of that #enum_for—don’t get me wrong, it’s a wonderful method, but it’s an eyesore. Second, we need to come up with a lazy alternative to #select and #map.

Erasing the #enum_for:

There are several methods in the standard library that return enumerators if you don’t provide a block.

1
2
3
4
5
>> ["foo", "bar"].each_with_index.to_a
=> [["foo", 0], ["bar", 1]]

>> ["foo", "bar"].cycle(2).to_a
=> ["foo", "bar", "foo", "bar"]

These are great when, for instance, you want to map a collection while taking each element’s index into account.

1
2
>> ["foo", "bar", "baz"].each_with_index.map {|str, idx| str * idx}
=> ["", "bar", "bazbaz"]

If we adopt the same “return an enumerator if you don’t get a block” approach for #listen_for_hockey_scores, then we can skip the #enum_for.

1
2
3
4
5
6
7
def red_wings_goal_scorers
  @hockey_client.listen_for_hockey_scores.select do |goal|
    goal.team == "Red Wings"
  end.map do |goal|
    goal.scorer
  end
end

Lazy selection and mapping:

Michael Harrison has an excellent post on Enumerator where he outlines an implementation of exactly these methods. So let’s use his code, with perhaps a couple of style changes to match our conventions.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
module Enumerable
  def lazy_select(&block)
    Enumerator.new do |yielder|
      self.each do |element|
        yielder << element if yield(element)
      end
    end
  end

  def lazy_map(&block)
    Enumerator.new do |yielder|
      self.each do |element|
        yielder << yield(element)
      end
    end
  end
end

class RedWingsFan
  def initialize(hockey_client)
    @hockey_client = hockey_client
  end

  def red_wings_goal_scorers
    @hockey_client.listen_for_hockey_scores.lazy_select do |goal|
      goal.team == "Red Wings"
    end.lazy_map do |goal|
      goal.scorer
    end
  end
end

red_wings_fan = RedWingsFan.new(HockeyClient.new)
puts "Red Wings scorers:"
red_wings_fan.red_wings_goal_scorers.each do |scorer|
  puts scorer
end

Excellent, now we don’t have any #enum_for or yielder << weirdness in our method! If you’re feeling courageous, you could try overriding the built-in #select and #map to make the laziness transparent.

The full source for this post is available here.

My next post will cover some advanced uses of enumerators that you might encounter in the real world.

Enumerating Ruby Series:

Conversation
  • banisterfiend says:

    Hi, nice article

    However, instead of using:

    `[“foo”, “bar”, “baz”].each_with_index.map {|str, idx| str * idx}`

    You can use:

    `[“foo”, “bar”, “baz”].map.with_index {|str, idx| str * idx }`

  • Michael Harrington says:

    banisterfiend, that’s an excellent point, and definitely more readable. Thank you for sharing that!

  • Ruby 2.0 Enumerable::Lazy | Railsware blog says:

    […] Comments were added to enumerator.c file explaining how laziness can be achieved and since that many many many great articles were published. ruby-lang discussion was started more than 3 (!) years […]

  • Comments are closed.