Learn how Clojure APIs like map, filter, and reduce take behavior as an argument, and how to choose between named functions, anonymous functions, and keyword functions.
Passing a function as an argument is the move that makes Clojure’s collection programming feel different from imperative Java loops.
Instead of writing the control flow yourself, you give an existing function the behavior it needs.
That is what happens in all of these:
1(map inc [1 2 3])
2(filter even? [1 2 3 4])
3(reduce + [1 2 3 4])
The library already knows how to walk the data. You supply the transformation, predicate, or reduction step.
These are the most common higher-order collection functions you will reach for first:
| Function | The function argument means | Typical job |
|---|---|---|
map |
“How should each item change?” | Transform each value |
filter |
“Which items should stay?” | Keep matching items |
remove |
“Which items should go away?” | Drop matching items |
reduce |
“How do I combine the next item into the result?” | Aggregate to one value |
sort-by |
“Which derived key should determine order?” | Sort by a projection |
This is why higher-order functions feel natural in Clojure: the function argument names the one piece of behavior the library cannot guess for you.
Suppose you need the total amount of paid orders.
1(defn paid? [{:order/keys [status]}]
2 (= status :paid))
3
4(defn amount [{:order/keys [amount]}]
5 amount)
6
7(defn total-paid-amount [orders]
8 (->> orders
9 (filter paid?)
10 (map amount)
11 (reduce + 0M)))
There are three function arguments in that pipeline:
paid? tells filter which orders to keepamount tells map what to extract+ tells reduce how to combine numbersThis is what “pass behavior, not control flow” looks like in ordinary code.
Not every function argument should look the same.
| Form | Example | Good fit |
|---|---|---|
| Named function | paid? |
Reused logic or logic worth naming |
| Anonymous function literal | #(>= (:order/amount %) 1000M) |
Short one-off behavior |
| Keyword as function | :order/id |
Extract one key from each map |
| Existing core function | inc, +, count |
Clear built-in behavior |
Here is the same projection using a keyword function:
1(map :order/id orders)
That is idiomatic because keywords can act as lookup functions on maps.
Java developers often compare this to streams, and that comparison is good as long as you do not push it too far.
| Java streams | Clojure |
|---|---|
.map(x -> x.getAmount()) |
(map :order/amount orders) or (map amount orders) |
.filter(Order::isPaid) |
(filter paid? orders) |
.reduce(BigDecimal.ZERO, BigDecimal::add) |
(reduce + 0M amounts) |
The difference is not only syntax. Clojure treats these function arguments as the default shape of data processing, not as a special stream API.
Passing functions around does not erase function contracts.
For example:
map with one collection calls its function with one argumentmap with two collections calls its function with two argumentsreduce expects a reducing function that can combine an accumulated result with the next itemThat means this works:
1(map vector [:a :b :c] [1 2 3])
2;; => ([:a 1] [:b 2] [:c 3])
because vector accepts two arguments.
But if you pass a one-argument function where map will call it with two arguments, that is an arity error, not a style issue.
| Mistake | Why it happens | Better version |
|---|---|---|
| Passing a result instead of a function | Java habits make the call site look “one step too far” | (map normalize orders), not (map (normalize order) orders) |
Writing a large #(...) literal |
The short syntax looks convenient at first | Use fn or a named function when the logic has real weight |
| Reaching for mutation inside the callback | Imperative habits survive the pipeline rewrite | Keep the callback focused on returning a value |
| Forgetting laziness | map and filter often return lazy seqs |
Realize with into [], doall, or a consumer when needed |
If the function argument names a real business rule, naming it usually improves the code.
1(defn discount-eligible? [order]
2 (and (= :paid (:order/status order))
3 (>= (:order/amount order) 500M)))
4
5(filter discount-eligible? orders)
That is easier to review than embedding the entire rule inline every time it appears.
If the behavior is truly tiny and local, anonymous syntax is still fine:
1(filter #(>= (:order/amount %) 500M) orders)
The tradeoff is readability, not ideology.