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.
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 }`
banisterfiend, that’s an excellent point, and definitely more readable. Thank you for sharing that!
[…] 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 […]