Treat functions as values so you can build reusable, composable transformations over data.
In Clojure, functions are ordinary values. You can pass them around, store them, return them, and combine them. That sounds familiar if you already use Java lambdas, but the role is much bigger in Clojure. Here, functions are not just a convenience feature. They are one of the main building blocks of everyday design.
First-class function: a function that can be passed, returned, stored, and treated like any other value.
Higher-order function: a function that accepts another function, returns one, or both.
For a Java engineer, the real shift is this: in Clojure, reusable behavior is often expressed as a function argument instead of an interface implementation.
Consider a small reporting helper:
1(defn count-matching [pred xs]
2 (count (filter pred xs)))
3
4(count-matching even? [1 2 3 4]) ;; => 2
5(count-matching #(> (:total-cents %) 5000)
6 [{:total-cents 3000}
7 {:total-cents 9000}]) ;; => 1
The workflow stays the same. The behavior varies because pred varies.
In Java, you might reach for a strategy interface or a Predicate<T>. In Clojure, passing a function is the default move, not the advanced move.
Higher-order code gets more useful when you return a function that remembers some configuration:
1(defn minimum-total [cents]
2 (fn [order]
3 (>= (:total-cents order) cents)))
4
5(filter (minimum-total 5000)
6 [{:id 1 :total-cents 3000}
7 {:id 2 :total-cents 9000}])
8;; => ({:id 2 :total-cents 9000})
The returned function closes over cents. That is a common functional pattern:
Much of idiomatic Clojure comes from a small family of higher-order tools:
map transforms each elementfilter keeps elements that satisfy a predicateremove drops elements that satisfy a predicatereduce folds many values into onesort-by uses a function to decide orderingThese are not “utilities off to the side.” They are the language you use to describe work over collections.
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 :active?)
9 (map :email)
10 (map normalize-email)
11 distinct))
The code reads as a transformation pipeline, not as a conversation among objects.
Another important step for Java developers is becoming comfortable with dispatch through data:
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"}
This can replace a surprising amount of lightweight polymorphism. If the set of behaviors is small and explicit, a map of functions is often simpler than a hierarchy of types.
Once you internalize that functions are values, your API design shifts:
This is one reason Clojure code often feels smaller than equivalent Java code. The language gives you a cheap way to parameterize behavior without creating a new class every time.
Higher-order programming helps when it makes variation explicit. It hurts when it turns a simple rule into an unreadable nest of anonymous functions.
Good practice:
If a higher-order API makes the call site harder to understand than the duplicated code would have been, step back and simplify.