nREPL 0.6 was released almost two months ago, but it never got the attention it deserved. At the time of the release I was enjoying some time off from development, and when I got back home I was swamped with work. Still, I had a created todo item to write a blog post detailing the important improvements in nREPL 0.6 and I’m finally doing this.

New Print Middleware

The most important user-visible change in this release is the replacement of the pr-values middleware with a new print middleware. pr-values, as it name implies, was the middleware that printed evaluation result values to strings, so they could be returned to the clients in an easy to consume manner.1 It was around since the early days of nREPL, and while it generally got the job done, it was also problematic as it had hardcoded the print function it was using and because it forced the value to be fully printed it could kill the server (by eating all its memory) or kill your client (most editors struggle with huge chunks of texts) if you happened to evaluate something huge by accident. We tried to fix the first problem in nREPL 0.5 by tweaking pr-values, but later Brandon Bloom pointed out some problems with the approach we had chosen at the time.

The new print middleware that succeeded pr-values solves all of those problems and more. This middleware is intended to provide generic printing functionality to other middlewares. By default its behaviour is entirely backwards-compatible with pr-values. Rather than being coupled to the specific key :value, the new middleware allows other ops to provide :nrepl.middleware.print/keys in responses.

Here’s a summary of the new middleware’s options are summarised below:

  • :nrepl.middleware.print/var – (namespaced) name of var to use as printing function
  • :nrepl.middleware.print/options – map of options to pass to printing function
  • :nrepl.middleware.print/stream? – whether or not to stream the result of printing each value over one or more messages
  • :nrepl.middleware.print/buffer-size – size of the buffer to use when streaming results; represents an upper bound on response size in bytes
  • :nrepl.middleware.print/quota – hard limit of number of bytes to print before truncating

Note: The options are namespaced for two reasons - to avoid conflicts with older middlewares and to highlight the fact that printing is Clojure-specific and not part of the language-agnostic nREPL protocol.

An example of non-streamed vs. streamed printing:

;; request
-> {:op :eval
    :code "(range 512)"}

;; responses
<- {:ns "user" :value "(0 1 2 3 ... 510 511)"}
<- {:status ["done"]}
-> {:op :eval
    :code "(range 512)"
    :nrepl.middleware.print/stream? 1}

<- {:value "(0 1 2 3 ... 282 2"}
<- {:value "83 284 ... 510 511)"}
<- {:ns "user"}
<- {:status ["done"]}

Clients have the choice of rendering each part of the result as they arrive, or using some buffering strategy (perhaps to prevent the output from being intermingled with that of *out* / *err*).

An example of truncation with streaming enabled:

-> {:op :eval
    :code "(range 512)"
    :nrepl.middleware.print/stream? 1
    :nrepl.middleware.print/quota 8}

<- {:value "(0 1 2 3"}
<- {:status ["nrepl.middleware.print/truncated"]}
<- {:ns "user"}
<- {:status ["done"]}

and without streaming enabled:

-> {:op :eval
    :code "(range 512)"
    :nrepl.middleware.print/quota 8}

<- {:ns "user"
    :value "(0 1 2 3"
    :status "nrepl.middleware.print/truncated"
    :nrepl.middleware.print/truncated-keys ["value"]}
<- {:status ["done"]}

The implementation details behind this are quite interesting, but I’ll omit them here. I’ll only summarize the highlights:

  • Big results start appearing much faster than before (provided you enable streaming).
  • You can interrupt the printing of a big result (provided you enable streaming).
  • You can set a hard limit for the size of a result.

More information about the new middleware is available in the user manual.

Special thanks to Michael Griffiths for tackling this and to Brandon Bloom, who pointed out the deficiencies in the approach we had taken in nREPL 0.5!

New Caught Middleware

nREPL 0.6 includes a caught middleware which provides a configurable hook for any java.lang.Throwable that should be conveyed interactively (generally by printing to \*err*). Like the print middleware, any options may be provided in either requests or responses (the former taking precedence over the latter if any options are specified in both). The following options are supported:

  • :nrepl.middleware.caught/caught: a fully-qualified symbol naming a var whose function to use to convey interactive errors. Must point to a function that takes a java.lang.Throwable as its sole argument. Defaults to clojure.main/repl-caught.

  • :nrepl.middleware.caught/print?: if logical true, the printed value of any interactive errors will be returned in the response (otherwise they will be elided). Delegates to nrepl.middleware.print to perform the printing. Defaults to false.

