Move from mutable loops to value-oriented collection pipelines with `map`, `filter`, `reduce`, and threading macros.
One of the first places immutability becomes practical rather than philosophical is collection work.
Java often teaches you to transform collections by:
Clojure pushes you toward a different model:
That usually produces code that is easier to read, test, and adapt at the REPL.
Suppose you need the subtotal of billable order items.
A common Java style might look like:
1BigDecimal subtotal = BigDecimal.ZERO;
2
3for (OrderItem item : items) {
4 if (item.isBillable()) {
5 BigDecimal lineTotal =
6 item.getUnitPrice().multiply(BigDecimal.valueOf(item.getQty()));
7 subtotal = subtotal.add(lineTotal);
8 }
9}
There is nothing wrong with this. But to understand it, you have to mentally simulate:
subtotalClojure usually makes those steps more explicit as a value pipeline.
1(defn line-total [{:keys [qty unit-price]}]
2 (* qty unit-price))
3
4(defn subtotal [items]
5 (->> items
6 (filter :billable?)
7 (map line-total)
8 (reduce + 0M)))
This version still does the same work, but the structure is easier to scan:
The original items collection is untouched throughout.
map, filter, And reduce Cover Most Early WorkThe three core collection operations show up constantly:
map: transform each elementfilter: keep only matching elementsreduce: collapse many values into oneExample:
1(def orders
2 [{:order/id 1001 :subtotal 120M :status :paid}
3 {:order/id 1002 :subtotal 35M :status :draft}
4 {:order/id 1003 :subtotal 80M :status :paid}])
5
6(->> orders
7 (filter #(= :paid (:status %)))
8 (map :subtotal)
9 (reduce + 0M))
10;; => 200M
This is a good mental pattern to internalize early. Many imperative loops turn into exactly this three-stage story.
The ->> threading macro is useful when the collection flows through the final argument position of each step.
Without it, the same code is still valid but less readable:
1(reduce + 0M
2 (map :subtotal
3 (filter #(= :paid (:status %)) orders)))
Both versions work. The threaded form is easier to read in the same order the data moves.
For Java engineers, ->> often feels like the missing piece that makes nested functional calls readable instead of inside-out.
Anonymous functions are fine for tiny steps, but once the transformation has real meaning, naming it makes the pipeline clearer:
1(defn paid? [order]
2 (= :paid (:status order)))
3
4(defn order-subtotal [order]
5 (:subtotal order))
6
7(->> orders
8 (filter paid?)
9 (map order-subtotal)
10 (reduce + 0M))
This is especially helpful during refactoring because you can test the smaller functions independently.
mapv When You Actually Want A VectorOne practical nuance matters here.
map returns a sequence, which is often exactly what you want in a pipeline. But if you specifically want a vector result, use mapv:
1(mapv :order/id orders)
2;; => [1001 1002 1003]
This is a useful habit when:
Not every transformation must eagerly realize data, but when you know the concrete result shape you want, say so directly.
Collection transformation is not limited to flat lists.
You will often transform nested application data:
1(defn add-line-total [item]
2 (assoc item :line-total (line-total item)))
3
4(defn price-order [order]
5 (update order :items #(mapv add-line-total %)))
Here, price-order returns a new order with priced items. It does not mutate the existing order or any shared list of items.
That style scales much better than passing around mutable objects whose nested contents change invisibly.
When you see imperative collection code, ask:
Those three questions often map directly to:
filtermapreduceEven if the final solution uses a slightly different combination, this is a strong first-pass way to translate Java loops into idiomatic Clojure.