Control Flow in Clojure
Learn control flow in Clojure - conditionals with if/when/cond/case, recursion, loop/recur, and sequence-driven iteration with Docker-ready examples
Control flow is how a program decides what to do next. In most languages this means statements that execute for their side effects—if blocks, for loops, switch cases. Clojure approaches control flow differently: as a functional Lisp, almost everything is an expression that returns a value, and there are no statements to sequence.
Because Clojure favors immutability, it doesn’t lean on counter-based loops that mutate a variable. Instead, iteration is expressed through recursion, the loop/recur construct, and—most idiomatically—through sequence operations that transform whole collections at once. Conditionals like if, when, cond, and case are themselves expressions: they evaluate to a value you can bind, return, or pass along.
In this tutorial you’ll learn how Clojure expresses branching with if and its friends, how to choose between many cases with cond and case, and how to iterate without mutable loop counters using loop/recur, dotimes, and doseq. Along the way you’ll see why thinking in expressions and sequences—not statements—is the heart of writing idiomatic Clojure.
Conditionals: if, when, if-not
The fundamental conditional is if. It takes a test, a “then” expression, and an optional “else” expression. Crucially, if returns a value—it isn’t a block of statements.
In Clojure, only false and nil are falsey; everything else (including 0, "", and empty collections) is truthy.
Create a file named conditionals.clj:
| |
when is preferred over if when there is no else branch and you have multiple expressions to evaluate—its body has an implicit do, so all expressions run in order and the last one is returned.
Choosing Among Many: cond and case
When you have more than two branches, nesting if expressions gets unwieldy. Clojure provides cond for arbitrary test conditions and case for matching against constant values.
Create a file named multi_branch.clj:
| |
Use case when you’re matching against fixed literal values—it’s faster because it dispatches in constant time. Use cond when each branch needs a different test expression (ranges, predicates, multiple variables).
Iteration with loop and recur
Clojure has no mutable loop counter. For explicit iteration, use loop to establish initial bindings and recur to jump back to the loop with new values. recur performs tail-call optimization, so this won’t blow the stack even for large counts.
Create a file named recursion.clj:
| |
The accumulator pattern in factorial—carrying the running result as a loop binding—is the idiomatic functional alternative to mutating a variable inside a for loop.
Sequence-Driven Iteration: doseq and dotimes
Most real Clojure code doesn’t write explicit loops at all. To iterate over a collection for side effects (like printing), use doseq. To repeat something a fixed number of times, use dotimes. Both return nil—they exist for their side effects.
Create a file named sequences.clj:
| |
The last block shows the most idiomatic Clojure style: rather than looping and accumulating manually, you describe a transformation of the data with map, filter, and reduce. This is the functional mindset—operate on collections as a whole rather than stepping through them element by element.
Running with Docker
You can run every example above without installing Clojure locally.
| |
Expected Output
Running conditionals.clj:
It's cool
nil
Grab a jacket
Maybe a scarf too
Temperature is non-zero
zero is truthy
empty string is truthy
nil is falsey
Running multi_branch.clj:
Score 95 -> A
Score 83 -> B
Score 71 -> C
Score 42 -> F
Saturday -> Weekend
Monday -> Start of the week
Wednesday -> Weekday
Funday -> Unknown day
Running recursion.clj:
5! = 120
10! = 3628800
T-minus 3
T-minus 2
T-minus 1
Liftoff!
fib(10) = 55
Running sequences.clj:
Fruit: apple
Fruit: banana
Fruit: cherry
Iteration 0
Iteration 1
Iteration 2
2 is even
4 is even
6 is even
8 is even
10 is even
Squares: (1 4 9 16 25)
Evens: (2 4)
Sum: 15
Key Concepts
- Conditionals are expressions —
if,when,cond, andcaseall return values, so you can bind their result, return it, or pass it to another function. - Only
falseandnilare falsey — unlike many languages,0,"", and empty collections are all truthy in Clojure. whenvsif— usewhen(andwhen-not) when there’s no else branch and you have one or more side-effecting expressions; its body has an implicitdo.condfor predicates,casefor constants —condevaluates arbitrary test expressions top to bottom;casedoes fast constant-time dispatch on literal values.- No mutable loop counters — iteration uses
loop/recurwith an accumulator instead of mutating a variable;recuris tail-call optimized and won’t overflow the stack. doseqanddotimesare for side effects — both returnnil; usedoseqto walk a collection (with optional:whenfilters) anddotimesto repeat a fixed number of times.- Prefer transformations over loops — the most idiomatic control flow is often
map,filter, andreduce, which operate on whole sequences rather than stepping through them imperatively.
Comments
Loading comments...
Leave a Comment