Practice translating Java loops into Clojure sequence pipelines, reduce, loop/recur, lazy sequences, and direct recursion with stack-safety checkpoints and review prompts.
This practice page is a checkpoint for the whole recursion and looping chapter. The goal is not to prove that recursion is better than Java loops. The goal is to choose the Clojure construct that makes the repeated work easiest to read, test, and maintain.
Use this decision table before starting each exercise:
| If the Java code… | Try this Clojure shape first | Review question |
|---|---|---|
| Builds a new collection | map, filter, keep, into, mapv |
Is each transformation named or obvious? |
| Updates one accumulator | reduce or transduce |
Is the accumulator value explicit? |
Uses break to find one match |
some, first, take-while, reduced |
Does the code stop for the right reason? |
Uses while for retries or scanning |
loop/recur |
Are all evolving bindings visible? |
| Walks nested self-similar data | Direct recursion, tree-seq, explicit stack |
Is the recursive depth bounded or controlled? |
| Performs side effects | doseq, run!, or a named ! function |
Are effects isolated at the boundary? |
Start with this Java loop:
1public static int totalCents(List<Order> orders) {
2 int total = 0;
3
4 for (Order order : orders) {
5 if (order.isPaid()) {
6 total += order.totalCents();
7 }
8 }
9
10 return total;
11}
Write the Clojure version using filter, map, and reduce:
1(defn total-paid-cents [orders]
2 (->> orders
3 (filter :paid?)
4 (map :total-cents)
5 (reduce + 0)))
Then rewrite it as one reduce:
1(defn total-paid-cents [orders]
2 (reduce (fn [total {:keys [paid? total-cents]}]
3 (if paid?
4 (+ total total-cents)
5 total))
6 0
7 orders))
Answer these review questions:
| Question | What to look for |
|---|---|
| Which version is clearer for a teammate new to Clojure? | The pipeline separates filtering and summing; the reducer keeps the pass count to one. |
| Is performance relevant here? | Only after measurement. Prefer clarity until the loop is known to be hot. |
| Where did mutation go? | The changing total became an explicit accumulator value. |
breakJava search loops often use break:
1User admin = null;
2
3for (User user : users) {
4 if (user.isAdmin()) {
5 admin = user;
6 break;
7 }
8}
Write the Clojure version with some:
1(defn first-admin [users]
2 (some (fn [user]
3 (when (:admin? user)
4 user))
5 users))
Now adapt it:
:not-found instead of nil.Use some when the function can return the value you want. Use first plus filter when a pipeline is easier to read.
loop/recur for a Retry LoopNot every loop is a collection transformation. Translate this Java shape:
1int attempts = 0;
2
3while (!client.ready() && attempts < 3) {
4 client.refresh();
5 attempts++;
6}
One Clojure version:
1(defn refresh-until-ready [ready? refresh!]
2 (loop [attempts 0]
3 (cond
4 (ready?) :ready
5 (= attempts 3) :timed-out
6 :else (do
7 (refresh!)
8 (recur (inc attempts))))))
Check the recur rules:
| Rule | Why it matters |
|---|---|
recur must be in tail position |
No extra work can remain after the jump |
recur targets the nearest loop or function entry |
The jump target is local and visible |
| The arity must match the target bindings | Every next state value must be supplied |
recur is not general JVM tail-call optimization. It is an explicit, compiler-checked loop.
This looks innocent but is wrong for reliable effects:
1(map println lines)
Because map is lazy, printing happens only when the returned sequence is consumed. Use doseq for effects:
1(doseq [line lines]
2 (println line))
Practice:
map that sends emails into a doseq block.!, such as send-emails!.Use direct recursion when the data repeats inside itself:
1(def comment-tree
2 {:id 1
3 :text "Root"
4 :replies [{:id 2
5 :text "First reply"
6 :replies []}
7 {:id 3
8 :text "Second reply"
9 :replies [{:id 4
10 :text "Nested reply"
11 :replies []}]}]})
Collect all comment IDs:
1(defn comment-ids [comment]
2 (cons (:id comment)
3 (mapcat comment-ids (:replies comment))))
4
5(comment-ids comment-tree)
6;; => (1 2 3 4)
Now make it production-aware:
Create a lazy producer:
1(def powers-of-two
2 (iterate #(* 2 %) 1))
Consume it safely:
1(take 10 powers-of-two)
2;; => (1 2 4 8 16 32 64 128 256 512)
Practice:
take-while to keep values below 1000.count on the unbounded sequence is a bug.Use this checklist on one of your own translations:
| Review area | Good sign | Warning sign |
|---|---|---|
| Intent | The construct names the work: map, filter, reduce, recur, recurse | The code is a Java loop rewritten token by token |
| State | Accumulators are explicit values | State is hidden in atoms, globals, or mutable Java objects |
| Stack safety | Deep linear work uses reduce or loop/recur |
Plain self-recursion handles unbounded input |
| Laziness | Lazy results are consumed intentionally | Lazy sequence bodies perform effects |
| Interop | Java-side effects are isolated in ! functions |
Pure and side-effecting work are mixed |
flowchart TD
A["Java loop"] --> B["Name the intent"]
B --> C{"Collection result?"}
C -->|Yes| D["map/filter/into"]
C -->|No| E{"One accumulated result?"}
E -->|Yes| F["reduce"]
E -->|No| G{"State machine?"}
G -->|Yes| H["loop/recur"]
G -->|No| I{"Recursive data?"}
I -->|Yes| J["direct recursion or traversal helper"]
I -->|No| K["reframe or isolate Java interop"]
reduce for accumulation.loop/recur for local state transitions.