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."

python
    count = 0
    for each piece in pieces:
        if piece.kind == pawn:
            count = count + 1
    // count is now 3

This 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:

python
    // 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:

python
    // 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.

elixir
    // 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.

FunctionPattern
mapTransform each element
filterKeep elements matching condition
reduceCombine all elements into one value

Object-Oriented: Internal Iteration

The object-oriented mind asks: "Who should know how to iterate?" The collection itself!

python
    // 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:

python
    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

ProceduralFunctionalOOP
Stylefor looprecursion / mapinternal iteration
ControlExternalInternalInternal
StateMutable accumulatorCall stack / new valuesEncapsulated
FocusHow to iterateWhat result looks likeAsk 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.