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:
That distinction makes Clojure code much easier to reason about.
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:
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.
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:
This is one reason Clojure developers spend so much time inspecting data directly.
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:
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 ItselfAnother 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 operationThe 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:
Once you start seeing top-level definitions and value-producing calls as different evaluation events, the REPL output becomes much less confusing.
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:
For Java developers, this is often the moment when the REPL starts to feel like a genuine working environment instead of a toy.
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 REPL keeps track of recent results:
*1*2*3*eExample:
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.
When you enter a form, ask:
Those three questions will keep you from misreading a lot of REPL output.
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:
That is far faster than scaffolding a whole app run just to test a pricing rule.