Learn what it means for functions to be first-class in Clojure, why higher-order functions are central to the language, and how passing behavior directly changes API and pattern design.
One of the most important shifts in Clojure is that functions are ordinary values. Once that feels natural, many design patterns become smaller, many APIs become simpler, and a lot of behavior that would be hidden in classes or strategy objects elsewhere becomes direct and explicit.
First-class function: A function that can be stored, passed, returned, and composed like other values.
From that idea, higher-order functions follow naturally.
Higher-order function: A function that takes functions as arguments, returns functions, or both.
These are not advanced tricks in Clojure. They are daily tools.
Because functions are values, you can bind them, store them, and pass them around directly.
1(defn add-one [x]
2 (inc x))
3
4(def transform add-one)
5
6(transform 5)
7;; => 6
That seems small, but it changes how you model behavior. If behavior can be passed directly, many problems do not need extra interface layers.
Higher-order functions let you separate what varies from what stays the same.
1(defn apply-twice [f x]
2 (f (f x)))
3
4(apply-twice inc 5)
5;; => 7
Here the reusable structure is “apply something twice.” The varying part is the function itself.
That pattern shows up constantly in Clojure:
Many of the most important Clojure functions are higher-order:
mapfilterremovereducesort-bygroup-byupdate1(map inc [1 2 3])
2;; => (2 3 4)
3
4(filter even? [1 2 3 4])
5;; => (2 4)
6
7(reduce + [1 2 3 4])
8;; => 10
These functions make behavior explicit. Instead of writing loops full of hidden control flow, you pass the relevant transformation or predicate directly.
You can also build functions dynamically.
1(defn make-adder [n]
2 (fn [x]
3 (+ x n)))
4
5(def add-five (make-adder 5))
6
7(add-five 10)
8;; => 15
This is useful when you want to create specialized behavior from configuration or local context without reaching for global state.
Once functions are first-class, many architecture choices get simpler:
That does not remove the need for protocols or multimethods, but it changes the order of choices. In Clojure, the first question is often: “Would a plain function be enough?”
Passing behavior around is useful when behavior genuinely varies. It becomes ceremony when the variation is imaginary.
This is a good fit:
1(defn retry [attempts f]
2 (loop [remaining attempts]
3 (try
4 (f)
5 (catch Exception e
6 (if (= remaining 1)
7 (throw e)
8 (recur (dec remaining)))))))
Here the helper owns the retry policy, and the caller supplies the effectful operation.
This is a weaker fit:
1(defn save-user [persist-fn user]
2 (persist-fn user))
If there is only ever one real persistence path, the abstraction is probably noise.
graph TD;
A["Data"] --> B["Higher-order function"]
C["Behavior function"] --> B
B --> D["New data or a new function"]
The diagram below captures the main point: Clojure often treats behavior as just another input to a reusable transformation or control structure.