{:op :eval
 :code "(/ 1 0)"
 :nrepl.middleware.caught/caught 'my.custom/print-stacktrace
 :nrepl.middleware.caught/print? true}

Note: Just like that print middleware, the caught middleware is also Clojure-specific and not considered part of nREPL’s generic protocol.

The functionality of the caught middleware is reusable by other middlewares. If a middleware descriptor’s :requires set contains #'nrepl.middleware.caught/wrap-caught, then it can expect:

  • Any returned responses containing the key :nrepl.middleware.caught/throwable will have that key’s corresponding value passed to the hook.

  • Any handled requests will contain the key :nrepl.middleware.caught/caught-fn, whose value is a function that can be called on a java.lang.Throwable to convey errors interactively.

That middleware is another fine piece of work by the awesome Michael Griffiths.

Single evaluation thread per session

Previously nREPL’s evaluation mechanism was based on a homemade agents implementation, which worked well, but would change the evaluation thread used by the REPL unexpectedly. We’ve simplified the evaluation logic by simply associating a permanent evaluation thread to each nREPL session. The thread never changes now unless you interrupt an evaluation in progress, which kills the thread and spawns a new one. The new implementation is much simpler and its behaviour is completely predictable. See the discussion here for more details.

Technically this change is breaking, as it touched a few public vars, but in practice it seemed that cider-nrepl and Piggieback were the only projects making use of them, so no one really noticed any of this (as we had them updated by the time nREPL 0.6 hit Clojars).

You can get an idea about the nature of the change by taking a look at the relevant code from Piggieback:

