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:
- Object#enum_for
- Lazy Chains
- Handling Memory Constraints and Recovering from Errors
[…] 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 […]
[…] Object#enum_for […]