Iteration: Doing Things to Many
You have a basket of apples. You want to wash each one. The task is simple: take an apple, wash it, put it aside. Repeat until the basket is empty.
This is iteration---processing a collection of things, one at a time. It's one of the most fundamental patterns in computation. Every program that handles lists, files, records, or streams must iterate.
But here's what's fascinating: there are radically different ways to express the same iteration. Each paradigm has its own idiom, its own way of thinking about "do this to all of those."
The Problem
Let's make it concrete. We have a list of chess pieces, and we want to count how many are pawns.
pieces = [knight, pawn, pawn, bishop, pawn, rook]
// How many pawns?This is a universal problem: given a collection, compute some aggregate (a count, a sum, a filtered subset, a transformation). Let's see how each paradigm approaches it.
Procedural: The Explicit Loop
The procedural mind thinks: "Initialize a counter. Go through each piece. If it's a pawn, increment. When done, the counter has the answer."
count = 0
for each piece in pieces:
if piece.kind == pawn:
count = count + 1
// count is now 3This is maximally explicit. We can see:
- The accumulator (
count) being initialized - The iteration variable (
piece) taking each value - The condition being checked
- The accumulator being updated
The pattern generalizes:
// Sum all piece values
total = 0
for each piece in pieces:
total = total + piece.value
// Collect all pawns
pawns = []
for each piece in pieces:
if piece.kind == pawn:
pawns.add(piece)Functional: Recursion
The functional mind avoids mutable variables. Instead of "update a counter," it thinks: "The count of pawns in a list is either zero (if empty) or the count in the rest, plus one if the first is a pawn."
count_pawns(pieces) =
if pieces is empty:
0
else if first(pieces).kind == pawn:
1 + count_pawns(rest(pieces))
else:
count_pawns(rest(pieces))This is recursion: the function refers to itself. The list gets smaller with each call until it's empty (the base case).
The same pattern for other aggregations:
// Sum values recursively
sum_values(pieces) =
if pieces is empty:
0
else:
first(pieces).value + sum_values(rest(pieces))
// Filter recursively
only_pawns(pieces) =
if pieces is empty:
[]
else if first(pieces).kind == pawn:
[first(pieces)] + only_pawns(rest(pieces))
else:
only_pawns(rest(pieces))Functional: Higher-Order Functions
Recursion works, but writing it out every time is tedious. The functional world abstracts the pattern of iteration into reusable functions.
// count, using filter and length
pawn_count = pieces
|> filter((p) => p.kind == pawn)
|> length
// sum, using map and reduce
total_value = pieces
|> map((p) => p.value)
|> reduce(0, (a, b) => a + b)
// filter directly
pawns = pieces |> filter((p) => p.kind == pawn) The key insight: filter, map, and reduce are higher-order functions---they take other functions as arguments. The iteration logic lives inside them. We just provide the specific operation.
| Function | Pattern |
|---|---|
| map | Transform each element |
| filter | Keep elements matching condition |
| reduce | Combine all elements into one value |
Object-Oriented: Internal Iteration
The object-oriented mind asks: "Who should know how to iterate?" The collection itself!
// The collection iterates itself
pawn_count = pieces.count((p) => p.kind == pawn)
total_value = pieces.sum((p) => p.value)
pawns = pieces.select((p) => p.kind == pawn)This is internal iteration: the collection controls how it iterates. We just say what we want.
OOP also offers the Iterator pattern for external iteration:
const iterator = pieces.iterator()
while (iterator.hasNext()) {
const piece = iterator.next()
// process piece
}The iterator is an object that knows how to traverse the collection. Different collections can have different iterators (list iterator, tree iterator, etc.) but present the same interface.
Comparison: Three Flavors
| Procedural | Functional | OOP | |
|---|---|---|---|
| Style | for loop | recursion / map | internal iteration |
| Control | External | Internal | Internal |
| State | Mutable accumulator | Call stack / new values | Encapsulated |
| Focus | How to iterate | What result looks like | Ask collection |
Choosing an Idiom
When should you use which?
Procedural loops when:
Recursion when:
- You need fine-grained control (break early, skip elements)
- Performance is critical and you want minimal overhead
- The iteration has complex side effects
Higher-order functions when:
Internal iteration / iterators when:
- The data structure is recursive (trees, nested lists)
- You want to avoid mutation entirely
- The problem naturally decomposes into subproblems
- The operation fits a known pattern (map, filter, reduce)
- You want concise, declarative code
- You might want to parallelize later
- You want the collection to control traversal
- Different collections should be processed uniformly
- You want to hide implementation details
The Deeper Pattern
Beneath these different expressions lies a single idea: processing elements one at a time to produce a result.
- Procedural makes the "one at a time" explicit with loop variables
- Functional makes the "result" explicit with return values
- OOP makes the "elements" explicit as collaborating objects
Iteration is how we handle multiplicity. But as systems grow, we face another challenge: managing complexity. The next building block is decomposition---breaking big problems into smaller ones.