Browse Clojure Foundations for Java Developers

Defining and Testing Functions at the REPL

Use the REPL to shape functions with real inputs, then move the proven code back into source files and tests.

The REPL is one of the best places to shape a function before you commit to its final form. But there is a right way and a wrong way to do that.

The right way:

  • try a small function against realistic data
  • adjust it quickly
  • confirm the behavior
  • move the lasting version into a source file
  • keep formal tests for the behavior that matters

The wrong way:

  • keep redefining random helpers for half an hour
  • never save them anywhere
  • assume the live REPL state is now “the application”

This page is about the first workflow.

Start With Realistic Inputs

A Java developer often begins by defining the method signature first. At the REPL, it is usually better to begin with the shape of the data and the behavior you want.

For example, say you want to normalize a user record:

1(require '[clojure.string :as str])
2
3(def sample-user
4  {:user/email " DEV@EXAMPLE.COM "
5   :user/roles ["admin" "admin" "ops"]
6   :user/active? true})

Now write the function against that shape:

1(defn normalize-user [user]
2  (-> user
3      (update :user/email str/trim)
4      (update :user/email str/lower-case)
5      (update :user/roles set)))
6
7(normalize-user sample-user)
8;; => {:user/email "dev@example.com", :user/roles #{"admin" "ops"}, :user/active? true}

This is much more useful than writing an abstract function first and only later discovering what the input really looks like.

Let The REPL Answer Small Design Questions

The REPL is excellent for questions like:

  • Should this function return nil, a map, or a keyword status?
  • Do I want map, keep, or reduce here?
  • Is destructuring making this clearer or harder to read?
  • What does this transformation actually return?

It is less good for questions like:

  • What should the whole architecture be?
  • Which module boundaries are correct for the next year of this project?

That is the same lesson the official REPL-aided development guide emphasizes: the REPL is great for local iteration, but local iteration is not a substitute for higher-level design thinking.

Redefinition Is Normal

At the REPL, redefining a function is not a smell. It is one of the main points.

Start simple:

1(defn eligible-for-reminder? [subscription]
2  (<= (:days-until-expiry subscription) 7))

Try it:

1(eligible-for-reminder? {:days-until-expiry 10})
2;; => false

Then refine when the rules become clearer:

1(defn eligible-for-reminder? [subscription]
2  (and (not (:cancelled? subscription))
3       (<= 0 (:days-until-expiry subscription))
4       (<= (:days-until-expiry subscription) 7)))

Redefinition in a live session is a feature, not a hack. The discipline is making sure the final version gets written down where the team can find it later.

Use Assertions For Quick Checks, Not As A Test Strategy

The REPL is a fine place for quick sanity checks:

1(assert
2 (= {:user/email "dev@example.com"
3     :user/roles #{"admin" "ops"}
4     :user/active? true}
5    (normalize-user sample-user)))

That is useful while shaping the function, but it does not replace proper tests in your test suite.

Good rule:

  • use quick assertions in the REPL to validate your current thinking
  • use actual test namespaces to preserve important behavior for the future

The REPL helps you discover the test cases you should keep.

Prefer Plain Data To Mock Mazes

One of the best parts of working with functions in the REPL is that Clojure code often accepts plain data directly. That means you can test many ideas with simple maps and vectors instead of elaborate object graphs or mocking frameworks.

For Java developers, this can feel like a relief:

 1(def line-items
 2  [{:sku "A" :qty 2 :price-cents 500}
 3   {:sku "B" :qty 1 :price-cents 1200}])
 4
 5(defn total-cents [items]
 6  (->> items
 7       (map (fn [{:keys [qty price-cents]}]
 8              (* qty price-cents)))
 9       (reduce + 0)))
10
11(total-cents line-items)
12;; => 2200

The input is right in front of you. That makes both the function and the result easier to inspect.

Use (comment ...) Blocks In Source Files

Once a function has become useful, move it into a namespace and keep nearby REPL examples in a (comment ...) block:

 1(ns my.app.billing)
 2
 3(defn total-cents [items]
 4  (->> items
 5       (map (fn [{:keys [qty price-cents]}]
 6              (* qty price-cents)))
 7       (reduce + 0)))
 8
 9(comment
10  (total-cents [{:qty 2 :price-cents 500}
11                {:qty 1 :price-cents 1200}])
12  ;; => 2200
13  )

This is one of the best ways to preserve what you learned in the REPL without losing the interactive workflow. The example stays close to the code, and future you can evaluate it again.

Test The Edge Cases While The Function Is Small

The REPL is especially good at probing edge cases before the function gets buried in a larger system:

1(total-cents [])
2;; => 0
3
4(normalize-user {:user/email "A@B.COM" :user/roles [] :user/active? false})
5;; => {:user/email "a@b.com", :user/roles #{}, :user/active? false}

These quick checks often reveal the tests that should later become permanent.

A Better Workflow Than “Write Then Pray”

The healthy loop looks like this:

  1. build a realistic input value
  2. define a small function
  3. call it with a few representative cases
  4. refine the function
  5. move the lasting version into source
  6. keep the important behaviors in tests

That workflow is one of the main reasons Clojure development feels so interactive once it clicks.

Common Mistakes

  • using toy inputs that hide the real data shape
  • piling up REPL-only definitions without moving them into source
  • treating a few REPL assertions as a substitute for tests
  • using the REPL only after the function is already “finished”

The REPL is strongest early, while the function is still being discovered.

Knowledge Check: Using The REPL To Shape Functions

### What is the best starting point when shaping a function at the REPL? - [x] Realistic sample data and a small behavior to verify - [ ] A full application framework - [ ] An inheritance hierarchy - [ ] An empty class with placeholder methods > **Explanation:** The REPL works best when you can exercise a function against realistic values immediately. ### Why is redefining a function at the REPL normal? - [x] Because iterative refinement in a live session is one of the main benefits of the REPL - [ ] Because Clojure functions are supposed to stay only in the REPL - [ ] Because source files cannot hold function definitions - [ ] Because tests are unnecessary in Clojure > **Explanation:** Redefinition is part of the exploratory workflow. The important discipline is moving the lasting result back into source code. ### What is a good use of `assert` in the REPL? - [x] Quick sanity checks while exploring behavior - [ ] Permanent replacement for a test suite - [ ] Loading third-party libraries - [ ] Replacing namespace declarations > **Explanation:** REPL assertions are good for fast feedback, but durable behavioral checks should still live in normal test files. ### Why are plain maps and vectors valuable in REPL function work? - [x] They make inputs visible and easy to manipulate without heavy scaffolding. - [ ] They eliminate the need for namespaces. - [ ] They automatically generate optimized bytecode. - [ ] They force all functions to be recursive. > **Explanation:** Plain data makes function inputs cheap to build and easy to inspect, which is ideal for interactive exploration. ### What is a strong way to preserve useful REPL experiments in source files? - [x] Keep runnable examples in `(comment ...)` blocks near the functions they exercise. - [ ] Leave them only in shell history. - [ ] Store them only in memory in the current REPL session. - [ ] Convert every REPL experiment into a macro. > **Explanation:** `(comment ...)` blocks are a practical way to keep exploratory examples close to the code without making them part of normal execution.
Revised on Friday, April 24, 2026