Clojure Tutorial: nREPL

By Xah Lee. Date:

nREPL is a Clojure library. The name “nREPL” means “network REPL”. It lets applications to connect to a Clojure engine via network (remotely or locally). For example, it can be used to let editor/IDE such as emacs or web browser based app to evaluate Clojure code.

nREPL home page: https://github.com/clojure/tools.nrepl

nREPL API documentation: http://clojure.github.io/tools.nrepl/


Here's a basic tutorial for writing app using nREPL.

This page is WORK IN PROGRESS.

nREPL includes both server and client. That is, you can use it to create/start a Clojure code evaluation server. It also includes a client. That is, you can use it to query a Clojure code evaluation server and get result.

using nREPL as Client

start a test nrepl server

first, lets use lein to start a nrepl server.

to start a nrepl server using lein, in terminal type

#start a nrepl server using lein
lein repl :start :port 55555

[see Clojure Leiningen Tutorial]

lein nrepl server 31366
starting a lein nrepl server.

Now, create a project lein new app xah-clojure-nrepl-client. In this project, we will write code to connect to a nrepl server and evaluate clojure code.

add nrepl lib in project.cjl

in the project file project.clj, add [org.clojure/tools.nrepl "0.2.5"] to your :dependencies vector.

so it looks something like this:

(defproject xah-clojure-nrepl-client "0.1.0-SNAPSHOT"
  :description "FIXME: write description"
  :url "http://example.com/FIXME"
  :license {:name "Eclipse Public License"
                  :url "http://www.eclipse.org/legal/epl-v10.html"}
  :dependencies [[org.clojure/clojure "1.6.0"]
                 [org.clojure/tools.nrepl "0.2.5"]]
  :main ^:skip-aot xah-clojure-nrepl-client.core
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all}})

write first nrepl client code

now, in src/xah_clojure_nrepl_client/core.clj , modify the file so it looks like this:

(ns xah-clojure-nrepl-client.core
    (:require [clojure.tools.nrepl :as nrepl] )
    (:gen-class))

(defn -main
  [& args]
  (with-open [tp (nrepl/connect :port 55555)]
             (pr
              (nrepl/response-values
               (nrepl/message
                (nrepl/client
                 tp
                 1000)
                {:op "eval" :code "(+ 3 4)"}))))
  ) ; [7]

now, in terminal, type lein run, you'll see result of [7]

here's a quick explanation of what the code is doing.

(with-open […] body) is like (let […] body). It bind local constants and eval body, except that it's for opening files, and the whole thing is wrapped in a try. clojure.core/with-open clojure.core/try

(nrepl/connect :port 33623) connects to a nrepl server and returns a nrepl “transport” clojure protocol object. http://clojure.github.io/tools.nrepl/#clojure.tools.nrepl/connect (a clojure protocol is a named set of named methods and their signatures. clojure.core/defprotocol)

The (-> x f1 f2 …) is like unix pipe. It passes x to f1 (as first arg), and pass that result to f2, …. clojure.core/->

(nrepl/client transport response-timeout) Returns a function. (todo: the function represents a “client”.)

http://clojure.github.io/tools.nrepl/#clojure.tools.nrepl/client

(nrepl/message client {:keys [id], :as msg, :or {id (uuid)}}) → Sends a message via client with a fixed message :id added to it. Returns the head of the client's response seq, filtered to include only messages related to the message :id that will terminate upon receipt of a “done” :status. http://clojure.github.io/tools.nrepl/#clojure.tools.nrepl/message

Usage: (nrepl/response-values responses)

Given a seq of responses (as from response-seq or returned from any function returned by client or client-session), returns a seq of values read from :value slots found therein. http://clojure.github.io/tools.nrepl/#clojure.tools.nrepl/response-values

Usage: (doall coll) (doall n coll)

When lazy sequences are produced via functions that have side effects, any effects other than those needed to produce the first element in the seq do not occur until the seq is consumed. doall can be used to force any effects. Walks through the successive nexts of the seq, retains the head and returns it, thus causing the entire seq to reside in memory at one time.

clojure.core/doall


nREPL largely are made up of three parts: {handler, middleware, transport}

nREPL is message-oriented and asynchronous.

nrepl messages

nrepl server and client send each other “messages”.

A message is a clojure “map” data type.

for example, here's a nrepl message:

