Bad nREPL: 10 Things You Hate About nREPL
New is always better.
– Barry Stinson, Senior Clojure developer
Over the years I’ve heard countless complaints about nREPL and its numerous perceived flaws. I dawned me today that it might be a good idea to write this “living”1 article that will try to catalog and address some of them.
I’ll focus mostly on the common complaints in the Clojure community, but I’ll also touch upon some of the broader perceived issues with the nREPL protocol itself.
Bencode sucks
A lot has been said about the choice of Bencode for encoding data between the client and the server. The format was very primitive they said. JSON is better! EDN is better! X(ML) is better!
Of course, back when this decision was made EDN didn’t even exist and for most editors dealing effectively with JSON was a very tall order. For clients like vim and Emacs it was much easier to write a simple bencode implementation (typically a few hundred lines of code) than to have to create a fast (and correct) JSON parser in VimScript or Emacs Lisp. Admittedly, I struggled for a while to get even the CIDER bencode parser working properly!
You also can’t expect any language-agnostic protocol to adopt a niche format like EDN and expect any form of wide adoption. Sure, it’d be a great fit for Clojure, but it would impose a heavy burden on the clients if they don’t have access to Java or JavaScript libraries.
Bencode has many limitations, but it was the widest common denominator and in my opinion its choice contributed a lot to the early success of nREPL.
I find it somewhat funny that even though the Clojure nREPL implementation eventually provided JSON (Transit) (see https://github.com/nrepl/fastlane) and EDN (built-in) transports, practically no one uses them.2
Note: Fun trivia - the very first version of nREPL didn’t use Bencode! You can find the reasons for the adoption of Bencode here.
It’s not a real REPL (a.k.a. Rich doesn’t like it)
REPL stands for something - Read, Eval, Print, Loop.
It does not stand for - Eval RPC Server/Window.
It is not something I “feel” about. But I do think we should be precise about what we call REPLs.
While framing and RPC orientation might make things easier for someone who just wants to implement an eval window, it makes the resulting service strictly less powerful than a REPL.
– Rich Hickey (https://groups.google.com/forum/?pli=1#!msg/clojure-dev/Dl3Stw5iRVA/D_Kcb-naOx4J)
The above excerpt is from a 2015 conversation around the introduction of Clojure’s socket REPL server and what (nREPL) problems does it solve. I’ve seen this conversation cited many times over the years to support the thesis that nREPL is problematic in one regard or another. So, is nREPL a real REPL or not? Does this even matter if it does what you need it to do?
Well, nREPL was never meant to be a “real REPL”. It was meant to be a REPL
server that makes it easy for people to build development tools (e.g. Clojure
editor plugins) upon. And framing requests and responses makes a huge difference
there. Sure, it might look appealing to be relying only on eval
for
everything, but you still need to be able to process whatever you got from
eval
. It’s also pretty handy to be able to match requests and responses that
originated from them.
I guess nREPL’s approach was eventually “redeemed” when the socket REPL didn’t gain broad
tool adoption, and was followed up by prepl
that framed the input. Still, it
didn’t gain broad adoption for development tooling as was hard to deal with its
unstructured output.
Now things have come full circle with a recently introduced Clojure REPL (framed request, framed response).
To wrap it up here - we can either be dogmatic or practical. I always go for being practical - it might not be a real REPL, but it sure gets real work done.
Note: You can find a short comparison of nREPL, the socket REPL and prepl here.
It’s a 3rd-party application
Can’t argue with that one. That obviously puts nREPL at an disadvantage when it comes to Clojure’s built-in socket REPLs or REPLs that get “injected” by clients (e.g. the now defunct unrepl
).
On the bright side - this also untangles nREPL from the Clojure’s release cycle. And makes contributing to the project a lot easier.
By the way, I always felt this particular concern to be quite overblown as nREPL is small self-contained library with no external dependencies. Also it’s easy to have it only in development profile (where it’s typically used). Technically it’s also possible to “upgrade” any socket REPL to an nREPL, but we never finished the work in this direction. (see https://github.com/nrepl/nrepl/commits/injection) I guess there wasn’t enough demand for this.
One more thing - tools like CIDER and Calva have been injecting nREPL during development automatically for quite a while, which blurs quite a bit the line between built-in and third-party. The point I’m trying to make here is that there’s a fine line between potential issues and real issues. I prefer to focus on the latter group.
Middleware suck (insert here every possible reason why)
Maybe they do. Maybe they don’t. Obviously they are concept that predates nREPL and people have been divided about it pretty much its entire existence.
I’m a bit biased, having worked with middleware for many years (long before I got interested in Clojure) and I’ve always liked the idea of being able to process requests and responses like a pipeline transforms them. But I also realize that comes with a certain learning curve and the ordering of the pipeline steps is not always clear, which can result in various problems.
I guess no middleware is more controversial than piggieback, the one responsible for ClojureScript support. Love it or hate it, there’s no denying that the decision to organize nREPL’s functionality around middleware made it possible to provide ClojureScript support (ClojureScript didn’t even exist at the time when nREPL was created) without having to touch the nREPL codebase. It also made possible some fairly interesting workflows like hosting a Clojure and a ClojureScript on the same nREPL server. And, of course, this also means that anyone can provide a replacement for piggieback if they wish to.
At the end of the day, however, one should not forget that middleware is just an implementation detail in the reference Clojure nREPL server and you can implement the nREPL protocol without middleware. Babashka’s nREPL server is a nice example of such an approach.
I was hoping that by now there would also be more native ClojureScript nREPL servers, but alas. Maybe this year?
It’s Clojure-only (or optimized for Clojure)
No, it’s not. Just take a look at the nREPL protocol ops (sans session management ops):
completions
eval
interrupt
load-file
lookup
stdin
Which one of them seems Clojure-specific to you?
Yeah, the protocol design was obviously influenced by Clojure and we never gave much thought on how to standardize nREPL certain responses, but nREPL was always meant to be a language-agnostic protocol.
Admittedly it didn’t gain much traction outside of the Clojure & broader Lisp community, that’s a different matter altogether.
It’s bloated and has too many (useless) features
Some people feel that over the course of recent years nREPL has become too bloated with the additions of features like:
- Support for Unix sockets (as an alternative of using TCP)
- Support for EDN (instead of Bencode)
- Dynamic middleware loading/unloading
- TLS encryption of the TCP connections
- The addition of completion and lookup middleware
I get their point. nREPL is definitely bigger and more complex than it used to be when I took over project 5 years ago. I’ve been quite selective with the approval of new features and my general approach has been to say “No, thanks.”, unless there’s a lot of demand or a great justification for something. I’ve also avoided anything that would introduce external dependencies for the project, so it’s still 100% self-contained.
Yeah, obviously all of those feature additions are not really “must haves” (but what is really?) and some of them could have been distributed as external middleware packages. Still, I felt that a good out-of-the-box experience was preferable over a minimal core in the context of a tool like nREPL. Time will tell whether I was right or wrong. You can also share your take on this in the comments.
Note: Most of those newer features are marked as “experimental” and might be removed down the road if we notice issues with them or they don’t gain traction.
One more thing - it has been pointed out that because of the growing nREPL
codebase it loads slower than it used to (simply because you have to load more
Clojure code). While I acknowledge the problem, I think that the impact
(something like an extra 500ms
on application startup from what I saw
somewhere) is not that big given most Clojure workflows don’t involve restarting
applications all the time. I often work with the same instance of a Clojure
application for days.
It doesn’t have enough features
For everyone who thinks nREPL is bloated there’s also someone who feels it doesn’t have enough features. People would love to see things like “find references” support, more built-in transports, built-in support for ClojureScript, debugging, rich data types, etc.
That’s one of the realities of life - no matter what you do you can’t please everyone. But I keep trying regardless of the odds being stacked against me!
The protocol is poorly documented (lack of formal specification)
Well, that’s more or less true as of today, but I plan to address this eventually. In particular the structure of responses is mostly
undocumented, which is forcing some people reverse engineer it from existing nREPL implementations. That response structure also needs
a bit of consideration as ops like lookup
have responses that are somewhat linked to common Clojure var metadata.
If you’ve always wanted to contribute to open-source projects - that’s a great way to get started.
There’s no standard protocol compatibility test suite
Basically, we need a standard test suite that checks whether nREPL implementation conform to the nREPL protocol specification. This would make it much easier to build nREPL servers and to verify their proper work.
That’d be certainly nice to have and I think it’s very doable. Perhaps one of you, dear readers, is looking for a fun small OSS project?
Clojure nREPL Server vs nREPL Protocol
I came across the following statement today:3
First of all, nREPL is mainly designed for Clojure, tested against Clojure, and implements features that Clojure needs in a pretty specific Clojure way.
That’s certainly not how I see things. I believe Chas Emerick wouldn’t agree with the assessment either.
Many misconceptions about nREPL originate from a conflation of the nREPL protocol and the reference Clojure implementation. When I took over nREPL I expanded the documentation greatly and made the protocol more prominent here and there. Still, a lot of damage has already been done and we’ll need to do more to separate those clearly in the minds of most people.
I see this quite linked to documenting the protocol clearly and building a standard nREPL protocol compatibility test suite.
Epilogue
No software is perfect and neither is nREPL. It grew organically over the course of many years and it definitely accumulated a bit of weirdness and cruft along the way. And a lot of charm, plus a track record of stability and robustness.
Would we have done everything the same way knowing what we know today? Probably not. But nREPL still gets the job done and it’s still one of the better options for tool authors who don’t want to reinvent the wheel every time.
I get that it’s always fun to complain about something4 and to work on exciting new alternatives5, but I think in the spirit of Clojure it’s also not a bad idea to appreciate the value of stability and reliability. And keep improving a good thing.
Next time you’re very frustrated with nREPL consider trying to find a solution for your problem that everyone in the community might benefit from. New is always better until it’s not. In the (n)REPL we trust!
P.S. If I forgot to mention something you hate about nREPL, please share it in the comments.
P.P.S. You can also check out the related Reddit discussion.
-
I’ll be updating it from time to time. ↩
-
Admittedly we never adjusted the protocol structure for them, meaning those transports don’t make use of something that can’t be represented with Bencode. ↩
-
See https://andreyorst.gitlab.io/posts/2023-03-25-implementing-a-protocol-based-fennel-repl-and-emacs-client/ ↩
-
I’m often guilty of this myself. ↩
-
And I have no doubt that some of the alternatives are better than nREPL in one way or another. ↩