Iterators & Enumerable Deep Dive

Ruby Intermediate/Advanced — Iterators & Enumerable Deep Dive
Ruby Intermediate/Advanced
Course 2 · Chapter 4 · Iterators & Enumerable Deep Dive

🔄 Iterators & Enumerable Deep Dive

Chapter 3 showed that defining a single <=> 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.

class Inventory include Enumerable # same mixin mechanism from Course 1, Chapter 7 def initialize @items = [] end def add(item) @items << item self end def each @items.each { |item| yield item } # the ONE method Enumerable actually needs end end stock = Inventory.new stock.add("Widget").add("Gadget").add("Gizmo") stock.map(&:upcase) # => ["WIDGET", "GADGET", "GIZMO"] stock.select { |i| i.start_with?("G") } # => ["Gadget", "Gizmo"] stock.count # => 3

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

PHPPythonRuby
What you implementIterator interface: current(), key(), next(), rewind(), valid()__iter__ and __next__ (or a generator with yield)A single each method
Bonus methods you get for freeNone — foreach onlyNone automatically — itertools helps separatelymap, 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

enum = [10, 20, 30].each # no block given -- returns an Enumerator object instead of iterating enum.next # => 10 -- pulls one value at a time, manually enum.next # => 20 enum.peek # => 30 -- look ahead without advancing enum.next # => 30

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:

# DON'T do this -- (1..Float::INFINITY) is an infinite range, this hangs forever: # (1..Float::INFINITY).select(&:even?).first(5) # DO this instead -- .lazy processes one element at a time: (1..Float::INFINITY).lazy .select(&:even?) .first(5) # => [2, 4, 6, 8, 10] -- stops as soon as it has 5, never scans "the rest" of infinity
⚠ Ruby defaults to eager — Python's generators default to lazy

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.

→ Solution

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.

→ Solution

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.

→ Solution

💎 each Is the Only Method Enumerable Truly Needs

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.