Iterators & Enumerable Deep Dive
🔄 Iterators & Enumerable Deep Dive
<=> method and including Comparable unlocks an entire family of comparison operators for free. Enumerable — used constantly since Course 1 Chapter 5 without being built yourself — works on exactly the same principle. This chapter shows the mechanism, then goes further: what happens when a collection is too large, or too infinite, to process eagerly all at once.
🏗️ Building Your Own Enumerable Class
Everything Enumerable provides — map, select, reduce, count, sort_by, dozens more — is built on top of a single method you supply: each.
map, select, and count were never written anywhere in Inventory — every one of them comes from Enumerable, driven entirely by the each method that actually iterates over @items. This is the exact detail Course 1 Chapter 5 promised would eventually be explained.
Making a Class Iterable: PHP vs Python vs Ruby
| PHP | Python | Ruby | |
|---|---|---|---|
| What you implement | Iterator interface: current(), key(), next(), rewind(), valid() | __iter__ and __next__ (or a generator with yield) | A single each method |
| Bonus methods you get for free | None — foreach only | None automatically — itertools helps separately | map, select, reduce, sort_by, count, and dozens more |
PHP's Iterator interface needs five methods implemented by hand for basic foreach support alone, with no equivalent of map/select arriving automatically. Python's generator functions (using yield, similar in spirit to Ruby's) make basic iteration comparably concise — but Python still doesn't hand you a large family of transformation methods the way including Enumerable does.
➡️ Enumerator — Calling each Without a Block
Call any Enumerable method with no block, and Ruby hands back an Enumerator — an external, steppable iterator object rather than immediately running through every element. This is the foundation lazy evaluation is built on, next.
🐢 Lazy Evaluation
By default, Ruby's Enumerable methods are eager: each step in a chain builds a complete new array before the next step even starts. Add .lazy, and the entire chain instead processes one element at a time, only as far as actually needed:
This is a genuine philosophical difference worth internalizing rather than a small syntax quirk. In Python, a generator (or range() in Python 3) is lazy by default — you'd have to deliberately materialize it into a list to make it eager. In Ruby, plain Enumerable chains are eager by default, and laziness is something you explicitly opt into with .lazy. Forgetting .lazy on an infinite or very large range — as the commented-out line above shows — doesn't raise an error; it just hangs, attempting to build an infinitely large intermediate array.
💻 Coding Challenges
Challenge 1: A Custom Enumerable Class
Write a class Playlist that includes Enumerable, stores songs in an internal array, has an add(song) method, and implements each by yielding every song. Add five songs, then call .select to find songs containing a particular word and .count to get the total — neither method should be written directly on Playlist.
Goal: Implement the single each method and prove the rest of Enumerable follows automatically.
Challenge 2: Manual Stepping With an Enumerator
Given the array ["a", "b", "c", "d"], call .each with no block to get an Enumerator, then call .next three times, printing each result, and finally .peek once to preview the fourth element without consuming it.
Goal: Get comfortable with an Enumerator as a pausable, external iterator rather than an all-at-once loop.
Challenge 3: Lazy Evaluation on an Infinite Range
Using (1..Float::INFINITY).lazy, find the first 5 numbers that are both divisible by 3 and greater than 100, chaining .select calls before .first(5). Add a comment explaining what would happen if .lazy were removed.
Goal: Use lazy evaluation to safely work with an infinite sequence.
It's worth remembering how small the actual contract is: Enumerable asks for exactly one method, each, and derives everything else — map, select, sort_by, min, max, group_by, dozens more — purely from calling that one method repeatedly. It's one of the clearest examples in the whole language of a small, well-designed contract producing a disproportionately large amount of free functionality.
🎯 What's Next
Next chapter: File I/O & Serialization — reading and writing files with File/IO, and converting Ruby objects to and from JSON, YAML, and Marshal.