Building sequences with Enumerator →
Monday, 14 January 2013
Ruby's Enumerator class is one of my favorites. Most of the time you deal with Enumerators they will have been returned from a class that implements the Enumerable mixin, but you can also use Enumerator to create your own sequences. This article is going to be about doing just that!
A gentle introduction to Enumerators
According to the docs, an Enumerator is “[a] class which allows both internal and external iteration.” I'll be honest that I hadn't encountered the terms “internal iternation” and “external iteration” before, but the concepts are simple enough.
- External iteration: the code that is using the iterable object controls the iteration
- Internal iteration: the iterable object itself controls the iteration
When we say that Enumerators allow both internal and external iteration, this is what we mean:
# external iteration
nums = (1..20).each # returns an enumerator if not called with a block
3.times do
puts nums.next
end
# => 1
# => 2
# => 3
# internal iteration
(1..20).take(3).each do |n|
puts n
end
# => 1
# => 2
# => 3
In the first example other code (3.times) controlled the looping, and the Enumerator's next method explicitly advanced Enumerator's internal pointer and got the next value. In the second example, we took the first three values from the Enumerator and then used each to internally iterate over the values while we simply supplied a block to be executed at each iteration. The beauty of internal iteration is that you don't have to worry about bounds — if the enumerator only has four objects, each won't accidently try to get the fifth and throw an exception, whereas if you iterate over an Enumerator manually and you try to get a value that doesn't exist, you'll get a StopIteration exception that you'll have to catch.
Enumerators as generators
Today what I want to do is use the Enumerator class to create generators.
In computer science, a generator is a special routine that can be used to control the iteration behaviour of a loop. A generator is very similar to a function that returns an array, in that a generator has parameters, can be called, and generates a sequence of values. However, instead of building an array containing all the values and returning them all at once, a generator yields the values one at a time, which requires less memory and allows the caller to get started processing the first few values immediately. In short, a generator looks like a function but behaves like an iterator.
Let's build our first generator, as a simple example case:
evens = Enumerator.new do |e|
a = 2
loop do
e.yield a
a += 2
end
end
What this does should be pretty obvious. Though evens contains an infinite loop, it will only run far enough each time to yield a single value and then hand back execution (it achieves this using Fibers, which is a topic we'll cover at some point in the future). If you want the ten billionth even number you can get it, but if you only want the first three, you won't be stuck with the performance impact of generating more than the first three.
Now let's make some more interesting generators.
Random dice
Let's say you wanted to simulate dice rolls. While you could (pretty easily) calculate a random range for each roll of n dice, you could also create a generator that gave you an infinite series of random dice rolls, and then just sum them up. Like this.
# s is the number of sides
def dice(s)
Enumerator.new do |e|
loop do
e.yield Random.rand(s) + 1
end
end
end
dice(6).take(5)
# => [2, 3, 1, 5, 3]
# 3d6
dice(6).take(3).inject(:+)
# => 10 (something between 3 and 18)
Polygonal numbers
A polygonal number is “a number represented as dots or pebbles arranged in the shape of a regular polygon. The dots are thought of as alphas (units). These are one type of 2-dimensional figurate numbers.” Thus, a triangle number of pebbles is arrangeable as a triangle, a hexagonal number of pebbles is arrangeable as a hexagon, etc.
Polygonal numbers come up reasonably frequently, and there is a simple formula to generate the nth polygonal number for an s-sided figure. We're going to build method that returns a generator for the number of sides you request, and which uses procs to optimize for a few common side counts.
def polynum_generator(s)
Enumerator.new do |e|
gen_proc = case s
when 3
Proc.new { |s, n| ((n ** 2) + n) / 2 }
when 4
Proc.new { |s, n| n ** 2 }
when 5
Proc.new { |s, n| ((3 * (n ** 2)) - n) / 2 }
when 6
Proc.new { |s, n| ((4 * (n ** 2)) - (2 * n)) / 2 }
when 10
Proc.new { |s, n| ((8 * (n ** 2)) - (6 * n)) / 2 }
else
Proc.new { |s, n| ((n ** 2) * (s - 2) - (n * (s - 4))) / 2 }
end
n = 1
loop do
e.yield gen_proc.call(s, n)
n += 1
end
end
end
polynum_generator(3).take(10)
# => [1, 3, 6, 10, 15, 21, 28, 36, 45, 55]
polynum_generator(212).take(10)
# => [1, 212, 633, 1264, 2105, 3156, 4417, 5888, 7569, 9460]
Here's a nice use case for procs (or lambdas) — varying the internal functionality of a method based on passed arguments. Here we define a general formula for calculating the nth term of an s-gonal sequence, but then also provide some pre-simplified formulas for generating certain s-gonal number sequences.
Fibonacci sequence
A classic generator example is generating a Fibonacci sequence, which is trivial with Enumerator.
def fibonacci
Enumerator.new do |e|
a, b = 0, 1
loop do
e.yield a
a, b = b, a + b
end
end
end
fibonacci.take 20
# => [0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 377, 610, 987, 1597, 2584, 4181]
Prime numbers
Finally, Ruby has a fast built-in prime generator, so you don't need to create one of your own. Simply require the module and start generating prime!
require 'prime'
Prime.take(20)
# => [2, 3, 5, 7, 11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71]
Wrapping up
Number sequences are fun, and using Enumerator to generate them is quick and efficient. If you're interested in more sequences, check out the Online Encyclopedia of Integer Sequences, which catalogs thousands and thousands of them.