The REPL is Not Enough
By Alys Brooks
The usual pitch for Clojure typically has a couple ingredients, and a key one is the REPL. Unfortunately, it’s not always clear on what ‘REPL’ means. Sometimes the would-be Clojure advocate breaks down what the letters mean—R for read, E for eval, P for print, and L for loop—and how the different stages connect to the other Lispy traits of Clojure, at least. However, even a thorough understanding of the mechanics of the REPL fails to capture what we’re usually getting at: The promise of interactive development.
To explore why this is great, let’s build it up from (mostly) older and more familiar languages. If you’re new to Clojure, this eases you in. If Clojure’s one of your first languages, hopefully it gives you some appreciation of where it came from.
Casino REPL: Interactive code execution
The first level is being able to interactively enter code and run it at all. You might be surprised to learn that (technically) Java has a REPL. Similar tools exist for other static languages, like C and C#.
These can be handy for figuring out an obscure or forgotten bit of syntax or playing around with an API.
These REPLs are typically afterthoughts and have limitations on what you can
define. In jshell
, for example, you can define static methods but no classes.
License to Eval: Full Access to the Language
The next step is basically no-compromises eval/execute—all the constructs of the
language are available. Most dynamic languages offer this, as do static
functional languages like Haskell and OCaml. Shells, including bash
or
PowerShell, also have this level of capability.
These languages are generally high-level and may even resemble the pseudocode you wrote or referenced, so using these REPLs to try out ideas and do some quick testing can be a fluent experience. After all, shells were the main interface to computers for several decades, and have remained in the toolbox of power users, system administrators, developers, and quartermasters since.
Still, you run into some disadvantages:
- These REPLs start in a blank slate, but most development is in the context of an existing program, often a very large one. You have to use imports to bring in the relevant code, and it may take quite a bit of typing.
- What you write in the REPL is often ephemeral. Ephemeral in the quality sense
is actually okay—writing one (or two, or three) versions in the REPL to
throw away isn’t bad. But it’s also ephemeral in a more literal sense. Once
the REPL ends, your code is either lost or not in a convenient format.
Recent history is typically just a few up arrow presses away, but to find
earlier code you have to either search or wade through typos, uses of
doc
and other inspection, and design dead ends.
From Devtools With Love: Adding Context
Going beyond a sequential experience of entering code and seeing the result takes us another step toward understanding what our code is doing. Actually, it’s really two steps:
- Going beyond text to include graphs, images, animations, and widgets.
- Showing the current state at all times.
Web developers and data scientists have taken the lead here. Every major desktop browser has a suite of tools for inspecting not only the JavaScript code but also the interface (the DOM) it generates or alters. Similarly, RStudio and Spyder are data science-oriented IDEs that keep a running process and allow you to see the values of any currently defined variables.
Some supercharged REPLs and REPL sidekicks exist for Clojure:
- Dirac tweaks Chrome’s DevTools to accomodate ClojureScript
- Reveal adds the ability to explore and visualize data structures returned by the forms you’re evaluating.
- Portal, inspired by Reveal, similarly lets you explore a variety of data types.
Along similar lines, re-frame applications can use re-frame10x, which allows for stepping back and forward to see the state of application.
Notebooks are another way of moving past the textual paradigm. Notebooks let you have inline diagrams, images, graphs, and even widgets. They also allow you to embed explanatory text and diagrams—the promised literate programming all the way from the 1970s. Some notebooks add a variable inspector and debugger, blurring the line between IDE and notebook. Clerk brings these to Clojure.
The Clojurian With the Golden Form: Out of the Textual REPL
Clojure’s base REPL already has the strengths of the dynamic, expressive
languages mentioned in License to Eval
(especially if you add tools and quality-of-life libraries from the previous section). However, many
Clojure developers find they are most productive if they can evaluate code as
they edit source code.
These are often done through advanced REPLs like nREPL, pREPL, and their alternatives. (Rich Hickey has argued “REPL” is a misnomer at least in nREPL’s case.)
Fully realized, Clojure forms and values become lingua franca allowing you to control, inspect, and redefine a variety of systems, as you send code from your editor, terminal, or notebook to a REPL, a local instance of your program, a browser, a node backend, or even a production instance. Unfortunately, most of these require some setup. In particular, getting a ClojureScript REPL is a multistage process, much like modern rockets, and prone to failure, much like early rockets.
These advantages transcend the basic command-line evaluation that “REPL” often suggests, so listing the REPL among Clojure’s advantages actually undersells the feature if you don’t explain what it can actually do.
Sessions are Forever: Common Lisp
The Clojure interactive development is not at the apex. Common Lisp went even further by persisting state between sessions and letting you examine the state.
Perhaps the most noticeable is that these save where you left off. This makes it
easier to build up your program over time, at the cost of some state ambiguity.
If you wrote a function process-input
, renamed it to the
canonicalize-user-commands
, fixed a bug, and refactored it, process-input
would still be
hanging around, with subtle differences.
Arguably working from an editor is a better fit for making changes to
long-running systems or collaborating with other programmers, but being able to persist sessions would be nice for smaller
programs or experimentation.
In addition to Common Lisp, Smalltalk, and R also remember where you left off.
Common Lisp has another super power: conditions and restarts. When your program fails, it’s paused at the moment everything went wrong, allowing you to try to recover, explore what went wrong, or even redefine things and resume execution like nothing happened.
In Clojure and most other languages, an error, exception, or panic basically shuts everything down. You can see the stack at the moment of failure, but you can’t interact with it. Rich Hickey was inspired by Common Lisp and the lack of the condition system is not because he thought they weren’t valuable. As he explains in A History of Clojure,
I experimented a bit with emulating Common Lisp’s conditions system, but I was swimming upstream.
Some Clojurians have decided to try swimming upstream, but since these libraries aren’t widely used, you’ll have to think carefully about how they’ll interact with libraries that rely on Clojure’s native exceptions.
Conclusion
Common Lisp being the endpoint of our journey puts the lie to my blog post’s structure. Like most stories of only-increasing progress, this one isn’t completely true. Interactive development hasn’t simply gotten better and better over time. We’ve lost ground in some areas even as we’ve gained ground in others.
Still, we’re in a good place with Clojure. As the recent introduction of Clerk demonstrates, there’s still interest in improving the interactive development experience in Clojure.
Appendix: All the James Bond-Clojure Puns I Could, Regrettably, Not Fit in this Post
- Dr. Nil
- Dyn Another Day
- Live and
let
Die - From nREPL with Love
- The
spy
Who Loved Me - You Only
defonce
- MoonREPL