Functions as values: composition as the tool of construction
Lesson 04 established that a pure function is trustworthy — same input, same output, no effects, so you can reason about a call by substituting its result. That made each small function reliable on its own. This lesson cashes that in: it shows how to treat a function as an ordinary value — store it, pass it, return it — so you can compose small trustworthy functions into large trustworthy behavior. This is the third of the three pillars (immutable data, pure functions, functions-as-values), and the one that turns the first two into a way of building things.
compose/andThen, with first-class functions, HOFs, closures, and currying as the machinery that makes composition convenient. Original Scala throughout.New capability: treat a function as a value of a function type (
Int => Int); pass and return functions (higher-order functions); capture scope safely with closures; specialize with currying/partial application; and combine functions with compose/andThen into pipelines that stay pure and reasoning-friendly.map/filter/reduce replace hand-written loops. (3) Lambdas and the underscore shorthand, now with meaning. (4) Closures capture lexical scope — safe because what they capture is immutable. (5) Currying and partial application specialize a function. (6) Composition — andThen/compose — the algebra, and why composing pure functions is itself pure. A worked data-cleaning pipeline ties it together.1 · A function is a value, and it has a type
In Scala, Int => Int is a type, exactly like Int or List[String] is a type. It is the type of "things that, given an Int, give back an Int." A value of that type can be stored in a val, dropped into a list, or passed to another function — there is nothing special about functions that keeps them out of the places ordinary values go. That is what "first-class" means.
val inc: Int => Int = x => x + 1 // a value whose type is (Int => Int)
inc(41) // 42 — apply it like any function
val ops: List[Int => Int] = List(inc, x => x * 2, x => x - 3) // functions in a list
ops.map(f => f(10)) // List(11, 20, 7) — apply each to 10
Read x => x + 1 as "given x, produce x + 1" — an anonymous function, also called a lambda. The val inc just gives that value a name. The type annotation Int => Int lets Scala infer that the bare x is an Int. The point: once a function is a value, every tool you already have for values — naming, lists, passing as arguments — works on functions too.
2 · Higher-order functions: passing behavior in
A higher-order function (HOF) is a function that takes a function as an argument or returns one. This is the move that lets you write the loop once and pass in the per-element behavior. The three canonical HOFs on collections replace three families of hand-written loops:
mapList[A].map(f: A => B): List[B]. "Transform each."filterList[A].filter(p: A => Boolean): List[A]. "Keep some."reduce / foldLeftList[A].reduce((a, b) => …): A. "Combine all." (Lesson 09 makes this precise.)Compare a hand-written loop with its HOF form. Both double the even numbers in a list, but the loop entangles three concerns — iteration, the test, the transform — plus a mutable accumulator; the HOF version names each concern separately and has no mutable state:
// imperative: iteration + mutation + logic tangled together
val out = scala.collection.mutable.ListBuffer.empty[Int]
for (n <- nums) { // iteration mechanics
if (n % 2 == 0) // the predicate
out += n * 2 // the transform, plus a mutating accumulator
}
out.toList
// functional: each concern is a separate function value, no mutation
nums.filter(n => n % 2 == 0).map(n => n * 2)
The HOF version reads as a sentence — "keep the evens, then double them" — and because filter and map take pure functions (Lesson 04), each step is independently understandable and the whole expression is referentially transparent. The loop, by contrast, only means something once you simulate the accumulator changing over time.
3 · Lambdas and the underscore shorthand, now with meaning
Lesson 02 showed the syntax; now it has a purpose. A lambda x => x + 1 is just a function value written inline. When the argument is used exactly once, in order, Scala lets you drop the name and write _ for it — the underscore shorthand:
nums.filter(_ % 2 == 0).map(_ * 2) // each _ is "the current element"
nums.reduce(_ + _) // two underscores = first and second args, in order
list.map(_.toUpperCase) // call a method on each element
The shorthand is sugar for a lambda and nothing more: _ * 2 is x => x * 2. Use it when it reads cleaner; reach back for the named form (x => …) the moment the body needs the argument twice or the meaning gets murky.
4 · Closures: capturing scope, safely
A function value can refer to names from the scope where it was defined, not just its own parameters. When it does, it "closes over" those names — it is a closure. The classic shape is a function that builds and returns another function, baking a captured value into it:
def makeAdder(n: Int): Int => Int =
x => x + n // the returned lambda captures n from makeAdder's scope
val add5 = makeAdder(5)
val add10 = makeAdder(10)
add5(100) // 105 — this closure remembers n = 5
add10(100) // 110 — a different closure, remembers n = 10
Each call to makeAdder produces a fresh function carrying its own captured n. This is how you manufacture specialized functions at runtime. And here the earlier pillars pay off: because n is an immutable value (Lesson 03), the closure captures a thing that can never change, so add5 means +5 forever — no "spooky action," no shared mutable state lurking inside the function. The closure is as trustworthy as the value it closed over.
5 · Currying and partial application: specialization
A curried function takes its arguments in separate parameter lists, one at a time. Supplying only some of them is partial application: it returns a new function waiting for the rest — which is just a closure over the arguments you already gave.
def add(a: Int)(b: Int): Int = a + b // two parameter lists = curried
add(3)(4) // 7 — apply both
val add3 = add(3) _ // partial application: Int => Int, with a fixed at 3
add3(4) // 7
add3(10) // 13
Why bother? Specialization and composition. add(3) _ is a one-liner factory for "add three to something" — the same thing makeAdder(3) did, but falling out of the function's own shape. More usefully, a curried function lets you fix the "configuration" arguments early and leave the "data" argument last, producing exactly the single-argument function that map/filter and the composition operators below want to consume.
6 · Composition: the headline
If f: A => B and g: B => C, you can glue them into one function A => C. Scala gives two operators that differ only in reading order:
andThen reads in data-flow order (run f, then feed the result to g); compose reads in the mathematical order g ∘ f. They produce the same function — pick whichever matches how you want to read the pipeline. Composition is the operation Lesson 00 named "the primary tool of construction": you build a big behavior by snapping small behaviors together end to end.
This works cleanly because the functions are pure (Lesson 04). The result of f andThen g depends only on its input — no step has reached out to the world or to shared state — so the composite is itself a pure function. Composition of pure functions is closed: glue any number of them and you still have one referentially transparent function you can reason about by substitution. That is the whole reason FP can build large programs out of small pieces without the pieces interfering.
val parse: String => Int = s => s.trim.toInt // " 42 " => 42
val normalize: Int => Int = n => n.max(0) // clamp negatives to 0
val validate: Int => Int = n => n.min(100) // cap at 100
val clean: String => Int = parse andThen normalize andThen validate
Trace clean(" 250 ") by substitution, left to right:
clean(" -7 ") → parse → -7 → normalize → 0 → validate → 0. clean is one pure function String => Int built from three; you can test each piece alone, swap one out, or read the whole thing as a single sentence — "parse, then clamp low, then cap high."7 · Common mistakes and failure modes
var in a closurevar n = 1; val f = () => n; n = 99; f() returns 99, not 1 — the closure captured a mutable cell, so it sees later edits and reintroduces exactly the shared-state spookiness Lesson 03 banished. Close over vals (immutable values) only; then a closure means one thing forever.compose and andThenf andThen g runs f first; f compose g runs g first. Reversed order silently produces a different (often type-mismatched, sometimes not) result. Mantra: andThen = and-then-next reads left→right; compose reads right→left like math's g ∘ f._ andThen _ compose _ with no named intermediates can collapse to line noise. Composition is for clarity; if naming a step (val normalize = …) makes the pipeline read like a sentence, name it. Cleverness that needs a whiteboard to decode is a net loss.Checkpoint exercise
val f: Int => Int = _ + 1, val g: Int => Int = _ * 2, val h: Int => Int = _ - 3:
(a) Compose them two different ways —
f andThen g andThen h and h andThen g andThen f — and predict the output of each on input 5 by substitution before running it. (They differ: one gives (5+1)*2−3 = 9, the other ((5−3)*2)+1 = 5 — confirm which is which.)
(b) Write a higher-order function
def applyTwice[A](f: A => A, x: A): A that applies f to x twice. Check that applyTwice(g, 5) is 20 and that it equals (g andThen g)(5) — i.e. applying twice is composing a function with itself.Where this points next
We have used functions operationally: passed them, returned them from closures, specialized them by currying, and glued them with composition. Lesson 06 asks what a function is mathematically — a map between sets, with a domain and a codomain — and turns that lens on types themselves: types as sets. Once a type is "the set of its legal values," you can count and shape those sets (product and sum types) to make illegal states unrepresentable, so the compiler rejects bad data before any function runs. The composition you learned here is exactly what Lesson 07 will recognize as the defining operation of a category — the same algebra, now named.
Int => Int: you can name it, list it, pass it, and return it. Higher-order functions (map, filter, reduce) take behavior as an argument and so replace hand-written loops, separating iteration from logic and dropping mutable accumulators. Lambdas and the _ shorthand write those function values inline; closures capture lexical scope and are safe precisely because what they capture is immutable (Lesson 03). Currying and partial application manufacture specialized functions. The headline is composition: (f andThen g)(x) == g(f(x)) builds large behavior from small, and because the pieces are pure (Lesson 04) the composite is pure too — so pipelines stay reasoning-friendly no matter how long they get. Composition is FP's tool of construction.Interview prompts
- What does it mean for functions to be "first-class," and how do you write a function's type in Scala? (§1 — a function is an ordinary value you can store in a
val, put in a list, pass, or return; its type is writtenA => B, e.g.Int => Int.) - What is a higher-order function, and how does
map/filterimprove on a hand-written loop? (§2 — a HOF takes or returns a function;map/filterwrite the iteration once and accept the per-element behavior as a value, separating iteration from logic and removing the mutable accumulator.) - What is a closure, and why is it safe in FP? (§4 — a function value that captures names from its defining scope;
makeAdder(n)returns a lambda closing overn. It is safe because captured values are immutable (Lesson 03), so there is no hidden shared mutable state.) - Explain currying and partial application; why are they useful? (§5 — a curried function takes arguments in separate lists; supplying some (
add(3) _) returns a closure over them. Useful for specialization and for producing the single-argument functions thatmap/composition want.) - Distinguish
andThenfromcomposewith the algebra. (§6 —(f andThen g)(x) == g(f(x))reads left→right;(g compose f)(x) == g(f(x))reads right→left likeg ∘ f. Same result, opposite reading order.) - Why is the composition of two pure functions itself pure, and why does that matter? (§6 — neither step touches the world or shared state, so the composite's output depends only on its input; it stays referentially transparent, letting you build arbitrarily long pipelines you can still reason about by substitution.)
- Show the trap of capturing a
varin a closure. (§7 — the closure captures the mutable cell, not a snapshot, so later edits change what it returns — reintroducing the shared-mutation bug; close overvals only.)