Browse Learn Clojure Foundations as a Java Developer

Recursion and Looping Practice

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?

Exercise 1: Replace a Mutable Accumulator

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.

Exercise 2: Translate break

Java 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:

  1. Return only the admin email.
  2. Return :not-found instead of nil.
  3. Add a predicate argument so the search can be reused.

Use some when the function can return the value you want. Use first plus filter when a pipeline is easier to read.

Exercise 3: Use loop/recur for a Retry Loop

Not 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.

Exercise 4: Avoid Lazy Side Effects

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:

  1. Rewrite a lazy map that sends emails into a doseq block.
  2. Keep the pure email formatting in a separate function.
  3. Name the side-effecting function with !, such as send-emails!.

Exercise 5: Choose Direct Recursion for Recursive Data

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:

  1. Return a vector instead of a lazy sequence.
  2. Add a depth limit.
  3. Rewrite it with an explicit stack if the tree can be very deep.

Exercise 6: Bound an Infinite Sequence

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:

  1. Use take-while to keep values below 1000.
  2. Convert the bounded result to a vector.
  3. Explain why calling count on the unbounded sequence is a bug.

Exercise 7: Review a Java-to-Clojure Translation

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"]

Key Takeaways

  • Translate intent before syntax.
  • Prefer sequence functions for flat collection transformations.
  • Prefer reduce for accumulation.
  • Prefer loop/recur for local state transitions.
  • Prefer direct recursion for recursive data, with stack depth in mind.
  • Keep side effects eager and obvious.

Quiz: Recursion and Looping Practice

### A Java loop mutates `total` while scanning a list. What is the usual Clojure replacement? - [x] `reduce` - [ ] Direct recursion in every case - [ ] A global atom - [ ] Lazy `map` with side effects > **Explanation:** `reduce` turns the changing total into an explicit accumulator value. ### What is the safest statement about `recur`? - [x] It is an explicit tail-position jump to the nearest function or `loop` entry. - [ ] It automatically optimizes every recursive function. - [ ] It can jump to any function on the stack. - [ ] It only works with lazy sequences. > **Explanation:** `recur` is local, explicit, and compiler-checked. It is not general automatic tail-call optimization. ### Which form should you prefer for printing every line now? - [x] `doseq` - [ ] Lazy `map` - [ ] `filter` - [ ] `def` > **Explanation:** `doseq` is intended for eager side effects. Lazy `map` should produce values, not hide effects. ### When is direct recursion most appropriate? - [x] When the data is self-similar, such as a tree of comments. - [ ] Whenever a Java loop used an index. - [ ] Whenever code needs to be faster than Java. - [ ] Whenever a sequence is flat. > **Explanation:** Direct recursion fits recursive data and recursive algorithms; flat sequence work usually has clearer alternatives. ### True or False: Tail-position `recur` is always faster than every non-tail recursive solution. - [ ] True - [x] False > **Explanation:** `recur` provides stack-safe looping. Actual performance depends on the algorithm, allocation, data shape, and measured workload.
Revised on Saturday, May 23, 2026