(defn- enqueue [{:keys [id session transport] :as msg} func]
  (if-let [queue-eval (resolve 'nrepl.middleware.interruptible-eval/queue-eval)]
    ;; nrepl 0.4.x / 0.5.x
    ;; mostly a copy/paste from interruptible-eval
    (queue-eval session @@(resolve 'nrepl.middleware.interruptible-eval/default-executor)
                (fn []
                  (alter-meta! session assoc
                               :thread (Thread/currentThread)
                               :eval-msg msg)
                  (binding [ieval/*msg* msg]
                    (func)
                    (transport/send transport (response-for msg :status :done))
                    (alter-meta! session dissoc :thread :eval-msg))))
    ;; nrepl 0.6.x
    (let [{:keys [exec]} (meta session)]
      (exec id
            #(binding [ieval/*msg* msg]
               (func))
            #(transport/send transport (response-for msg :status :done))))))

Special thanks to Christophe Grand for working on this!

Stop Evaluation After the First Read Error

Check out this:

user=> (comment {:a} (println "BOOM!"))
Syntax error reading source at (REPL:1:14).
Map literal must contain an even number of forms
BOOM!
nil
Syntax error reading source at (REPL:1:33).
Unmatched delimiter: )

Or this:

user=> (defn foo [] (println {:a}) (println "BOOM!"))
Syntax error reading source at (REPL:8:27).
Map literal must contain an even number of forms
Syntax error reading source at (REPL:8:28).
Unmatched delimiter: )
BOOM!
nil
Syntax error reading source at (REPL:8:47).
Unmatched delimiter: )

That’s pretty weird, right?

Observe how because of a syntactic error (odd number of elements in a map) the println form gets executed, even though it shouldn’t (since it’s in comment and defn form respectively).

Besides this potentially dangerous but rare behavior, there is also a much more common inconvenience when you have to debug such errors. What should be the result of evaluating a single form triggers multiple exceptions, all but one of which don’t make any sense. They obscure the original problem and make life harder, especially to the beginners. The only thing to do is to remember that Unmatched delimiter: ) means you probably have an odd-arg map somewhere.

That was the behavior in the standard Clojure REPL and nREPL until now. Starting with nREPL 0.6 you’d get only:

user=> (comment {:a} (println "BOOM!"))
Syntax error reading source at (REPL:1:14).
Map literal must contain an even number of forms

Notice the lack of the “fake” result and the second read error.

All the credit here goes to Alex Yakushev who implemented this improvement!

Grab Bag

As with every release there were also other small bug fixes and improvements here and there. I think there are two worth sharing here:

  • nREPL has 0 runtime dependencies (again). I’ve reconsidered the decision to extract nrepl.bencode into a separate library and brought it back to nREPL itself. The separate library will continue to exist and will be kept in sync with the nrepl.bencode namespace in nREPL.
  • We’ve exposed a public CLI API in the nrepl.cmdline namespace. The API is experimental and subject to changes, but I think some people might find it useful.

Check out the release notes for a complete list of changes in nREPL 0.6.

nREPL’s user manual was fully updated to reflect the changes in the latest version.

Upgrade Notes

The upgrade from nREPL 0.4/0.5 to 0.6 should be seamless for most users. There are just a couple of things to keep in mind:

  • Piggieback users have to upgrade to version 0.4+ (due to the eval changes mentioned earlier).
  • Users of cider-nrepl have to upgrade to version 0.21+ (for the same reason).
  • Middleware that were doing something with pr-values should be updated to use the new print middleware (there were extremely few of those, so I doubt many of you would run into pr-values-related problems).

Team Updates

I’m really glad to share the news that Christophe Grand and Michael Griffiths joined nREPL’s team recently! Probably that doesn’t really surprise you at this point, since they carried out most of the work for nREPL 0.6.

Christophe is a legend in the Clojure community and needs to introduction. One of his more recent projects is unrepl, which aims to create a tooling-friendly REPL on top of the basic socket REPL introduced in Clojure 1.8. Unrepl features a ton of innovative ideas and I’m really happy that Christophe agreed to join our team and bring some of Unrepl’s signature features to the nREPL crowd as well. That form of collaboration between tool authors makes me extremely happy!

Michael, on the other hand, had been a key CIDER contributor for several years now. He’s behind much of CIDER’s ClojureScript support and also worked on some cool features like CIDER’s reloaded workflow and the pretty-printing functionality we had prior to nREPL 0.6. He was away from CIDER for a couple of years and his absence was strongly felt, as without him things really stagnated on the ClojureScript side. It’s really great to have him back!

They are some of the smartest people I’ve ever worked with and I’m humbled and proud to have them working on a better future for nREPL!

(next nREPL)

The plan for the next nREPL release is fairly simple:

  • Provide a native EDN transport, as an alternative to the default bencode transport.2
  • Provide an injection mechanism for clients, so they can supply nREPL with resources and classes directly. See this ticket for details.

I’d love for us to also make some headway with respect to being able to upgrade a plain socket REPL to nREPL and with porting unrepl’s rich data printer.

Once those features are completed I think we’d be ready for long-awaited 1.0 release. More importantly - nREPL will reach an amazing level of flexibility and would serve you even better!

Epilogue

By the time I got to write this post nREPL 0.6 is already widely available, as it quickly made it’s way to Leiningen 2.9. It’s also merged in Boot’s master, but there’s no stable Boot release shipping nREPL 0.6 yet. I’m also quite pleased to report that 2 months after the release we got 0 reported bugs/regressions from the previous version. Good work, everyone!

CIDER users can already take full advantage of all the improvements if they upgrade to CIDER 0.21 (New York). I hope the other clients will quickly follow in its steps (if they haven’t done so already).

nREPL 0.6 is a massive step forward for nREPL and I couldn’t be more proud of it. That’s certainly the most important release we’ve had since the project left Clojure Contrib. I’ve had very few direct contributions to nREPL 0.6 (other than acting as project coordinator) and I’d like to thank once again all the great people who contributed their time and knowledge! Extra special thanks to Christophe Grand and Michael Griffiths. You rock! We wouldn’t have made it so far without you! This release is truly a testament to the power of open-source that’s all about you!

That’s all from me for now. I hope this long overdue post was useful to you. Keep hacking!

  1. Not to mention that you can’t really map EDN to bencode, which means that the only way in which you can return an EDN value is in a printed form. 

  2. That’s already done. See this ticket for details.