When you treat your collections as enumerators, you get to use all your favorite functions like #map and #reduce without having to write any extra code. It's pretty awesome.
In the olden days, defining enumerators was a little cumbersome. You had to make a new class, include the Enumerable module, and define an #each function.
Ever since Ruby 1.9, though, we have a much more lightweight way to define enumerators on the fly. Let's take a look.
Introducing the Enumerator class
The Enumerator class lets you define a one-off enumerator, by using a block syntax. In the example below, we create an enumerator that returns an infinite series of random numbers.
e = Enumerator.new do |y|
loop do
y << rand(10) # The << operator "yields" a value.
end
end
# Make the enumerator "yield" 10 values, then stop
puts e.first(10).inspect # => [6, 6, 7, 2, 2, 9, 6, 8, 2, 1]
You may have noticed that we're using the shift operator <<
in a strange way. This is a shortcut for the y.yield
method. You will call it for each item in the enumerator. If this all seems a bit magical to you, don't worry. It is.
Collection size
Figuring out the size of a collection poses a problem for Ruby's lazy enumerators. To count the items in a collection, you have to load the entire collection - which is against the entire point of using lazy enumerators.
There is a work-around, kind of. If you happen to know the size of the collection at the time you create the Enumerator, you can provide it.
# You can pass the length as an argument to the constructor, if you have it
e = Enumerator.new(10) do |y|
10.times { y << rand }
end
My real-world example
Just yesterday I was working on Honeybadger's new documentation site. It's built using Jekyll, and I was writing a plugin to create a table of contents based on the <h2>
and <h3>
tags in the documentation.
It's kind of awkward to figure out which <h3>
tags belong to a section defined by <h2>
tags. You have to parse the HTML using nokogiri, and then scan the resulting document. So I abstracted that bit of code out and made it an Enumerator. Here's what it looks like.
def subheadings(el)
Enumerator.new do |y|
next_el = el.next_sibling
while next_el && next_el.name != "h2"
if next_el.name == "h3"
y << next_el
end
next_el = next_el.next_sibling
end
end
end