Learn when it is worth writing your own function-taking functions in Clojure, how to design them cleanly, and how to avoid thin wrappers that add no value.
Built-in higher-order functions such as map, filter, and reduce get you a long way.
But eventually you will want to capture a domain-specific pattern, not just a raw collection operation. That is when writing your own function-taking function starts to make sense.
The important question is not “Can I accept a function here?”
The important question is:
does passing a function make this API clearer by isolating the part that actually varies?
Suppose you often need to transform only the orders that match a rule, while leaving the others unchanged.
1(defn transform-matching [pred xf coll]
2 (mapv (fn [item]
3 (if (pred item)
4 (xf item)
5 item))
6 coll))
Now you can reuse the same pattern with different rules and transformations:
1(defn expedited? [order]
2 (:order/expedited? order))
3
4(defn mark-priority [order]
5 (assoc order :order/priority :high))
6
7(transform-matching expedited? mark-priority orders)
This abstraction earns its keep because it names a real pattern:
This, by contrast, is usually not worth introducing:
1(defn apply-to-all [f coll]
2 (map f coll))
It adds no domain meaning and teaches the reader nothing that map did not already say clearly.
That distinction matters. A custom higher-order function should capture a reusable pattern, not just rename a core function.
| Rule | Why it matters |
|---|---|
| Name the varying behavior clearly | pred, xf, score-fn, and key-fn communicate the contract quickly |
| Put collection or main data arguments last when practical | It works better with threading macros and partial application |
| Keep the passed-in functions pure when possible | The abstraction stays predictable and easier to test |
| Document the callback shape through examples | Readers need to know what the function will receive and what it should return |
| Avoid wrapping core functions without adding meaning | Indirection only helps when it captures a real pattern |
Many business calculations share the same traversal shape but differ in what they measure.
1(defn sum-by [metric-fn coll]
2 (reduce (fn [total item]
3 (+ total (metric-fn item)))
4 0M
5 coll))
You can now use the same structure with different metrics:
1(sum-by :order/amount orders)
2
3(sum-by (fn [{:order/keys [discount]}]
4 discount)
5 orders)
This is a good higher-order function because the reusable idea is clear: “sum a derived numeric value across the collection.”
When people are new to Clojure, they often recreate object-oriented extension patterns by building too many tiny wrappers.
Common overcorrections:
mapThat leads to the functional equivalent of Java over-engineering.
The better Clojure habit is:
Before writing a custom function-taking function, ask:
| Question | If the answer is yes |
|---|---|
| Does it capture a real repeated pattern? | It may deserve its own function |
| Does the callback name make the contract clear? | The API will be easier to read |
Would plain map, filter, or reduce already be clearer? |
Do not add the wrapper |
| Can the callback stay pure? | Testing and reuse will be simpler |
The call site should tell the story.
Good:
1(transform-matching overdue? escalate orders)
2(sum-by :invoice/amount invoices)
Weak:
1(apply-to-all transform orders)
2(do-stuff thing orders)
The strong version makes the domain intent visible. The weak version only hides a built-in behind a vague name.