Learn when `map` is the right tool in Clojure, how it differs from Java loop and stream habits, and how to use it clearly with single and multiple collections.
map is the function you reach for when every input item should produce a corresponding output item.
That “one in, one out” mental model is the simplest reliable way for a Java developer to recognize when map belongs in a Clojure pipeline.
map Meansmap applies a function to each item in a collection and returns a lazy sequence of the results.
1(map inc [1 2 3])
2;; => (2 3 4)
That is the whole contract:
If your operation keeps some items and drops others, that is filter, not map.
If your operation combines many items into one result, that is reduce, not map.
| If you need to… | Reach for |
|---|---|
| turn each order into its amount | map |
| keep only paid orders | filter |
| total all order amounts | reduce |
That distinction matters because many awkward Clojure pipelines come from using map where the code is really selecting or aggregating.
Suppose you have order maps and want a vector of invoice lines.
1(defn ->invoice-line [{:order/keys [id customer total]}]
2 {:invoice/order-id id
3 :invoice/customer customer
4 :invoice/total total})
5
6(defn invoice-lines [orders]
7 (->> orders
8 (map ->invoice-line)
9 (into [])))
The value of map here is not brevity. The value is that the transformation is explicit:
That clarity is why map is such a core tool.
map Versus Java HabitsJava developers usually meet this shape first through loops or streams.
| Java habit | Clojure shape |
|---|---|
for loop that builds a result list |
map plus a consumer such as into [] |
.stream().map(...) |
(map f coll) |
| getter-heavy projection | keyword functions or small named functions |
The important shift is that Clojure does not treat mapping as a special collection API bolted onto one data type. It is an ordinary sequence operation that works across seq-able data.
You often do not need a verbose anonymous function.
If the transformation is “extract one field”, a keyword function is usually the cleanest version:
1(map :order/id orders)
If the transformation names real domain behavior, a named function is often clearer:
1(defn normalize-email [customer]
2 (update customer :customer/email clojure.string/lower-case))
3
4(map normalize-email customers)
Use #(...) only when it genuinely keeps the code short and readable.
map With Multiple Collectionsmap can also walk several collections in parallel.
1(map vector [:customer/a :customer/b] [100M 250M])
2;; => ([:customer/a 100M] [:customer/b 250M])
This works because vector can accept one item from each collection.
That makes map useful for:
By default, map returns a lazy sequence.
That is usually a benefit:
But it also means you should be deliberate when callers need a concrete collection:
1(->> orders
2 (map :order/id)
3 (into []))
Using into [] here says, clearly, that the caller wants a vector result.
| Mistake | Why it is wrong | Better move |
|---|---|---|
Using map to remove items by returning nil |
The pipeline still has one output per input | Use filter, remove, or keep depending on the intent |
| Building a huge anonymous mapping function | The transformation is doing too much inline | Name the function or split the pipeline |
Forgetting that map is lazy |
Side effects or closed resources can behave unexpectedly | Realize the result or keep the mapping function pure |
Expecting map to aggregate |
map does not combine values into one result |
Use reduce for aggregation |
That first mistake is especially common. If you want to both transform and drop missing values, keep or a filter + map pipeline usually expresses the idea better than “map to nil and hope.”
One of the most common production shapes is:
1(->> orders
2 (filter :order/paid?)
3 (map :order/total)
4 (into []))
The roles stay clean:
filter decides which items survivemap decides how survivors changeThat separation is one of the reasons Clojure pipelines read well when each step does one job.