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.
| 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 |
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 AccumulationWhen 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 MachinesSome 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 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 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.
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.
| 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 |
reduce.->>, filter, map, and vec.loop/recur.map println example using doseq.reduce is the normal choice for one-result accumulation.loop/recur is the normal choice for explicit stack-safe local state.