{"op" "eval" "code" "(+ 3 4)"}

A message has many keys. Some keys are required. The key/value pairs and meaning depends on “transport” used. (“transport” is explained later.)

the "op" key means the operation to perform.

A key may imply existance of other key. For example, "op" key with value of "eval" requires a key named "code".

The result of performing each operation may be sent back to the nREPL client in one or more response messages. The content depend on the operation.

Depending on its :op, a message might be required to contain other slots, and might optionally contain others. It is generally the case that request messages should contain a globally-unique :id. Every request must provoke at least one and potentially many response messages, each of which should contain an :id slot echoing that of the provoking request.

Once a handler has completely processed a message, a response containing a :status :done must be sent. Some operations necessitate that additional responses related to the processing of a request are sent after a :status :done is reported (e.g. delivering content written to *out* by evaluated code that started a future).

Other statuses are possible, depending upon the semantics of the :op being handled; in particular, if the message is malformed or incomplete for a particular :op, then a response with a :status :error should be sent, potentially with additional information about the nature of the problem.

nrepl transports

“transport” is a layer that specify what protocol to use to transmit messages. The default transport is Network socket, and message content is encoded by Bencode (basically a scheme of transmitting bytes using ASCII characters. [see ASCII characters])

nrepl handlers

nrepl handler is a function that accept a single incoming message as argument.

Handler takes a message (map) as argument. The argument given to handler has keys of datatype “keyword” (that is, of the form {:x1 …, :x2 …, …}).

the argument given to handler are guaranteed to have a key :transport. Its value is a transport object, and is used to send response via that object.

handler return values are ignored.

middleware

“Middleware” are functions that accept a handler function and return a new handler.

“Middleware” is used for adding additional functionality onto or around the original handler.

Middleware are wrapers, for extending server's functionality, in other words.

All of nREPL's default functionality are made of many middleware. You can see these at clojure.tools.nrepl.server/default-middlewares.

default middleware are merged with any user-specified middleware provided to clojure.tools.nrepl.server/default-handler.

The merge is done in server configuration step. Server configuration are done in a format called “descriptor”. (“descriptor” is explained later.)

For example, here's a middleware that handles a message that contains {… :op "time?" …}. It replies with the local time on the server. Here's the code:

