Delegation
A wise manager delegates. Rather than doing every task themselves, they say: "Here is the problem. Here is someone who knows how to solve it. Let them handle the details."
Programming has the same pattern. Instead of writing detailed loops and conditionals for every operation, we can delegate to functions that handle the mechanics while we focus on the specifics.
These delegating functions are called higher-order functions. They are among the most powerful abstractions in programming.
The Pattern of Iteration
Consider how often we do something like this:
// Double every number in a list
result = []
for each x in numbers:
result.append(x * 2)
return resultAnd this:
// Get the name of every player
result = []
for each player in players:
result.append(player.name)
return resultAnd this:
// Square every number
result = []
for each x in numbers:
result.append(x * x)
return resultThe pattern is always the same:
The only thing that changes is step 3---the transformation. Everything else is boilerplate.
- Start with a collection
- Go through each element
- Transform it somehow
- Collect the results
Map: Delegation of Transformation
What if we could say: "Apply this transformation to every element"---without writing the loop ourselves?
function map(collection, transform):
result = []
for each item in collection:
result.append(transform(item))
return result Visually, map is an assembly line---each element passes through the same machine:
Now our three examples become:
doubled = map(numbers, (x) => x * 2)
names = map(players, (p) => p.name)
squared = map(numbers, (x) => x * x) Each line says what we want, not how to iterate. We delegate the mechanics to map and focus on the transformation.
Filter: Delegation of Selection
Another common pattern is selecting items that match some criterion:
// Get all even numbers
result = []
for each x in numbers:
if x is even:
result.append(x)
return resultAgain, the structure is always the same. Only the condition changes. We can abstract this:
function filter(collection, predicate):
result = []
for each item in collection:
if predicate(item):
result.append(item)
return resultNow:
evens = filter(numbers, (x) => x is even)
adults = filter(people, (p) => p.age >= 18)
active = filter(games, (g) => not g.is_finished)Reduce: Delegation of Combination
Sometimes we want to combine all elements into a single result:
// Sum all numbers
total = 0
for each x in numbers:
total = total + x
return total // Find the maximum
best = first element
for each x in rest:
if x > best:
best = x
return bestThe pattern: start with an initial value, combine each element with the accumulated result. We can abstract:
function reduce(collection, combine, initial):
accumulated = initial
for each item in collection:
accumulated = combine(accumulated, item)
return accumulatedNow:
sum = reduce(numbers, (a, b) => a + b, 0)
product = reduce(numbers, (a, b) => a * b, 1)
max = reduce(numbers, (a, b) => bigger(a, b), first)Composition: Chaining Delegations
The real power emerges when we combine these operations:
// Get the total score of all active players over level 10
active = filter(players, (p) => p.is_active)
experienced = filter(active, (p) => p.level > 10)
scores = map(experienced, (p) => p.score)
total = reduce(scores, (a, b) => a + b, 0)Or, written as a chain:
total = players
|> filter((p) => p.is_active)
|> filter((p) => p.level > 10)
|> map((p) => p.score)
|> reduce((a, b) => a + b, 0)This reads like a description of what we want:
- Take all players
- Keep only the active ones
- Keep only those over level 10
- Get their scores
- Sum them up
No loops. No intermediate variables. Just a declaration of intent.
Back to Chess
Let's apply these patterns to our chess game.
// Get all squares where white has pieces
white_squares = filter(
all_squares,
(sq) => board[sq] is white piece
)
// Get all possible moves for white
all_moves = flatMap(
white_squares,
(sq) => get_moves_from(sq, board)
)
// Keep only legal moves (don't leave king in check)
legal_moves = filter(
all_moves,
(move) => not leaves_king_in_check(move, board)
)Compare this to writing nested loops with conditionals. The delegating style:
- Is more readable (each step has a clear purpose)
- Is more composable (easy to add or remove steps)
- Separates what from how
"Tell Me What, Not How"
Higher-order functions embody a philosophy: tell me what you want, not how to do it.
When we write:
evens = filter(numbers, (x) => x is even)We're saying what we want: the even numbers. We're not saying:
- Create an empty list
- Start at the beginning
- Check each element
- If it's even, add it
- Move to the next
- Continue until done
The "how" is delegated to filter. We trust it to iterate correctly. We focus on what matters: the criterion for selection.
This separation of concerns makes code:
- Easier to read: Intent is clear
- Easier to change: Steps are encapsulated
- Easier to optimize:
filtercan be improved once, benefiting all uses
The Cost of Abstraction
Higher-order functions aren't free. They introduce:
But for most programs, the benefits far outweigh these costs. The code is shorter, clearer, and less prone to off-by-one errors and forgotten edge cases.
- Indirection: You must understand what
mapandfilterdo - New vocabulary: Readers must learn these patterns
- Sometimes less efficient: Creating intermediate collections
Try It Yourself
Try It Yourself
9 exercises to practice
We've learned to name transformations (functions), treat them as values (first-class functions), and delegate iteration patterns to higher-order functions (map, filter, reduce).
But so far, our values have been unstructured. A number is a number. A string is a string. In the next part, we explore how to categorize and constrain values---introducing the concept of types.