I've spent a lot of time over the past few years working on Odoc, the OCaml documentation generator, so when it came time to (re)start my own website and blog, I found it hard to resist thinking about how I might use odoc as part of it. We've spent a lot of time recently trying to make odoc more able to generate structured documentation sites, so I've gone all in and am trialling using it as a tool to generate my entire site. This is a bit of an experiment, and I don't know how well it will work out, but let's see how it goes.
Additionally, I've recently been working on a project currently called odoc_notebook
, which is a set of tools to allow odoc mld
files to be used as a sort of Jupyter-style notebook. The idea is that you can write both text and code in the same file, and then run the code in the notebook interactively. Since I've only got a webserver, all the execution of code has to be done client side, so I'm making extensive use of the phenomenal Js_of_ocaml project to get an OCaml engine running in the browser.
My focus has initially been on getting 'toplevel-style' code execution working. As an example, let's write a little demo.
Let's start with a little demo:
# let x = 1 + 2;;
val x : int = 3
It's intended to look like an OCaml toplevel session, so each new expression starts with a #
and is terminated with a double semicolon. The response from the toplevel is then below that indented with 2 spaces. Right now, there's not much in the way of error checking so you can make it all very confused by deleting the hash, removing the ;;
and so on. Avoiding this, however, you can edit the numbers here and hit 'run' (maybe twice!) to see the results being updated.
There is also a little integration to allow the code to produce output more interesting than just text. The following cell creates an SVG image and 'pushes' it to Mime_printer
, which receives the mime value and renders it in the browser below the code block.
# let svg = [
{|<svg height="210" width="500" xmlns="http://www.w3.org/2000/svg">|};
{|<polygon points="100,10 40,198 190,78 10,78 160,198" |};
{|style="fill:lime;stroke:purple;stroke-width:5;"/></svg>|}];;
val svg : string list =
["<svg height=\"210\" width=\"500\" xmlns=\"http://www.w3.org/2000/svg\">";
"<polygon points=\"100,10 40,198 190,78 10,78 160,198\" ";
"style=\"fill:lime;stroke:purple;stroke-width:5;\"/></svg>"]
# Mime_printer.push "image/svg" (String.concat "\n" svg);;
- : unit = ()
There are a bunch of things I want to add to this, for example, Merlin support. In fact, merlin-js already exists and works, thanks to the fantastic work of Ulysse, but the problem is that it's not really designed for toplevel work, and it doesn't work when the code is broken up into chunks like I do here. So either I need to concatenate all the cells together before I give it to Merlin, or I need to make each cell it's own little module and 'open' every previous cell's module.
Within a single cell, it does already work. You can see that Merlin is correctly underlining the error in the following cell. You should also be able to hover over the variables and see their types.
type t = { foo : int; bar : string };;
let x = { foo = 1; bar = "hello" };;
let this_line_has_an_error = { foo = 1; bar = None };;
But across cells, I've broken Merlin, though the code is executes correctly. You can see the problem in the following cell, which re-pushes the SVG image using the variable svg
defined in the cell above. Merlin highlights the use of the varible svg
is, because it's not aware of the varible, but the code gets executed correctly and the image is rendered below the cell.
Mime_printer.push "image/svg" (String.concat "\n" svg);;
Currently the use of libraries it quite limited - they are defined more-or-less statically. I've had dynamic libraries working in the past, but I need to re-implement them. The plan is to have the cma
files converted to js
files and then load them on-demand when the notebook specifies them. The tricky thing here is that we need to be able to use them both in the browser and in bytecode executables so that the 'test-promote' workflow still works. This will probably require specifying the libraries by name, and having to re-implement the work that findlib does to find the libraries and load them and their dependencies in the right order, though this time entirely over HTTP.
There are loads of other things I'm interested in doing, including:
Right now though, my focus is on the functionality required for this blog, with a secondary goal of looking at how we might use this sort of technology on the docs site on ocaml.org. Wouldn't it be cool to be able to drop into a live OCaml toplevel for any library in opam?
As a more extended example of odoc notebooks, I have converted to this format the course that I help teach at the University of Cambridge; Foundations of Computer Science. Try them out for yourself!.