Use first-class and higher-order functions to pass behavior directly, replace small strategy interfaces, build reusable pipelines, and keep Clojure APIs compact.
In Clojure, functions are ordinary values. You can pass them as arguments, return them, store them in maps, and compose them into pipelines.
First-class function: A function that can be treated like any other value.
Higher-order function: A function that accepts another function, returns one, or both.
For Java engineers, the practical shift is that reusable behavior often becomes a function argument rather than an interface implementation.
1(defn count-matching [pred xs]
2 (count (filter pred xs)))
3
4(count-matching even? [1 2 3 4])
5;; => 2
6
7(count-matching #(> (:order/total-cents %) 5000)
8 [{:order/id 1 :order/total-cents 3000}
9 {:order/id 2 :order/total-cents 9000}])
10;; => 1
In Java, this might be a Predicate<T> or strategy interface. In Clojure, the function value is the strategy.
1(defn minimum-total [cents]
2 (fn [order]
3 (>= (:order/total-cents order) cents)))
4
5(filter (minimum-total 5000)
6 [{:order/id 1 :order/total-cents 3000}
7 {:order/id 2 :order/total-cents 9000}])
8;; => ({:order/id 2, :order/total-cents 9000})
The returned function remembers cents. That replaces a lot of small “configuration holder plus method” classes.
| Function | Role | Java comparison |
|---|---|---|
map |
Transform each element | Stream map |
filter |
Keep matching values | Stream filter |
remove |
Drop matching values | Negated predicate filter |
reduce |
Fold many values into one | Stream reduction or loop accumulator |
sort-by |
Sort using a key function | Comparator based on extracted key |
Example:
1(require '[clojure.string :as str])
2
3(defn normalize-email [email]
4 (-> email str/trim str/lower-case))
5
6(defn active-email-list [users]
7 (->> users
8 (filter :user/active?)
9 (map :user/email)
10 (map normalize-email)
11 (into [])))
The code describes data flow. Behavior is passed as named functions, keywords, or small anonymous functions.
1(def renderers
2 {:json (fn [rows] {:content-type "application/json" :body rows})
3 :count (fn [rows] {:content-type "text/plain" :body (str (count rows))})})
4
5((get renderers :count) [{:id 1} {:id 2}])
6;; => {:content-type "text/plain", :body "2"}
For small explicit dispatch tables, a map of functions can be clearer than a class hierarchy. Use it when the set of cases is visible and stable enough to review directly.
Function-heavy code can become unreadable if every rule is anonymous and nested. Keep these review rules:
| Smell | Correction |
|---|---|
| Large anonymous function | Give it a name |
| Several nested higher-order calls | Consider ->> or helper functions |
| Behavior hidden in a data map nobody can find | Move it near the call site or namespace it clearly |
| Clever abstraction with one use | Inline it until variation is real |
The goal is direct behavior, not abstraction density.