I've started implementing a Jupyter kernel for Roc. I've got a first version running. It's not quite ready for prime-time yet, but here's a teaser:
How does jupyter work with functional langs and immutability? I'm not sure how to phrase the question other than by example - the notebooks I use with Elixir must execute in sequence, and rerunning a cell only captures variable state from earlier cells and not any that have been reassigned. This prevents the number of times, or sequence you have run cells, from being a concern except for external effects. Can you control this kind of behaviour with a jupyter kernel (if desirable)? I imagine it might be interesting with Roc with vars too, but lack of shadowing does prevent the badness I'm thinking of.
Basically a Jupyter kernel is a little server that communicates over 5 ZeroMQ sockets:
I've plugged into the Roc REPL so it's exactly like a regular Roc REPL session. In the REPL, you can redefine a function (which is what I demo near the end of the video by redefining the square function to return xxx. I don't know how the REPL does that.
For example, when I ran square(3), the code came in through the Shell socket. The kernel immediately broadcast status (busy) over the IOPub socket. The Roc compiler evaluated the code, and the kernel pushed the "9" out over IOPub. Finally, the kernel told IOPub to broadcast status (idle) and told Shell that the request was complete. Meanwhile, Heartbeat was pinging in the background to keep the connection alive.
There are various ways to build a Jupyter kernel. Here are a few options:
![]()
JMP means Jupyter Messaging Protocol.
Option A is the simplest: the kernel just launches a roc repl and uses stdin/stdout to send commands to it, and capture the responses. It's cool for a proof of concept, but it's brittle (any change to the REPL's UI will break it) and it doesn't allow the full set of Jupyter features (e.g., no autocomplete).
Option B would improve greatly on option A: instead of capturing the REPL's stdin/stdout, Roc could have a special mode which would give access to all of its REPL features through a programmatic interface, such as JSON RPC. The kernel would launch something like roc eval-server and communicate with it. Any tool can query it using JSON RPC.
Option C is the same as Option B but it uses the JMP protocol instead of JSON RPC. This means that the roc binary integrates the Jupyter kernel directly: you run roc jupyter and it acts as a Roc kernel, serving the 5 ZMQ sockets I listed earlier. Any JMP-compatible tool can use it.
Option D: the kernel lives separately from the main code, but it is based on the same Zig libraries. This is what I have implemented to get started, but the downside is that the kernel needs to be recompiled every time the Roc compiler changes. Moreover, it makes the kernel quite large since it includes Roc.
Wdyt?
Oh I couldn't say :smile: but thank you I now understand Jupyter better!
I implemented option B in draft PR #9923: running roc repl --rpc will make the REPL wait for JSON commands and output JSON outputs, for example:
% roc repl --rpc
{"method": "repl.start", "id": "req001"}
{"session_id":"68da396c-9302-4d2a-a800-a3b644665c68","id":"req001"}
{"method": "repl.evaluate", "params": {"session_id": "68da396c-9302-4d2a-a800-a3b644665c68", "code": "2 + 2"}, "id": "req003"}
{"result":"4.0","stdout":"","diagnostics":null,"id":"req003"}
{"method": "repl.stop", "params": {"session_id": "68da396c-9302-4d2a-a800-a3b644665c68"}, "id": "req004"}
{"success":true,"id":"req004"}
I built a Roc kernel for Jupyter based on this RPC mode. Feel free to try it out, you should have an experience similar to the video I posted earlier in this thread. You need to compile Roc with the draft PR #9923, and follow the build & install instructions for the Roc kernel.
@Richard Feldman said that he'd rather not introduce JSON into any of our CLI or compiler outputs. I'm happy to change the draft PR: what other options do we have to ensure that the REPL can be controlled programmatically?
I went this JSON-RPC route because I believe that's how the LSP works? Actually, perhaps we could just extend the LSP to support these extra methods (start/stop/reset a session and evaluate some code in a given session)?
We could add support for a repl "plugin" script like roc glue maybe? and then handle the json inside that?
Or maybe this is something roc tooling could help with?
Idk ... just throwing some ideas around
I'm not familiar with roc glue or roc tooling. I believe that roc experimental-lsp starts a JSON RPC server (JSON objects in through stdin, JSON objects out through stdout), so I figured that this was the way to go, since it's very similar functionality (i.e., the LSP provides static info & features, while the REPL-RPC provides dynamic features).
yeah I don't want us to
have lsp in the CLI either :sweat_smile:
in general I want to follow glue's example
where you write roc code to transform into whatever format you need (json etc)
Luke Boswell said:
We could add support for a repl "plugin" script like
roc gluemaybe? and then handle the json inside that?
I'm also not familiar with how glue works, but what comes to mind is a built in platform that is coupled to the compiler. You could only use it if you version it correctly in your app, but other than that, it would give you programatic access to the repl.
As I'm typing this, the question arises: how do we do IO in such an application? And that is a rabit hole where the compiler includes a builtin platform with every possible io type, so I immediately dislike my idea.
How would the plugin approach work?
for something like this I assume we wouldn't support arbitrary I/O
glue is like "give me a list of output files you want, and their contents, and I will write them to disk" and I think it has restrictions on where they can go (e.g. must be a subdirectory of the current dir or something)
Yes roc glue is what I have been calling the "plugin" use-case... there are no prebuilt host files in the platform, the roc cli itself embeds the roc compiler (it's already there for roc build etc) and compiles the app and runs the script.
This is a pattern I have used elsewhere too, like another totally unrelated program that runs the roc apps instead of the roc cli building standalone binaries.
In future you could imagine libroc for doing this more easily, but for now you can wrap the roc modules using a zig program easy by pointing it at the roc-lang/roc repo.
The LSP is currently named experimental, because the long term goal is to remove all that logic from the compiler, and decouple that. So the LSP can upgrade independently etc.
I'm happy to follow the Roc glue path, but I'm not sure I have a good understanding of the architecture we're shooting for. Is it the option D in my diagram above? Basically the Roc eval tools would be compiled directly into the Roc kernel?
I've already implemented option D, where the Roc kernel simply uses the Roc compiler code to evaluate the cells directly, without using the Roc CLI at all. It works fine, but it has two drawbacks:
I still haven't really understood the alternative "roc-glue-like" architecture you have in mind. Would someone be kind enough to draw a picture? It sounds to me like it's similar to Option D, so it would still have these two issues, wouldn't it? I'm not pushing back, I'm just confused.
I am also not entirely sure, thinking out loud for what it's worth? I don't know how 'glue like' it is, but the repl could operate like the elm architecture, where a plugin receives/subscribes to repl events, and emits actions (or directly calls effectful host functions not sure) etc, like "evaluate snippet" or "write these bytes to a socket". The compiler supplies the repl platform, and expects the plugin to expose handlers etc. I think you would be able to supply this to Roc like roc repl --plugin jupyter.roc, and the terminal repl could be a bundled terminal.roc plugin?
I think this is along the lines of what Luke meant - the repl plugin is compiled against a platform, but stops short of linking against it and instead roc drives it directly? This is also my very rough understanding of the glue pipeline.
There's definitely some design questions here about what are the effects or the API for this kind of functionality
Aurélien Geron said:
I still haven't really understood the alternative "roc-glue-like" architecture you have in mind. Would someone be kind enough to draw a picture? It sounds to me like it's similar to Option D, so it would still have these two issues, wouldn't it? I'm not pushing back, I'm just confused.
yeah sorry, I haven't really had time to dig deeply into this...the basic idea is:
roc CLI that involves communicating data in a structured formatroc CLI understand JSON - emit it, consume it, whateverFoo and Bar, instead of having the roc CLI serialize those structs to output.json, you have the CLI take a .roc file which exposes a function that receives these Foo and Bar structs as ordinary Roc records, and then returns a List(U8) for what to print to stdout. (This is basically what glue does - you write a Roc function that takes ordinary Roc data structures which represent the shape of your host boundary, and then you return a description of what files you want to be written to disk.)so if you wanted to go the JSON route, you could implement that function to take the Foo and Bar structs from the compiler and just have the Roc function serialize them as JSON and return that, at which point you have the same interface to Jupyter as the original PR (but with the indirection of having the Roc code say "I want to turn this into JSON" instead of having that be hardcoded in the compiler)
but it also opens up other possibilities, e.g. you could make the Roc code not be restricted to just returning a Str that gets printed to stdout, you could do things like have it describe what bytes to send over a socket, and the CLI could take care of opening that socket for you, watching the filesystem, and just calling the Roc function repeatedly whenever things change
does that make sense?
Ah, that makes complete sense, thanks for the detailed explanation @Richard Feldman . :folded_hands:
So the Jupyter kernel could remain exactly the same as today, but instead of calling roc repl --rpc, it would call something like roc repl json-rpc.roc.
Would the json-rpc.roc script be based on the CLI platform to read from stdin and write to stdout? If so, how would it interface with Roc's REPL (for the Foo and Bar data structures)? Or should I develop a Roc platform designed for this use-case, which would provide both Stdin/Stdout and the REPL interface?
is it literally just taking in repl inputs and responding with output?
if so, we could abstract it more than that - like it just receives a Str from the repl and then returns a Str for what to output, for example
Yes, the core functionality is just Str in, Str out, but kernels typically offer a few more functionalities, including auto-complete and type info.
Oh and there can also be rich outputs, like HTML, images, and even interactive outputs (HTML+JavaScript).
I'd say let's start with just the core functionality then
like basically a way to drive the repl programmatically
Last updated: Jul 23 2026 at 13:15 UTC