Hello,
so REPLs usually incite heated debates over what is best in life and I don't want to start one of those again. I'm only looking for feedback. I obviously like this idea but as long as no tool supports it it is not that useful.
I want to propose a very small (about 100 LoC) extension (library) on top of clojure.main/repl. The only purpose of this is to simplify better tool support. The streaming model is kept as is, it just becomes extensible and inspectable from the outside.
To do this I introduce two new concepts: REPL "root" and REPL "level"
The "root" is whoever created the *in* and *out* streams. There will usually be one for the JVM but you can get additional ones via sockets but this is optional.
A "level" is created whenever a new read-eval-print-loop is started. Any loop aware of the above model can register itself as a level.
Each root can expose additional features as can every level but the basic contract is you read from *in* and print to *out*. From outside the REPL you get a very simple API to query and interact with these.
Let me try a brief example, this is inside a clojure.main REPL:
user=> (shadow.repl-demo/main)
REPL ready, type :repl/quit to exit
[1:0] user=> (shadow.repl-demo/repl)
[1:1] user=> :repl/quit
nil
[1:0] user=> :repl/quit
REPL stop. Goodbye ...
:repl/quit
user=>
shadow.repl-demo/main just properly sets up the root and enters a new level which is just another clojure.main/repl. It modifies the prompt to show the current root/level. shadow.repl-demo/repl starts another level which you can see in the prompt. You can nest as many as you like and it is an important property of a REPL. Maybe not so useful for a CLJ REPL inside a CLJ REPL but very useful if you start a CLJS REPL for example.
For demo purposes I start a Socket REPL (extended to be aware of this API) and start a CLJS REPL inside that. The client connecting to this looks like:
rlwrap telnet localhost 5001
Trying ::1...
telnet: connect to address ::1: Connection refused
Trying 127.0.0.1...
Connected to localhost.
Escape character is '^]'.
[2:0] user=> (shadow.devtools.api/node-repl)
[:node-repl] Build started.
[:node-repl] Build completed. (22 files, 1 compiled, 0 warnings)
cljs.user=>
The [2:0] shows that the id of the root is 2 at level 0. Starting the node REPL added another level [2:1] (don't mind that the CLJS prompt doesn't show this, it is there).
From the other window I can query what is going on.
user=> (shadow.repl/roots)
{2
{:shadow.repl/type :remote,
:shadow.repl/levels
[{:shadow.repl/lang :clj,
:shadow.repl/get-current-ns
#object[shadow.repl_demo$repl$fn__176 0x35c395cc "shadow.repl_demo$repl$fn__176@35c395cc"],
:shadow.repl/root-id 2,
:shadow.repl/level-id 0}
{:shadow.repl/lang :cljs,
:shadow.repl/get-current-ns
#object[shadow.devtools.api$repl_level$fn__26443 0x651d2d15 "shadow.devtools.api$repl_level$fn__26443@651d2d15"],
:shadow.repl/root-id 2,
:shadow.repl/level-id 1}]}}
I skipped the other levels because they are not relevant now, but this is just a simple data structure. The root is a map and each level is a map. Each can contain namespaced keywords where each keyword would have a defined (as in clojure.spec'd maybe) meaning. The information exposed is limited for demo purposes but can be extended easily.
So both levels export a :shadow.repl/lang property to indicate which language it is. In addition both provide get-current-ns which is a function with no arguments that returns the current namespace of the loop.
user=> ((:shadow.repl/get-current-ns (shadow.repl/level 2 0)))
"user"
user=> ((:shadow.repl/get-current-ns (shadow.repl/level 2 1)))
{:ns cljs.user, :name "cljs/user.cljs", ...}
Note that they both should return the same kind of data structure but I'm not sure how much information is necessary so I just made two examples. This query happened from outside the actual REPL so the loop is unaffected. In the actual CLJS REPL I can do
cljs.user=> (in-ns 'cljs.core)
cljs.core=>
and query it again somewhere else
user=> ((:shadow.repl/get-current-ns (shadow.repl/level 2 1)))
{:ns cljs.core, :name "cljs/core.cljs", ...}
I used two REPLs in this demo but access to the API shadow.repl provides could also be accessed remotely via an RPC API and whatever the tool in question prefers really. The REPL does not care how the features it provides are consumed. It could provide any feature that would be useful to a tool that would need to be executed in the context of the REPL (in a true lisp-ish fashion I guess). The loop should never be affected by what a tool does in the background as the user should be in control of that. Some things you may just send to *in* but others maybe are better placed somewhere else.
I hope this is somewhat understandable, the implementation currently resides here:
The API does not have any dependencies and is very small. I would turn this into a standalone library if anyone agrees with me that this could be useful.
The demo code just shows the minimal implementation necessary:
The interesting bits are repl/enter-root and repl/takeover.
I'd be happy to go into more detail about my motivation behind all this but that really isn't important. Yes this would directly compete with nREPL and this was born out of my frustrations with nREPL. I do not claim in any way that this would be better than nREPL. I just like this idea a lot and wanted to share it.
Cheers,
/thomas
PS: I'm bad with names and all this probably has prior art in the LISP world, so if there are better names than root and level I'd be happy to adopt those.
PS: This discussion somewhat inspired this: