Enumerating Ruby: Object#enum_for

Have you ever wanted to make a lazy collection in Ruby? …build a pipeline of data, doing multiple operations to each element? …or iterate over an infinite sequence? This series will discuss Enumerator (Enumerable::Enumerator in 1.8.7), Object#enum_for, and other ways to simplify working with collections and things that can be thought of as collections.

This first post will focus on Object#enum_for, which can take any method that yields values to a block and turn it into an enumerator.

Given:

I have a class called HockeyClient that provides a method #listen_for_hockey_scores. This method lets me hook up to Versus or some other data source and get all of the goals scored in a particular game. The method blocks until the game is over.

Usage:

1
2
3
4
5
6
hockey_client = HockeyClient.new
puts "Listening to the game..."
hockey_client.listen_for_hockey_scores do |goal|
  puts "#{goal.scorer} scored a goal for #{goal.team}"
end
puts "The game is over."

Problem:

I want to write a method #red_wings_goal_scorers, which will return the names of the Red Wings players who scored goals during the game.

Solution:

Use #enum_for to transform #listen_for_hockey_scores into a collection that I can directly manipulate.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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

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

As you can see, #enum_for turned our listener method into something that we can treat as a collection. This collection is lazily-evaluated, so the call to #enum_for returns immediately, and working with our collection will only block while it waits for the next element.

Turning this into a collection does a few things for us. It opens up a whole world of Enumerable-based operations, such as #take, #select, #map, #max/min. It also provides a better interface from a testing standpoint. Since I’ve turned the scores into a plain old collection, I’m free to use an array in my tests instead of mocking a method to yield some results.

However, this setup is still not ideal. #select and #map are not lazy, and require the entire array before they can start producing values. So even though we’ve turned #listen_for_hockey_scores into something that is much easier to work with, our method will still block until all of the scores are available.

The full source for this post is available here.

My next post will cover some more advanced techniques that let us stay lazy throughout.

Enumerating Ruby Series:

Conversation
  • […] The obvious solution is to read and import the data one line at a time. If we didn’t know about Object#enum_for or Enumerator.new, we might be tempted to shoehorn the importer into the reader and have it be […]

  • Comments are closed.