Browse Clojure Foundations for Java Developers

Definition of Pure Functions

Understand purity as deterministic behavior plus no observable side effects, and learn why Clojure treats it as a design goal rather than a hard rule.

A pure function is a function whose result is determined only by its arguments and that produces no observable side effects while computing that result.

For Java engineers, that definition matters because it changes how you structure logic. You stop asking, “which object owns this behavior?” and start asking, “what values does this calculation need, and what value should it return?”

The Two Practical Tests

In day-to-day Clojure work, a function is pure if both of these are true:

  • the same arguments always lead to the same result
  • evaluating the function does not change anything outside the call

That means no:

  • reading the current time
  • generating random numbers
  • logging or printing
  • updating an atom, ref, or database
  • reading mutable global process state

This pure function depends only on its inputs:

1(defn line-total [{:keys [qty unit-price]}]
2  (* qty unit-price))

Given the same item map, line-total will always return the same number.

This function is not pure:

1(import '(java.time Instant))
2
3(defn stamp-order [order]
4  (assoc order :processed-at (Instant/now)))

The same input order can produce different outputs at different times, so the function is input-independent only in appearance.

Purity Is About Behavior, Not About Syntax

Clojure does not have a pure keyword. A function becomes pure because of how it behaves.

That matters because two functions can look similar while having very different properties:

1(defn discounted-total [subtotal discount-rate]
2  (* subtotal (- 1 discount-rate)))
3
4(def tax-rate (atom 0.13M))
5
6(defn final-total [subtotal discount-rate]
7  (+ (discounted-total subtotal discount-rate)
8     (* subtotal @tax-rate)))

discounted-total is pure.

final-total is not, because it reads external state from tax-rate. If some other part of the system changes that atom, the same call can produce a different answer later.

That is the heart of the distinction:

  • pure logic receives what it needs explicitly
  • impure logic reaches outside itself

Referential Transparency Is The Useful Consequence

Pure functions are referentially transparent. In practical terms, that means you can replace a call with its result without changing the meaning of the program.

Example:

1(def item {:qty 2 :unit-price 15M})
2
3(+ (line-total item)
4   (line-total item))

Because line-total is pure, you can reason about this as:

1(+ 30M 30M)

That sounds small, but it is one of the big reasons functional code becomes easier to test, refactor, and parallelize. You are reasoning about values, not about hidden interactions.

Purity Does Not Mean “No Real Work”

New Clojure developers sometimes hear “pure function” and assume that only toy arithmetic counts.

That is too narrow.

A function can still do serious work and remain pure:

  • validate nested data
  • transform request maps into domain values
  • build SQL parameter maps
  • compute pricing rules
  • choose a state transition

Example:

1(defn order-subtotal [order]
2  (->> (:items order)
3       (map line-total)
4       (reduce + 0M)))

This is still pure. The function is doing meaningful application work, but it is doing it entirely from input data.

Clojure Encourages Purity Without Pretending Everything Is Pure

One of Clojure’s strengths is that it does not force an unrealistically pure world.

Programs still need:

  • I/O
  • clocks
  • randomness
  • caches
  • databases
  • network calls

Clojure’s philosophy is not “pretend these do not exist.”

It is closer to:

  • keep most of the logic pure
  • isolate unavoidable side effects
  • make effectful boundaries explicit

That is why the phrase pure core, impure shell is so useful for Java engineers moving into idiomatic Clojure.

A Helpful Java Comparison

Pure functions in Clojure are often closer to:

  • a static utility method that uses only parameters

than to:

  • an object method that reads fields
  • a service method that logs, mutates collaborators, and calls a remote API

That does not mean “Java cannot be functional.” It means Java makes purity a discipline, while Clojure makes purity a more natural default design move.

One Important Nuance About Exceptions

If a function throws the same validation error for the same bad input and still does not touch external state, many engineers treat that as functionally pure enough for design purposes.

For example:

1(defn non-empty-name [s]
2  (when (clojure.string/blank? s)
3    (throw (ex-info "Name must not be blank" {:value s})))
4  s)

The behavior is still entirely determined by the input. That is very different from a function that logs, sleeps, queries a service, or reads mutable state.

A Quick Purity Checklist

When reviewing a function, ask:

  1. If I call this twice with the same arguments, should I expect the same result?
  2. Does it read anything besides its arguments?
  3. Does it change anything besides returning a value?
  4. Could I test it with plain input and expected output, without mocks or runtime setup?

If the answers are:

  • yes
  • no
  • no
  • yes

then you are probably looking at a pure function.

Knowledge Check

### Which definition best matches a pure function? - [x] A function whose result depends only on its arguments and that causes no observable side effects - [ ] A function defined with `defn` - [ ] A function that returns a number - [ ] A function that never allocates new values > **Explanation:** Purity is about deterministic behavior and absence of observable effects, not about syntax or return type. ### Why is `(assoc order :processed-at (Instant/now))` impure? - [x] Because the output depends on the current time rather than only on the input order - [ ] Because `assoc` is a mutating function - [ ] Because maps cannot hold timestamps - [ ] Because Java interop is always impure > **Explanation:** `assoc` itself is fine. The impurity comes from reading the clock, which means the same input can produce different outputs. ### What does referential transparency let you do? - [x] Replace a function call with its result without changing the program's meaning - [ ] Skip testing completely - [ ] Mutate shared state safely - [ ] Avoid using namespaces > **Explanation:** Referential transparency is what makes pure code easier to reason about, because the call behaves like its resulting value. ### What is Clojure's practical stance toward purity? - [x] Keep most logic pure, but isolate real-world side effects instead of pretending they do not exist - [ ] Force every function to be mathematically pure - [ ] Ban I/O from all applications - [ ] Require classes for all effectful work > **Explanation:** Clojure is an impure language that strongly supports functional design. The goal is better program structure, not ideological purity.
Revised on Friday, April 24, 2026