~ / track H / applied patterns
Refactoring imperative to functional
IntermediateMost working programmers come to functional programming from an imperative background. The translation rules are surprisingly mechanical: loops become reductions, mutable accumulators become return values, state machines become data threaded through pure functions. Once you know the patterns, you can refactor incrementally — module by module — without rewriting the whole codebase.
Move 1: a for-loop with an accumulator → reduce
A Python-shaped imperative loop:
total = 0
for x in xs:
if x > 0:
total += x * 2
Three things happen at once: filter, transform, sum. In Clojure, each is a named operation; the whole thing is one expression:
Or, with threading and transducers, even tighter:
The shape moved from "step through, mutate, return mutated thing" to "describe the transformation, apply it once."
Move 2: a mutable counter → an explicit accumulator
Imperative:
seen = {}
for x in xs:
seen[x] = seen.get(x, 0) + 1
Functional: pass the dict-being-built through a reduce:
The dictionary is no longer mutable; each step produces a new map sharing
structure with the old one. The shape of the code is the same; the meaning
of = shifts from assignment to binding.
Move 3: nested loops → flatmap (mapcat)
Imperative:
pairs = []
for a in xs:
for b in ys:
if a != b:
pairs.append([a, b])
Functional:
for (which is not a loop; it's a list comprehension) replaces the nested
imperative scan with declarative syntax. Each "loop variable" binds in turn,
:when filters, the body produces each output.
Move 4: state machines → step functions
Imperative state machines mutate a state variable. The functional version
treats each transition as a pure function from old state and event to new
state:
state = {"phase": "idle", "n": 0}
for ev in events:
if ev == "start": state["phase"] = "running"
elif ev == "tick": state["n"] += 1
elif ev == "stop": state["phase"] = "done"
Functional:
The state is data; the transitions are pure; the loop is reduce. You can
test step with example inputs in isolation, which is hard with the
mutating version.
Move 5: I/O at the edges, pure logic in the middle
The biggest refactor isn't a code shape — it's a topology shift. Push the pure transformations to the center of your function and the I/O (reads, writes, prints, network calls) to the edges. The function then becomes testable in two layers: trivial tests of the pure core, integration tests of the thin shell. The next topic (Functional core, imperative shell) goes deep on this.
What to expect
- Less code. A loop-and-mutate sequence often collapses 5 lines into 1.
- Easier tests. Pure transformations need no setup/teardown.
- Different debugging. You can no longer pause and inspect a mutable
variable; instead you inspect the value passed between steps with
tap>orprn. - Some friction with libraries. Java APIs are imperative; you'll write small wrappers that present a functional surface to your own code.
Check yourself
? quiz
An imperative loop builds up a vector by appending in each iteration. What's the canonical functional refactor?
Exercise
Refactor this imperative pseudocode into one functional expression:
result = []
for x in xs:
if x is even:
result.append(x * x)
return sum(result)