Browse Clojure Foundations for Java Developers

Evaluating Expressions at the REPL

Learn what actually happens when the REPL evaluates a form, including result values, side effects, and required libraries.

Learning the REPL is really learning how Clojure evaluates forms in front of you.

That sounds obvious, but it is the source of much beginner confusion. A Java developer often sees:

1(println "Hello")

and assumes the visible terminal output is “the result.” In REPL work, you need to distinguish between:

  • the value your code returned
  • the side effects your code produced

That distinction makes Clojure code much easier to reason about.

The REPL Evaluates Forms

When you type this:

1(+ 2 3)

the REPL reads the whole form and evaluates it to a value:

15

This is the simplest possible REPL interaction:

  • input form
  • output value

The important step is that Clojure is evaluating a form, not a line in the shell sense. A multi-line form is still one evaluation unit if the parentheses remain open.

Start With Values, Not Just Arithmetic

Arithmetic examples are fine, but values are the real point:

1[1 2 3]
2;; => [1 2 3]
3
4{:user/id 42 :user/status :active}
5;; => {:user/id 42, :user/status :active}
6
7(map inc [1 2 3])
8;; => (2 3 4)

What matters is not only that these work. What matters is that the REPL lets you see the exact value shape immediately:

  • vector
  • map
  • lazy sequence

This is one reason Clojure developers spend so much time inspecting data directly.

Understand The Two Kinds Of Printing

One of the official REPL guides highlights a subtle but important case:

1(println "Hello World")

At the REPL you may see:

1Hello World
2nil

These two lines are not the same kind of thing.

  • Hello World is a side effect produced by your code.
  • nil is the value returned by println, printed by the REPL.

This distinction becomes critical when debugging:

  • if you only look at terminal output, you may miss the actual return value
  • if you only look at the returned value, you may miss meaningful effects

Java developers often blur these together because quick debugging so often happens through logging. The REPL rewards cleaner separation.

def Returns A Var, Not The Value Itself

Another early surprise:

1(def tax-rate 0.13)
2;; => #'user/tax-rate

Why did the REPL print #'user/tax-rate instead of 0.13?

Because def does not evaluate to the value you stored. It evaluates to the var it created or updated.

Then:

1tax-rate
2;; => 0.13

That is an important mental model:

  • def is a top-level naming operation
  • using the symbol later gives you the var’s current value

Function Definitions Evaluate Too

The same idea applies to defn:

1(defn factorial [n]
2  (if (zero? n)
3    1
4    (* n (factorial (dec n)))))
5;; => #'user/factorial
6
7(factorial 5)
8;; => 120

Again:

  • defining the function returns the var
  • calling the function returns the value of the computation

Once you start seeing top-level definitions and value-producing calls as different evaluation events, the REPL output becomes much less confusing.

Requiring Libraries Is Also Evaluation

The REPL is not limited to code you typed there. It can load namespaces and make them available in the current context.

1(require '[clojure.string :as str])
2;; => nil
3
4(str/upper-case "clojure")
5;; => "CLOJURE"

This is an important pattern:

  • require a library
  • call qualified vars from that namespace
  • inspect real outputs immediately

For Java developers, this is often the moment when the REPL starts to feel like a genuine working environment instead of a toy.

Aliases Change What Unqualified Code Means

After:

1(require '[clojure.set :as set])

you can write:

1(set/union #{1 2} #{2 3})
2;; => #{1 2 3}

But that alias only affects the current namespace. If you switch namespaces later, do not assume the alias came with you. This is one reason namespace awareness matters so much in REPL work.

The Result Vars Are Part Of Evaluation Workflow

The REPL keeps track of recent results:

  • *1
  • *2
  • *3
  • *e

Example:

1(reduce + [10 20 30])
2;; => 60
3
4(* *1 2)
5;; => 120

This is useful because the REPL is not only a place to run finished expressions. It is a place to take one result and probe it further.

A Good Evaluation Habit

When you enter a form, ask:

  1. What value should this produce?
  2. Does it also cause a side effect?
  3. If it defines something, what does the REPL show me about that definition?

Those three questions will keep you from misreading a lot of REPL output.

Practical Examples For Java Engineers

Suppose you are exploring order data:

 1(def order
 2  {:order/id 1001
 3   :order/items [{:sku "A" :qty 2 :price-cents 500}
 4                 {:sku "B" :qty 1 :price-cents 1200}]})
 5
 6(->> (:order/items order)
 7     (map (fn [{:keys [qty price-cents]}]
 8            (* qty price-cents)))
 9     (reduce +))
10;; => 2200

This REPL sequence lets you:

  • define sample data
  • transform it incrementally
  • confirm the exact return value

That is far faster than scaffolding a whole app run just to test a pricing rule.

Knowledge Check: Reading REPL Output Correctly

### What does the REPL fundamentally evaluate? - [x] Forms - [ ] Files only - [ ] JVM bytecode directly - [ ] Semicolon-terminated statements > **Explanation:** The REPL reads and evaluates Clojure forms. A form may span one line or several lines. ### In a REPL interaction with `(println "Hello World")`, what is `nil`? - [x] The value returned by `println`, printed by the REPL - [ ] A failed attempt to print - [ ] A signal that the REPL is broken - [ ] The same thing as the visible `Hello World` line > **Explanation:** `println` prints to standard output as a side effect and returns `nil`; the REPL then prints that return value. ### What does `(def tax-rate 0.13)` usually print in the REPL? - [x] A var such as `#'user/tax-rate` - [ ] `0.13` only - [ ] Nothing at all - [ ] A Java stack trace > **Explanation:** `def` evaluates to the var it created or updated, not directly to the bound value. ### Why is requiring a library at the REPL useful? - [x] It makes real namespace code available immediately for interactive experimentation. - [ ] It permanently installs the library into the JDK. - [ ] It replaces the need for project config. - [ ] It disables namespace resolution. > **Explanation:** Requiring a namespace lets you call existing library code directly in your live session and inspect its behavior. ### What are `*1`, `*2`, and `*3` for? - [x] They hold recent REPL results so you can continue exploring from them. - [ ] They are aliases for the current namespace. - [ ] They are temporary filenames created by the compiler. - [ ] They are only available in Leiningen. > **Explanation:** The REPL stores recent results in these special vars, which is useful for incremental exploration and debugging.
Revised on Friday, April 24, 2026