When I started the Chlorine project, I just thought it would be great if I could target all Clojure-like REPLs that already exist but didn’t have tooling support. At the time, this would include Lumo and Plank, mostly. Also, Shadow-CLJS and Figwheel have some “clunky run-some-code-and-transform-in-cljs” way of working that simply didn’t click with me.
Now, almost a year later, Chlorine supports Clojure, ClojureScript (Shadow-CLJS, Lumo, Plank, or even over clj
), ClojureCLR, Arcadia, Babashka, Clojerl (Clojure on Erlang) and Joker (Clojure on Go, also a linter). But the reality is that working with a pure Socket REPL is really hard – a socket REPL works exactly like a regular one, printing namespaces after each code, and so on. Also, there are some strange decisions on some REPLs, mostly likely ClojureScript (that is the second most used Clojure flavor), so things are not always easy. To put things in perspective, currently Chlorine uses 3 ways to evaluate code: It uses unrepl, that only works on Clojure, or uses internal APIs of shadow-cljs (that obviously only works for shadow-cljs), and for other implementations it uses a kind of a hack – it evaluates the code, inside a try
…catch
, and it returns a vector where the first element is a symbol in a specific format that Chlorine will understand and then link that with the response. This “hacky way” is currently being used for every other implementation except Clojure and Shadow-CLJS. Things work (autocomplete works too), but it is not pretty and sometimes have strange results.
As a matter of fact, I was already thinking about removing UNREPL (it’s really hard to implement new features on it, and some good ideas only work in theory – for example, the ability to evaluate long strings / collections and render only a part at a time aren’t that good with lots of edge-cases) and, to do it, I though about a better, non-hacky way to evaluate things on some Socket-REPLs (that, again, would only work on some REPLs – ClojureScript REPLs will probably never support “upgradable REPLs” because of the way they work) – the only thing that I had to understand is how to implement this “upgraded REPL”…
Then, recently, Babashka added an initial support for nREPL, with an insane low amount of lines. So, I’ve tried to implement a way to evaluate code over nREPL… and it was really simple to do it, using a npm library that already did it. But implementing like this meant that the user would need to know if the host/port to connect is a Socket REPL, or a nREPL (and the user does not know – lots of tools like lein
and shadow-cljs
show an nREPL port to be connected).
So, the alternative was the harder route: to re-implement nREPL by hand, in ClojureScript. This way, the flow would be:
- Chlorine connects to a Socket
- Chlorine sends a nREPL
clone
operation (to create a session) - If this command fails, it is not an nREPL – continue as normal
- It this command works, pick up this session and instantiate an nREPL evaluator.
This is all implemented in this feature change.
Now, what do we lose?
UNREPL adds support for rendering infinite sequences. It also adds support for rendering invalid keywords and symbols (things that you can get when you evaluate (keyword "one two")
and (symbol "one two")
), adds support for reflection of Java objects, and other things that would need to be implemented on nREPL.
Now, again, I was already thinking of a better way to implement these elements, so I don’t think we’ll loose too much…
And what do we win?
Speed!
nREPL is really faster, and is also a tested architecture with lots of good documentation, support for Clojure community, and so on. It is also not a clojure.contrib product, and this means that its development cycle is faster.
It also means that some nREPL implementations like on Scheme or HyLang works! Okay, probably not as good as regular Clojure implementations, but we need to be aware that I didn’t write a single line of code to support then!
I don’t think Chlorine will ever use the “advanced features” of nREPL like middlewares and so on… but I’m thinking of supporting some middlewares like “autocomplete” – the possibility of people implementing their versions on their REPLs (even with hacky / magic ways), so Chlorine would not need to understand every possible implementation detail that exists would be great.
Also, as nREPL is a really simple protocol, implementing it on Chlorine means that I can “upgrade” pure socket REPLs to be a “simple nREPL” without bencode or middlewares or anything, and the current code is already prepared to understand it.
So… it’s not an April Fools – support for nREPL is here to stay, but I’ll keep the simple Socket REPL support forever because this is the way Chlorine was born, and this is the way it’ll stay!