---
title: 3 Mental Models For Ruby Enumerators
teaser: Get a better understanding of Ruby enumerators by looking at them from 3 different
  perspectives.
tags: enumerator,ruby,development
author: Joël Quenneville
published_on: 2024-06-05
---

I love to collect **mental models**. These give me a lens (sometimes several!) for thinking about a given type of problem. For example, thinking of `reduce` as a tool to scale a method that combines 2 items to a method that combines _n_ items helps me [simplify complex reduce functions](https://thoughtbot.com/blog/how-to-approach-a-reduce-problem).

Ruby's `Enumerator` (the class, NOT the `Enumerable` module!) can be a bit tricky to wrap your head around. Here are 3 different mental models I have for it:

1. An `Enumerator` is a **cursor** over a list
2. An `Enumerator` is a **lazy list**
3. An `Enumerator` is a **series generator**

These are by no means the only mental models out there. For example, check out [this deeper dive into the idea of enumerators as a "bring your own traversal" solution](https://thoughtbot.com/blog/return-an-enumerator-when-your-collection-has-multiple-traversals) for when a collection has multiple valid ways of being traversed.

## Cursor

![A diagram showing a list of 4 elements with a cursor paused on item 2](https://images.thoughtbot.com/013nnybf9rixbe9sceaxchgc6j66_enumerable-cursor.png)

Ruby's `Enumerator` can be thought of as a **cursor** within a collection. Unlike an array that has no sense of position, an enumerator remembers where it is in the list and can give you the next item(s). This persists even if you pass the enumerator around. In this way, it [behaves kind of like Ruby's IO objects](https://thoughtbot.com/blog/io-in-ruby#position).

```ruby
array = [1, 2, 3, 4]
cursor = array.to_enum

cursor.next #=> 1
cursor.next #=> 2
```

In addition to the [#next](https://ruby-doc.org/3.3.1/Enumerator.html#method-i-next) method which reads the next value and moves the cursor, you can use [#peek](https://ruby-doc.org/3.3.1/Enumerator.html#method-i-peek) to read the next value without moving the cursor. While you can't step the cursor backwards, you can jump back to the beginning using [#rewind](https://ruby-doc.org/3.3.1/Enumerator.html#method-i-rewind).

If a cursor that you can move forward, pause, pass the cursor to another object, and then keep moving forward sounds like a familar concept, that might be because **cursors are a form of the external iterator pattern**.

## Lazy List

![diagram of a list where the first items are 1 and 2 and the remaining items are question marks](https://images.thoughtbot.com/rilhymzzx0z3ut9nu1xzbmzuauw4_enumerator-lazy-list.png)

Another way to think of `Enumerator` is as a **lazy list**. It's a chain of Schrödinger's boxes. You don't know what each value will be (and neither does Ruby!) until you attempt to read it. Among other things, this allows us to construct infinite lists such as the list of all integers below.

```ruby
integers = Enumerator.produce(1) { |n| n + 1 }
integers.take(3) #=> [1, 2, 3]
integers.next #=> 4
integers.next #=> 5
```

Note that if you try to traverse such an infinite list using a method like `#map`, it will attempt to evaluate every item in your (infinite) list. Regular enumerators allow us to lazily read items, but to get _lazy traversals_ you need to call `#lazy` first.

```ruby
integers.map { |n| n * 2 } #=> will execute infinitely

evens = integers.lazy.map { |n| n * 2 } #=> Enumerator::Lazy
evens.take(3) #=> [2, 4, 6]
```

This mental model is helpful when you only need to interact with the first part of a collection that might be infinite, just too big to fit in memory (e.g. [loading 5 million property ids](https://thoughtbot.com/blog/how-we-used-a-custom-enumerator-to-fix-a-production-problem)), or where each item is expensive to compute.

## Series Generator

![diagram of a robot with speach bubbles that say 1 and 2](https://images.thoughtbot.com/kdfqvnxdxczgkcmmfo1cum9jifbf_enumerable-series-generator.png)

`Enumerator` can also be thought of as a **series generator**. In this mental model, think of an enumerator as a robot you can ask to generate new values for you according to a pattern. This pattern can even be random, as can be seen in the example below. Here we use an enumerator to construct a generator that will give us random numbers between 1 and 100.

```ruby
generator = Enumerator.produce { rand(100) }
generator.take(5) #=> [92, 64, 99, 77, 71]
generator.next #=> 73
generator.next #=> 94
```

This perspective is helpful when you aren't operating on static data but each value in the array needs to be calculated. For example, if you are [fetching data from a paginated API](https://thoughtbot.com/blog/modeling-a-paginated-api-as-a-lazy-stream), each page fetch generates a new set of records that will get yielded one at a time by the enumerator so only every `$PAGE_SIZE` fetch is expensive.

## Teaching Ruby to Count

Want to dig more into these ideas? The 3 mental models were shared in my talk [Teaching Ruby to Count](https://www.youtube.com/watch?v=PHMOsTK1jSE). The talk explores how to make the most of out ranges, enumerators, and enumerables as well as how to make your custom objects play just as nicely with them as built-in Ruby primitives.

<iframe width="560" height="315" src="https://www.youtube.com/embed/PHMOsTK1jSE?si=sIm-qPg2IlvDUMnS" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture; web-share" referrerpolicy="strict-origin-when-cross-origin" allowfullscreen></iframe>
