KC Sivaramakrishnan Building Functional Systems

Testing x-ocaml, OCaml notebooks as a WebComponent

Can we have OCaml notebooks as pure client-side code? Can these notebooks have rich editor support (highlighting, formatting, types on hover, autocompletion, inline diagnostics, etc.)? Can you take packages from OPAM and use them in these notebooks?

The answer to all of these turns out to be a resounding yes thanks for x-ocaml. This post is my experiment playing with x-ocaml and integrating that into this blog.

The most wonderful thing about programming is that it lets you experiment freely. You can try out an idea, get instant feedback, and learn by doing—much like playing with Lego bricks or sketching on a canvas. The two main courses that I teach at IITM, CS3100 and CS6225, both involve me live-coding during every lecture. However, blogging about OCaml where the code is static and non-interactive always felt a bit unsatisfying.

Enter x-ocaml, which allows for a way to embed OCaml notebooks into any webpage thanks to WebComponents. All you need to do is to load some JavaScript in your webpage and you can start embedding code cells using <x-ocaml> tag. The snippet below:

<x-ocaml>
print_endline "Hello, world"
</x-ocaml>

renders to:

print_endline "Hello, world"

The code is interpreted in the browser thanks to the OCaml interpreter compiled to JavaScript through the Js_of_ocaml compiler. There is also support for Merlin and OCamlformat in the code editor. Try hovering over the functions and writing some code. You should see inferred types and auto-completion suggestions. It turns out that this solution integrates well with Jekyll, which is what I use for this blog.

Reverse-mode AD using Effects

Js_of_ocaml also supports effect handlers. Here’s the implementation of reverse-mode algorithmic differentiation, implemented using effect handlers running in the browser.

open Effect open Effect.Deep module F : sig type t val mk : float -> t val ( +. ) : t -> t -> t val ( *. ) : t -> t -> t val grad : (t -> t) -> float -> float val grad2 : (t * t -> t) -> float * float -> float * float end = struct type t = { v : float; mutable d : float } let mk v = { v; d = 0.0 } type _ eff += Add : t * t -> t eff type _ eff += Mult : t * t -> t eff let run f = ignore (match f () with | r -> r.d <- 1.0; r; | effect (Add(a,b)), k -> let x = {v = a.v +. b.v; d = 0.0} in ignore (continue k x); a.d <- a.d +. x.d; b.d <- b.d +. x.d; x | effect (Mult(a,b)), k -> let x = {v = a.v *. b.v; d = 0.0} in ignore (continue k x); a.d <- a.d +. (b.v *. x.d); b.d <- b.d +. (a.v *. x.d); x) let grad f x = let x = mk x in run (fun () -> f x); x.d let grad2 f (x, y) = let x, y = (mk x, mk y) in run (fun () -> f (x, y)); (x.d, y.d) let ( +. ) a b = perform (Add (a, b)) let ( *. ) a b = perform (Mult (a, b)) end

Here are some tests.

(* XXX KC: `assert` from standard library doesn't work. Why? *) let assert' c = if not c then raise (Failure "assertion failed!") let test1 = (* f = x + x^3 => df/dx = 1 + 3 * x^2 *) for x = 0 to 10 do let x = float_of_int x in let d1 = F.(grad (fun x -> x +. (x *. x *. x)) x) in let d2 = 1.0 +. (3.0 *. x *. x) in Printf.printf "%f %f\n" d1 d2; assert' ( d1 = d2 ) done let test2 = (* f = x^2 + x^3 => df/dx = 2*x + 3 * x^2 *) for x = 0 to 10 do let x = float_of_int x in assert' ( F.(grad (fun x -> (x *. x) +. (x *. x *. x)) x) = (2.0 *. x) +. (3.0 *. x *. x)) done let test3 = (* f = x^2 * y^4 => df/dx = 2 * x * y^4 df/dy = 4 * x^2 * y^3 *) for x = 0 to 10 do for y = 0 to 10 do let x = float_of_int x in let y = float_of_int y in assert' ( F.(grad2 (fun (x, y) -> x *. x *. y *. y *. y *. y) (x, y)) = (2.0 *. x *. y *. y *. y *. y, 4.0 *. x *. x *. y *. y *. y)) done done

Using other libraries

x-ocaml also supports loading any js_of_ocaml compatible library into the webpage. Let’s use digestif.

For any library that you want to export, install the library using opam. x-ocaml provide a command-line utility to export the library.

$ x-ocaml --effects digestif.ocaml -o digestif.js

This produces the JavaScript artifact that can be used in the webpage. It may be instructive to look at the source of this post to see how the compiler and the libraries are integrated into this blog post. There is a little script at the top of the file:


<script async
  src="{{ '/assets/x-ocaml.js' | absolute_url }}"
  src-worker="{{ '/assets/x-ocaml.worker+effects.js' | absolute_url }}"
  src-load="{{ '/assets/digestif.js' | absolute_url }}"
></script>

let hash = Digestif.MD5.(digest_string "hello" |> to_hex)

What next?

There is a number of rough edges to x-ocaml. This is expected since this project appears to be one of Arthur’s hacking expeditions (which, as usual, is pushing the state of the art forward).

It would be fun to use this for teaching CS3100 and also other OCaml tutorials. Perhaps even have an interactive version of Real World OCaml book.

Not all OCaml libraries can be compiled to JavaScript. The common reason being that they may depend on features not available on JavaScript. In writing this post, I unsuccessfully tried for a long time to get mirage-cypto working. mirage-crypto has a large C dependency, which does not work with Js_of_ocaml. Js_of_ocaml promises to take any opam library installed on your opam switch and compiles that to JavaScript. However, at that point, we’re really cross compiling the opam packages installed on your switch to JavaScript since the installed package may make some assumptions about the platform that it is supposed to run on. Hence, JavaScript compilation of arbitrary OCaml packages is unlikely to work in the general case. Unfortunately, the error was difficult to debug since the failure was at runtime, and was not apparent in the error messages (at least for me, who has little JavaScript experience). It would be nice to have the opam packages explicitly say whether they are JavaScript compatible, and have build tooling that reports errors like these early.


Creative Commons License kcsrk dot info by KC Sivaramakrishnan is licensed under a Creative Commons Attribution 4.0 International License.