Don't Forget About Infinite Enumerators
When was the last time you created an Enumerator
?
We use enumerables all over the place but it’s rare to see Enumerator.new
.
If I’m being honest, sometimes I forget it’s an option.
I’m guessing you might as well.
Enumerators don’t have to be infinite but I find that’s where the power lies. With finite enumerations your immediately limiting where they can be used. They come with an extra expectation that must be addressed. Are there enough elements for me? When they’re infinite you can just go crazy.
An Example
I’ve created a List
class with a print
method.
class List
def initialize(items)
@items = items
end
def print
@items.each { |item| puts "* #{item}" }
end
end
It takes an array of items and prints them as a list.
> List.new(['eggs', 'milk', 'bread']).print
* eggs
* milk
* bread
It’s nothing too fancy but it works. The thing is, people want different kinds of markers. Some like the asterisk, some want dashes, others reach for a bullet. Forcing everyone to use the same marker might offend some aesthetics. It’ll also pose a problem if order is important.
I could add an argument to set the marker.
def print(marker)
@items.each { |item| puts "#{marker} #{item}" }
end
Now people can use “-“ or “•” but I didn’t solve the issue of ordered lists. To indicate order I’ll need to support an incrementing marker.
I could accept a symbol indicating the kind of marker.
That’ll limit the set of available markers.
It also means print
might become more marker selection code than printing code.
Sounds like a mess in the making.
As a Rubyist, I’ll fix this with a block.
First a Block
My new block argument means the caller can dictate the marker. I’ll provide the block with the current index allowing for incrementing markers. People using static markers (e.g. asterisks) can ignore the index.
def print
@items.each_with_index do |item, i|
puts "#{yield(i)} #{item}"
end
end
Using an incrementing numeric marker:
> list.print { |i| "#{i + 1}." }
1. eggs
2. milk
3. bread
Using a static marker:
> list.print { '*' }
* eggs
* milk
* bread
Great!
What if the caller wants letter instead of numbers?
I wonder how that works out.
I could add the current index to the ASCII value of "A"
.
> list.print { |i| "#{(65 + i).chr}." }
A. eggs
B. milk
C. bread
At a glance, it looks good but I don’t like it.
Most people don’t spend their nights memorizing the ASCII table.
That 65
falls into the “magic number” category of bad code.
I could fix it by setting it to a well named variable or adding a comment.
There is another, bigger problem.
What does it do after the 26th item?
...
Y. eggs
Z. milk
[. bread
That’s not great.
I’ll have to add logic to handle proper post-alphabet incrementing.
Usually, that means moving from “Z” to “AA”, “AB”, “AC”, and so on.
If I rework this to use next
Ruby will do it for me.
That approach doesn’t really work with the block as I have it.
Then an Infinite Sequence
As I look at it, this block seems to be doing the job of an enumerator. Instead of using a block, I’ll expect to be given an enumerator.
def print(markers)
@items.zip(markers) do |item, marker|
puts "#{marker} #{item}"
end
end
Each item will be joined with a marker generated by the enumerator and then iterated on. Now I can create an infinitely long incrementing alphabetic marker:
def incrementor
Enumerator.new do |yielder|
letter = 'A'
loop do
yielder << "#{letter}."
letter = letter.next
end
end
end
Passing the alphabetic incrementor looks good at first.
> list.print(incrementor)
A. eggs
B. milk
C. bread
What about that 27th item on the list?
...
Y. 25
Z. 26
AA. 27
There we are!
With a minor tweak, I can make this work for numerals.
See, integers also have next
.
I’ll add an argument that works for anything responding to next
.
def incrementor(incrementable)
Enumerator.new do |yielder|
loop do
yielder << "#{incrementable}."
incrementable = incrementable.next
end
end
end
For the alphabetic version pass in "A"
.
> list.print(incrementor('A'))
A. eggs
B. milk
C. bread
For the numeric version pass in 1
.
> list.print(incrementor(1))
1. eggs
2. milk
3. bread
What about non-incrementing markers?
With cycle
we can get an infinite list of the same character.
> list.print(['*'].cycle)
* eggs
* milk
* bread
All of this works but it feels a bit heavy.
Refinement
I can make it easier to use a static marker.
If the marker
passed doesn’t respond to each
then I’ll wrap it with cycle
.
def print(markers)
if !markers.respond_to?(:each)
markers = [markers].cycle
end
@items.zip(markers) do |item, marker|
puts "#{marker} #{item}"
end
end
Numeric and alphabetical incrementing are common so I’ll provide pre-built markers.
def self.incrementor(incrementable)
Enumerator.new do |yielder|
loop do
yielder << "#{incrementable}."
incrementable = incrementable.next
end
end
end
private_class_method :incrementor
NUMERIC = incrementor(1)
ALPHA_UPPER = incrementor('A')
ALPHA_LOWER = incrementor('a')
Put everything together and here’s the final product.
class List
def self.incrementor(incrementable)
Enumerator.new do |yielder|
loop do
yielder << "#{incrementable}."
incrementable = incrementable.next
end
end
end
private_class_method :incrementor
NUMERIC = incrementor(1)
ALPHA_UPPER = incrementor('A')
ALPHA_LOWER = incrementor('a')
def initialize(items)
@items = items
end
def print(markers)
if !markers.respond_to?(:each)
markers = [markers].cycle
end
@items.zip(markers) do |item, marker|
puts "#{marker} #{item}"
end
end
end
By leveraging enumerators I was able to keep print
flexible.
Want to use Roman numerals?
Go for it.
No pull request or monkey patching required.
In a way, it’s unfortunate that Ruby chose the name Enumerator
.
It implies a sequential nature that conceals the truth.
These are generators capable of spitting out an infinite number of… whatever.
Want a bunch of fake names?
> fnames = Enumerator.new do |y|
* loop { y << ['Aaron', 'Bob', 'Claire'].sample }
* end
> lnames = Enumerator.new { |y|
* loop { y << ['Smith', 'Jones', 'Jackson'].sample }
* end
> fnames.take(5).zip(lnames) { |names| puts names.join(' ') }
Claire Jones
Bob Jones
Claire Jackson
Aaron Jones
Bob Smith
If you haven’t created an enumerator before I hope you’re curious. If you have I hope you don’t forget they’re there. I’m doing my best to remember.