(require '[clojure.tools.nrepl.transport :as tp])
(use '[clojure.tools.nrepl.misc :only (response-for)])

(defn my-middleware-current-time
  "takes a function and returns a function."
  [xx]
  (fn [{:keys [op transport] :as msg}]
      (if (= "time?" op)
          (tp/send transport
                   (response-for msg :status :done :time (System/currentTimeMillis)))
        (xx msg))))

;; the arg, xx, is a function (a “handler”)

In the above, we defined a function “my-middleware-current-time”. It takes a function “xx” (a handler), and returns a function (a new handler).

this new handler adds a extra functionality. If a handler's input message contains {:op "time?"} pair, then it sends the current time (as side effect).

the (clojure.tools.nrepl.transport/send transport message) sends the message message via the transport object transport. It returns a transport object.

http://clojure.github.io/tools.nrepl/#clojure.tools.nrepl.transport/send

nrepl Server Middleware configuration: Middleware Descriptors

clojure.tools.nrepl.middleware/set-descriptor!

(set-descriptor! middleware_var descriptor)

Sets the given [descriptor] map as the ::descriptor metadata on the provided [middleware-var], after assoc'ing in the var's fully-qualified name as the descriptor's "implemented-by" value.

Sets the given [descriptor] map as the ::descriptor metadata on the provided [middleware-var], after assoc'ing in the var's fully-qualified name as the descriptor's “implemented-by” value.

nrepl supported operations (message keys)

for list and detail of nREPL's default middleware stack, what each operation expects in request messages, and what they emit for responses, see https://github.com/clojure/tools.nrepl/blob/master/doc/ops.md

Other nREPL middlewares are provided by the community.

Middleware descriptors and nREPL server configuration

It is generally the case that most users of nREPL will expect some minimal REPL functionality to always be available: evaluation (and the ability to interrupt evaluations), sessions, file loading, and so on. However, as with all middleware, the order in which nREPL middleware is applied to a base handler is significant; e.g., the session middleware's handler must look up a user's session and add it to the message map before delegating to the handler it wraps (so that, for example: evaluation middleware can use that session data to stand up the user's dynamic evaluation context). If middleware were “just” functions, then any customization of an nREPL middleware stack would need to explicitly repeat all of the defaults, except for the edge cases where middleware is to be appended or prepended to the default stack.

To eliminate this tedium, the vars holding nREPL middleware functions may have a descriptor applied to them to specify certain constraints in how that middleware is applied. For example, the descriptor for the clojure.tools.nrepl.middleware.session/add-stdin middleware is set thusly:

(set-descriptor! #'add-stdin
  {:requires #{#'session}
   :expects #{"eval"}
   :handles {"stdin"
             {:doc "Add content from the value of \"stdin\" to *in* in the current session."
              :requires {"stdin" "Content to add to *in*."}
              :optional {}
              :returns {"status" "A status of \"need-input\" will be sent if a session's *in* requires content in order to satisfy an attempted read operation."}}}})

Middleware descriptors are implemented as a map in var metadata under a :clojure.tools.nrepl.middleware/descriptor key. Each descriptor can contain any of three entries:

The values in the :handles map is used to support the “describe” operation, which provides “a machine- and human-readable directory and documentation for the operations supported by an nREPL endpoint” (see clojure.tools.nrepl.middleware/describe-markdown, and the results of “describe” and describe-markdown here).

The :requires and :expects entries control the order in which middleware is applied to a base handler. In the add-stdin example above, that middleware will be applied after any middleware that handles the “eval” operation, but before the clojure.tools.nrepl.middleware.session/session middleware. In the case of add-stdin, this ensures that incoming messages hit the session middleware (thus ensuring that the user's dynamic scope — including *in* — has been added to the message) before the add-stdin's handler sees them, so that it may append the provided stdin content to the buffer underlying *in*. Additionally, add-stdin must be “above” any eval middleware, as it takes responsibility for calling clojure.main/skip-if-eol on *in* prior to each evaluation (in order to ensure functional parity with Clojure's default stream-based REPL implementation).

The specific contents of a middleware's descriptor depends entirely on its objectives: which operations it is to implement/define, how it is to modify incoming request messages, and which higher- and lower-level middlewares are to aid in accomplishing its aims.

nREPL uses the dependency information in descriptors in order to produce a linearization of a set of middleware; this linearization is exposed by clojure.tools.nrepl.middleware/linearize-middleware-stack, which is implicitly used by clojure.tools.nrepl.server/default-handler to combine the default stack of middleware with any additional provided middleware vars. The primary contribution of default-handler is to use clojure.tools.nrepl.server/unknown-op as the base handler; this ensures that unhandled messages will always produce a response message with an :unknown-op :status. Any handlers otherwise created (For example, via direct usage of linearize-middleware-stack to obtain a ordered sequence of middleware vars) should do the same, or use a similar alternative base handler.


response-values will return only the values of evaluated expressions, read from their (by default) pr-encoded representations via read. You can see the full content of message responses easily:

(with-open [conn (repl/connect :port 33623)]
            (-> (repl/client conn 1000)
                (repl/message {:op :eval :code "(time (reduce + (range 1e6)))"})
                doall ;; `message` and `client-session` all return lazy seqs
                pprint)) ;; nil

({:out "\"Elapsed time: 68.032 msecs\"\n",
       :session "2ba81681-5093-4262-81c5-edddad573201",
       :id "3124d886-7a5d-4c1e-9fc3-2946b1b3cfaa"}
 {:ns "user",
      :value "499999500000",
      :session "2ba81681-5093-4262-81c5-edddad573201",
      :id "3124d886-7a5d-4c1e-9fc3-2946b1b3cfaa"}
 {:status ["done"],
          :session "2ba81681-5093-4262-81c5-edddad573201",
          :id "3124d886-7a5d-4c1e-9fc3-2946b1b3cfaa"})

Each message must contain at least an :op (or "op") slot, which specifies the "type" of the operation to be performed. The operations supported by an nREPL endpoint are determined by the handlers and middleware stack used when starting that endpoint; the default middleware stack (described below) supports a particular set of operations, detailed here.

using nREPL as Server

(use '[clojure.tools.nrepl.server :only (start-server stop-server)]) ; nil

(defonce server (start-server :port 7888))

If you have a question, put $5 at patreon and message me.