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
I'm back to working on iRoc, a Jupyter kernel for Roc. As discussed, I've replaced the --rpc option based on JSON with a --formatter option that takes a Roc script, such as json-repl-formatter.roc. Here's a sneak preview:
iRoc-demo.mov
Away from the computer so I'll have to try this later! But I have questions:
$foo = $foo + 1 in a separate cell to the vars initialisation could increment it?This sounds about right, I'll test it asap.
Edit: the first point is exactly right. However, the second is not, as I get a ✗ var outside body error when trying to define a var outside a block or function. This matches the behavior of the REPL.
If you want to try it out (you'll need Zig 0.17.0 and Python 3.10+):
# Build Roc with formatter support.
git clone --branch add-repl-structured-values --single-branch \
https://github.com/ageron/roc.git roc-iroc
cd roc-iroc
zig build roc -Doptimize=ReleaseFast
IROC_COMPILER="$PWD/zig-out/bin/roc"
cd ..
# Install iRoc and JupyterLab in a virtual environment.
git clone https://github.com/ageron/iRoc.git
cd iRoc
python3 -m venv .venv
source .venv/bin/activate
python -m pip install . jupyterlab
python -m iroc install --user --roc "$IROC_COMPILER"
jupyter lab
You can now run Roc on Colab! ![]()
![]()
Colab notebooks could be a convenient format for Roc tutorials.
Notebooks could be great for https://github.com/roc-lang/examples too
Is it possible to give collab notebooks syntax highlighting? Id be happy to help if we need anything
The main problem with Colab is that it only supports Julia, Python, and R by default (which are the 3 languages that inspired the name Jupyter, btw). This means that there's a mandatory setup phase (it currently lasts ~45 seconds) which may dissuade new users. There are other platforms such as MyBinder that let you run containers, they're more flexible but I've found them to be a bit slow to start and not always reliable.
Luke Boswell said:
Is it possible to give collab notebooks syntax highlighting? Id be happy to help if we need anything
Sadly there's no official way to do that. That said, a Colab runtime gives the user root access so certainly possible to hack it (after all, it doesn't officially support Roc either, but it works).
Can't we make our own simple notebook that uses the wasm compiler?
I'd love that!
We have the REPL being bundled into the nightlies now dont we?
oh, that never landed https://github.com/roc-lang/nightlies/pull/31
If we're going to make a Roc notebook, I think it should be reactive, like Livebook (elixir).
Luke Boswell said:
We have the REPL being bundled into the nightlies now dont we?
Oh you mean the REPL in WASM? Cool.
Although I think that a notebook is more than a REPL: it's literate programming.
This was that REPL you helped me design the API for to support a notebook experience
Oh I forgot that we had talked about a WASM version, right!
I haven't looked at it in a while, it was blocked from merging at the time or something and I got distracted working on other things
Aurélien Geron said:
If we're going to make a Roc notebook, I think it should be reactive, like Livebook (elixir).
Other pre-existing reactive notebooks to learn from include Pluto.jl (Julia) and marimo (Python).
Specifically, I like marimo's usage of plain Python files as the storage format, it lets you: run tests, run formatters, run them headless as scripts, use git (versioning, diffs...) and other plain text tools, import cells (and their ancestors) from other notebooks, import whole notebooks (e.g. an interactive UI)...
Here's the Roc tutorial on Colab
![]()
That sounds concerning at first glance...
Yeah, Colab always displays this warning. :face_with_diagonal_mouth:
I guess the notebook code could try to hack the Pentagon or something.
Plus, it could try to access your Google Drive or Colab secrets: in these cases, you get a permission prompt, but it might not be clear to some users that they are trusting the notebook's author (not Google) with their personal data. Hence this message. Perhaps they should only display it when the notebook tries to access something personal.
anything that reduces Python footprint is automatically an improvement /opinion ;)
Haha, I understand where you're coming from, but I personally love Python, especially for ML and scripting. I've used a bunch of languages, and Python is still in my top 3. It's main strength is expressivity: it's both incredibly concise and clear. However, it's slow and unsafe, so it's definitely not well suited for every use case.
I think we should take inspiration from the things it did well, such as super handy syntax for the most common data structures (tuples, sets, dicts...), the batteries included philosophy, the flat-better-than-nested philosophy, and in fact virtually all of the Zen of Python (which Roc is well aligned with). There's a reason it is one of the most popular languages out there.
Completely agree, plus "typed" Python is all that plus ~80% of the safety guarantees of say C#. That is, Ruff and Ty on toughest settings. As a fan of statically typed languages, I was surprised by how pleasant modern Python is, even compared to something like Typescript that, according to most people, should be better at this. One of my goals/dreams for Roc is to carry that philosophy and the smooth and simple experience of Pydantic + FastAPI + Datastar to Roc
Last updated: Oct 15 2026 at 17:56 UTC