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.