Learn when a custom higher-order function is worth creating in Clojure, how to design the function contract, and how to avoid wrappers that only hide map, filter, or reduce.
A custom higher-order function should earn its place in the codebase.
The goal is not to prove that functions can take other functions. The goal is to capture a repeated pattern clearly enough that future readers understand the program faster.
For Java developers, this is the same discipline you already apply to classes and interfaces: introduce an abstraction when it names a real idea, not merely because the language makes it possible.
Create a custom higher-order function when all three are true:
If the function only renames map, filter, or reduce, it is probably not worth adding.
This function technically accepts a function, but it does not improve the code:
1(defn apply-to-each [f coll]
2 (map f coll))
Most readers would rather see:
1(map normalize-order orders)
The direct version is shorter and clearer. apply-to-each hides a familiar core function without adding domain meaning.
Now compare that with a function that captures a real business pattern.
Suppose several parts of your system need to validate entities and return structured failures instead of just true or false.
1(defn validate-with [rules entity]
2 (->> rules
3 (keep (fn [{:keys [id pred message]}]
4 (when-not (pred entity)
5 {:rule/id id
6 :rule/message message})))
7 (into [])))
The callers provide rule functions:
1(def order-rules
2 [{:id :has-customer
3 :pred :order/customer-id
4 :message "Order must have a customer"}
5 {:id :positive-total
6 :pred #(pos? (:order/total %))
7 :message "Order total must be positive"}])
8
9(validate-with order-rules {:order/total 0M})
10;; => [{:rule/id :has-customer, :rule/message "Order must have a customer"}
11;; {:rule/id :positive-total, :rule/message "Order total must be positive"}]
This higher-order function is worth having because it names a repeated workflow:
That is more than a wrapper around map.
Before writing the code, decide what the callback receives and returns.
| Question | Example answer |
|---|---|
| What does the callback receive? | One entity, one request, one row, or accumulator plus item |
| What must it return? | A transformed value, truthy/falsy decision, error map, or new accumulator |
| Is the callback expected to be pure? | Usually yes; document exceptions through naming |
| What shape does the higher-order function return? | Lazy sequence, vector, map, boolean, or function |
That contract is more important than the implementation trick.
Names like f are fine for tiny examples, but production functions often benefit from role names:
1(defn summarize-by [key-fn value-fn coll]
2 (reduce (fn [summary item]
3 (update summary (key-fn item) (fnil + 0M) (value-fn item)))
4 {}
5 coll))
The names tell you the contract:
key-fn decides the grouping keyvalue-fn decides the numeric value to addcoll is the data being summarizedUsage stays readable:
1(summarize-by :order/status :order/total orders)
Clojure code often puts the main collection last because it composes well with threading macros and partial application.
1(->> orders
2 (filter :order/paid?)
3 (summarize-by :order/status :order/total))
This is not an absolute law. It is a convention that often improves pipeline readability.
In Java, a similar abstraction might become:
Function, Predicate, or BiFunctionClojure usually starts with a function because the abstraction is about behavior, not object identity.
| If the Java design would introduce… | Check whether Clojure can start with… |
|---|---|
| Strategy object | Function argument |
| Validator interface | Predicate or rule map |
| Builder for a small behavior variant | Function returning a function |
| Mutable collector | reduce with an explicit accumulator |
That is the migration skill: choose the smallest abstraction that honestly names the behavior.
Before keeping a custom higher-order function, ask:
| Question | Keep it if… |
|---|---|
| Does the name explain a domain pattern? | The name teaches more than the implementation hides |
| Is the callback contract obvious? | Argument names and examples make it clear |
Would direct map, filter, or reduce be clearer? |
If yes, delete the wrapper |
| Does it keep callbacks pure where possible? | The behavior stays testable and composable |