Browse Learn Clojure Foundations as a Java Developer

Alternatives to Recursion in Clojure

Use sequence functions, reduce, transducers, lazy sequences, doseq, and loop/recur when they express a loop's intent better than direct recursion.

Most Java loops do not need to become direct recursive functions in Clojure. They need to become clearer expressions of transformation, accumulation, search, side effects, or state transition.

Choose by Intent

If the Java loop does this… Prefer this in Clojure Example
Produces one output per input map, mapv Convert cents to dollars
Drops invalid inputs filter, remove, keep Keep active users
Builds a new collection into, mapv, for Build DTO maps
Accumulates one result reduce, transduce Sum totals
Stops when it finds a match some, first, take-while, reduced Find first admin
Performs side effects doseq, run! Log or write records
Advances custom local state loop/recur Retry, poll, scan
Consumes a large stream lazy sequence, transducer, reducer Process file lines

Sequence Functions for Transformations

Java often combines filtering, mapping, and collection building in one block:

1List<String> activeEmails = new ArrayList<>();
2
3for (User user : users) {
4    if (user.isActive()) {
5        activeEmails.add(user.getEmail().toLowerCase(Locale.ROOT));
6    }
7}

Clojure should separate those ideas:

1(require '[clojure.string :as str])
2
3(defn active-emails [users]
4  (->> users
5       (filter :active?)
6       (map :email)
7       (map str/lower-case)
8       vec))

The pipeline is not just shorter. It makes the data contract visible: users in, email strings out.

reduce for Accumulation

When a Java loop updates an accumulator, reach for reduce:

1(defn totals-by-currency [orders]
2  (reduce (fn [totals {:keys [currency cents]}]
3            (update totals currency (fnil + 0) cents))
4          {}
5          orders))

This replaces a mutable Map<Currency, Integer> with a returned value. For Java teams, that shift is useful because the accumulator is local, explicit, and easy to test.

loop/recur for State Machines

Some loops are not collection transformations:

1(defn poll-until [ready? poll! max-attempts]
2  (loop [attempt 1]
3    (cond
4      (ready?) :ready
5      (> attempt max-attempts) :timed-out
6      :else (do
7              (poll!)
8              (recur (inc attempt))))))

This is a good alternative to direct recursion because the algorithm is a local state transition. recur is explicit and stack-safe when used in tail position.

Lazy Sequences for Pull-Based Work

Lazy sequences are useful when the consumer should decide how much work to realize:

1(defn powers-of-two []
2  (iterate #(* 2 %) 1))
3
4(take 8 (powers-of-two))
5;; => (1 2 4 8 16 32 64 128)

Use laziness for values. Do not hide side effects inside lazy map calls:

1(map println lines) ; bad for effects: work is lazy
2
3(doseq [line lines]
4  (println line))   ; clear: effects happen now

Transducers for Reusable Pipelines

Transducers let you define a transformation independent of the input and output collection:

 1(def active-email-xf
 2  (comp
 3    (filter :active?)
 4    (map :email)
 5    (map str/lower-case)))
 6
 7(defn active-email-vector [users]
 8  (into [] active-email-xf users))
 9
10(defn active-email-count [users]
11  (transduce active-email-xf
12             (completing (fn [n _] (inc n)))
13             0
14             users))

Do not introduce transducers just to look advanced. Use them when the same transformation is reused, when avoiding intermediate collections matters, or when the target is not a lazy sequence.

Java Interop and Side Effects

When you are calling Java APIs that are intentionally side-effecting, Clojure should make the boundary obvious:

1(defn write-lines! [writer lines]
2  (doseq [line lines]
3    (.write writer line)
4    (.newLine writer)))

The ! suffix is a convention that signals side effects. It helps reviewers distinguish pure transformations from host interop.

Decision Table

Prefer recursion Prefer an alternative
The input is recursive data The input is a flat collection
The recursive shape explains the algorithm A named sequence function explains the algorithm
The depth is bounded or controlled The depth can be unbounded
You need structural traversal You need accumulation, filtering, or effects

Practice

  1. Rewrite a recursive flat-list sum using reduce.
  2. Rewrite a Java loop that appends to a list using ->>, filter, map, and vec.
  3. Rewrite a retry loop using loop/recur.
  4. Rewrite a lazy map println example using doseq.
  5. Convert a reused transformation into a transducer only after writing the simple pipeline first.

Key Takeaways

  • Sequence functions are the normal choice for flat collection transformations.
  • reduce is the normal choice for one-result accumulation.
  • loop/recur is the normal choice for explicit stack-safe local state.
  • Lazy sequences are for deferred values, not hidden side effects.
  • Direct recursion is still important, but it should earn its place by matching the data shape.

Quiz: Alternatives to Recursion

### What is the best first choice for transforming every item in a flat collection? - [x] `map` - [ ] Direct self-recursion - [ ] A global atom - [ ] `throw` > **Explanation:** `map` directly expresses one output value per input value. ### What should replace a Java loop that updates one accumulator? - [x] `reduce` - [ ] `println` - [ ] `ns` - [ ] `future` > **Explanation:** `reduce` makes the accumulator an explicit value passed through each step. ### Why should side effects not be hidden inside lazy `map` calls? - [x] The work may run later than expected or not at all. - [ ] Lazy `map` always runs twice. - [ ] `map` mutates Java arrays automatically. - [ ] Clojure does not support side effects. > **Explanation:** Lazy sequence work is realized only when consumed, so side effects should use clear eager forms such as `doseq`. ### When are transducers most justified? - [x] When a transformation is reused or intermediate collections matter. - [ ] Whenever a beginner writes their first pipeline. - [ ] Whenever direct recursion would be shorter. - [ ] Whenever Java interop is impossible. > **Explanation:** Transducers are useful for reusable and efficient transformations, but simple pipelines should come first. ### True or False: `loop/recur` is a good fit for retry loops and local state machines. - [x] True - [ ] False > **Explanation:** `loop/recur` handles explicit local state transitions in a stack-safe way.
Revised on Saturday, May 23, 2026