The magic of enumerators
What are enumerators?
Whenever I try and explain enumerators to people, they tend to have no idea what I’m even referring to. Enumerators are objects that many enumerable methods return if they have not been given a block.
How do I use an enumerator?
The first step is to grab an enumerator to play with. I’m going to start with the most simple one for now:
Now that we have an enumerator, let’s check out what methods it has. Looking at the Ruby docs, we have:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
# each # each_with_index # each_with_object # feed # inspect # next # next_values # peek # peek_values # rewind # size # with_index # with_object |
While many of these are very useful, I’m only going to go over a few. My personal favorite of these methods is the:
.with_index
I find this method to be very useful in some cases, and I usually use enumerators for this functionality only. In most ruby code, you don’t need the index you’re currently at because enumerables take care of those problems very well. However, you start needing every once in a while, especially when working with algorithms and want your code to still look like decent, thought-out ruby code.
What does it do?
.with_index passes the index as an argument to the block you give it after the typical arguments of the enumerator you’re using. For example:
1 2 3 4 5 6 7 8 9 |
enumerator = ["a","b","c"].each enumerator.with_index do |element, index| puts "#{element} is at #{index}" end # >> a is at 0 # >> b is at 1 # >> c is at 2 |
Ok, you understood that much but where is this actually useful? Any algorithms that is dependent on the position of the element, such as a checksum for credit card numbers, can be written much cleaner using the enumerator .with_index method.
Examples
One part of a check sum’s calculation is doubling every other digit. As Rubyists, we immediately go to enumerables to solve all of our problems. A likely first answer is:
1 2 3 4 5 6 7 8 9 10 11 12 |
some_array = [] [4, 5, 9].each_with_index do |digit, index| if index.even? some_array << digit * 2 else some_array << digit end end puts some_array.inspect # >> [8, 5, 18] |
Sure we got our answer, but who likes to shovel things into another array when you’re already using an enumerable method? Here’s how you can do this with the .with_index enumerator method:
1 2 3 4 5 6 7 |
some_array = [4, 5, 9].map.with_index do |digit, index| index.even? ? digit * 2 : digit end puts some_array.inspect # >> [8, 5, 18] |
Doesn’t that just look a little bit nicer? It’s a bit more concise how Ruby should look, and we’re not shoveling things into an array inside of our enumerable method. Even without the ternary, the .with_index makes this looks quite a bit better:
1 2 3 4 5 6 7 |
some_array = [4, 5, 9].map.with_index do |digit, index| if index.even? digit * 2 else digit end end |
I think that’s enough time for that method, let’s look at a few more.
.next and .peek and .take
These two methods are useful if you want to take one step at a time while going through your enumerator. Enumerable methods go through everything in the list, but this is one way to see what’s going on between all that.
What does it do?
.next will give you the next value the enumerator would “yield” to (call the block) and it also iterates once. If there’s nothing else to iterate over, it raises a StopIteration.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
enum = [1,2,3].each puts enum.next puts enum.next puts enum.next puts enum.next # >> 1 # >> 2 # >> 3 # ~> StopIteration # ~> iteration reached an end |
Then we have .peek which does the same as .next but it doesn’t iterate at all. It still raises a StopIteration when reaching the end though.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
enum = [1,2,3].each puts enum.peek puts enum.peek enum.next puts enum.peek enum.next puts enum.peek enum.next puts enum.peek # >> 1 # >> 1 # >> 2 # >> 3 # ~> StopIteration # ~> iteration reached an end |
Then .take(n) will do the same as .next n times:
And if you do need to go back, you have:
.rewind
There’s not much explination needed for this one, so here’s an example:
1 2 3 4 5 6 7 8 9 10 11 12 |
enum = [1,2,3].each puts enum.next puts enum.next enum.rewind puts enum.next puts enum.next # >> 1 # >> 2 # >> 1 # >> 2 |
All rewind does is reset the iteration back to the beginning. Pretty straightforward.
Time to get fancy
Alright, we went over some of the methods that enumerators give us. Now how can we make some enumerators to work with?
Many enumerable methods will return us one when called without a block. Here’s a short list of common enumerable methods that return enumerators:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
[].collect [].each [].each_slice(num) [].each_with_index [].each_with_object [].find [].flat_map [].group_by [].map [].max_by [].min_by [].reject [].select [].sort_by |
We can also use Enumerator.new and give it a block to iterate over. Here’s a relatively simple example:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
enum = Enumerator.new do |yielder| (4..10).each do |num| yielder << num end end puts enum.take(5) # >> 4 # >> 5 # >> 6 # >> 7 # >> 8 |
This yielder parameter is where we add our items to iterate over for our enumerator. A nice thing about this is that we infinitely add items to it, then iterate only a set number of times. For example, if we wanted all the even numbers:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
all_evens = Enumerator.new do |yielder| i = 1 loop do yielder << i if i.even? i += 1 end end puts all_evens.take(5) # >> 2 # >> 4 # >> 6 # >> 8 # >> 10 |
We are looping infinitely here, but we can still take the first n even numbers since the enumerator only generates numbers when we called the .take method.
So what’s so fancy?
We can write a few complex algorithms much simpler using these enumerators, especially those that are dependent on previous values. The famous example of the fibonacci sequence can be generated quite easily without recursion. Here’s one way to construct this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
fib = Enumerator.new do |yielder| 2.times { yielder << 1 } one_before = 1 two_before = 1 loop do add_value = one_before + two_before two_before = one_before one_before = add_value yielder << add_value end end puts fib.take(10) # >> 1 # >> 1 # >> 2 # >> 3 # >> 5 # >> 8 # >> 13 # >> 21 # >> 34 # >> 55 |
I tried to keep the more complex algorithmic pieces out of the way, so we can focus more on the enumerator piece. The enumerator can now give us as many fibonacci values as we want, without having to regenerate everything in a recursive way for every value.
What I mean by a recursive way is something like this:
That’s shorter code you say? Valid point, but the enumerator never has to call itself again, and is thus much faster. I benchmarked the enumerator version and the recursive version to find only the 50th fibonacci number. Here are my results:
In other words, enumerators are super cool and everyone should them.