My aggregated monorepo of OCaml code, automaintained

Interactive OCaml Tutorials — User's Guide#

This guide is for tutorial authors and web developers who want to create interactive OCaml content — live code cells, exercises, and widgets — served as static HTML pages with no server-side component.

How it works#

Authors write documentation in .mld files using odoc's tagged code block syntax. The odoc plugin translates {@ocaml ...} blocks into <x-ocaml> HTML elements. A WebComponent (x-ocaml.js) and a Web Worker (worker.js) handle all interactivity in the browser: editing, execution, autocompletion, and type feedback.

Author writes .mld          odoc plugin            x-ocaml + worker
─────────────────────  ──>  ────────────────  ──>  ─────────────────
{@ocaml exercise           <x-ocaml              WebComponent reads
 id=factorial               mode="exercise"       data attrs, manages
 [let facr n = ...]}        data-id="factorial">  UI, sends code to
                            ...                   worker for execution
                            </x-ocaml>

There is no server-side component beyond serving static files over HTTP.

Universes#

A universe is a self-consistent set of compiled OCaml packages. OCaml requires that all libraries in a universe are built with exactly the same versions of all transitive dependencies — you cannot mix libraries from different build environments.

Each universe is identified by a content hash and has a single entry point: findlib_index.json. This file tells the runtime which compiler version to use and where to find every package's artifacts.

Content sources#

The ocaml.org multiverse — the OCaml package documentation site hosts pre-built universes for published opam packages. Each package version's documentation page links to the correct universe for that package and its dependencies.

Self-hosted — you can generate and host your own universes for custom package sets or private libraries. See the Admin Guide for instructions on using jtw opam or day10 to produce these artifacts. Serve the output directory over HTTP with appropriate CORS headers if loading cross-origin.

Directory layout#

compiler/<ocaml-version>/<hash>/
  worker.js                        -- the OCaml toplevel web worker
  lib/ocaml/
    *.cmi, stdlib.cma.js           -- stdlib artifacts

p/<package>/<version>/<hash>/
  lib/<findlib-name>/
    META, *.cmi, *.cma.js          -- package artifacts

u/<universe-hash>/
  findlib_index.json               -- entry point

All paths include a content hash, making them safe to cache indefinitely.

Authoring tutorials#

Page-level configuration#

Custom tags at the top of the .mld file configure the page:

@x-ocaml.universe https://ocaml.org/universe/5.3.0
@x-ocaml.requires cmdliner, astring
@x-ocaml.auto-execute false
@x-ocaml.merlin false
Tag Default Purpose
universe ./universe/ URL where findlib_index.json lives
requires none Packages to preload before any cells run
auto-execute true Whether cells run automatically on page load
merlin true Whether Merlin-based LSP feedback is enabled

Cell types#

Code blocks use odoc's tagged code block syntax: {@ocaml <attributes> [...code...]}.

Attribute Purpose Editable? Visible?
interactive Demo or example cell No Yes
exercise Skeleton for the reader to edit Yes Yes
test Immutable test assertions No Yes
hidden Setup code, runs but not shown No No

Per-cell attributes#

Attribute Purpose
id=name Name this cell for explicit linking
for=name Link a test cell to a specific exercise
env=name Named execution environment
merlin Override page-level merlin setting

Execution environments#

Cells sharing an env attribute see each other's definitions. By default, all cells on a page share one environment. Named environments allow isolation when needed:

{@ocaml hidden env=greetings [
let greeting = "Hello"
]}

{@ocaml interactive env=greetings [
Printf.printf "%s, world!\n" greeting
]}

{@ocaml interactive env=math [
(* This cell cannot see 'greeting' *)
let pi = Float.pi
]}

Exercise linking#

Test cells are linked to exercise cells by two mechanisms:

  • Positional (default) — a test cell applies to the nearest preceding exercise cell.
  • Explicit — use id and for attributes when the test is distant or ambiguous.

Examples#

Interactive tutorial#

A step-by-step walkthrough where cells build on each other:

@x-ocaml.universe https://ocaml.org/universe/5.4.0
@x-ocaml.requires fmt

{1 Working with Fmt}

The [Fmt] library provides composable pretty-printing combinators.

{@ocaml interactive [
let pp_greeting ppf name =
  Fmt.pf ppf "Hello, %s!" name
]}

Try it:

{@ocaml interactive [
Fmt.pr "%a@." pp_greeting "world"
]}

Assessment worksheet#

An exercise with hidden setup, editable skeleton, and visible tests:

{@ocaml hidden [
(* Setup code the student doesn't see *)
let check_positive f =
  assert (f 0 = 1);
  assert (f 1 = 1)
]}

Write an OCaml function [facr] to compute the factorial by recursion.

{@ocaml exercise id=factorial [
let rec facr n =
    (* YOUR CODE HERE *)
    failwith "Not implemented"
]}

{@ocaml test for=factorial [
assert (facr 10 = 3628800);;
assert (facr 11 = 39916800);;
]}

Generated HTML#

The odoc plugin maps attributes directly to HTML data attributes:

<x-ocaml mode="hidden">
(* Setup code the student doesn't see *)
let check_positive f = ...
</x-ocaml>

<p>Write an OCaml function <code>facr</code> to compute the factorial
by recursion.</p>

<x-ocaml mode="exercise" data-id="factorial">
let rec facr n =
    (* YOUR CODE HERE *)
    failwith "Not implemented"
</x-ocaml>

<x-ocaml mode="test" data-for="factorial">
assert (facr 10 = 3628800);;
assert (facr 11 = 39916800);;
</x-ocaml>

The plugin also injects a <script> tag for x-ocaml.js (once per page) and <meta> tags for page-level configuration.

Using x-ocaml outside odoc#

The <x-ocaml> WebComponent works in any HTML page — it doesn't require odoc. You can write the elements by hand:

<!DOCTYPE html>
<html>
<head>
  <meta name="x-ocaml-universe"
        content="https://ocaml.org/universe/5.4.0">
  <meta name="x-ocaml-requires" content="str">
  <script src="https://ocaml.org/jtw/x-ocaml.js" type="module"></script>
</head>
<body>
  <x-ocaml mode="interactive">
  let words = Str.split (Str.regexp " ") "hello world"
  </x-ocaml>

  <x-ocaml mode="exercise" data-id="reverse">
  let reverse lst =
      (* YOUR CODE HERE *)
      failwith "todo"
  </x-ocaml>

  <x-ocaml mode="test" data-for="reverse">
  assert (reverse [1;2;3] = [3;2;1]);;
  </x-ocaml>
</body>
</html>

This makes the system usable with other documentation generators, static site builders, or hand-written HTML.

Client library (advanced)#

For custom integrations that need direct control over the Web Worker, the OcamlWorker JavaScript class provides a programmatic API.

Creating a worker#

import { OcamlWorker } from './ocaml-worker.js';

const indexUrl = './u/UNIVERSE_HASH/findlib_index.json';
const { worker, stdlib_dcs, findlib_index } =
  await OcamlWorker.fromIndex(indexUrl, '.', { timeout: 120000 });

await worker.init({
  findlib_requires: ['fmt'],
  stdlib_dcs,
  findlib_index,
});

Evaluating code#

const result = await worker.eval('List.map (fun x -> x * 2) [1;2;3];;');
console.log(result.caml_ppf);
// val x : int list = [2; 4; 6]

The result object contains:

Field Type Description
caml_ppf string Toplevel-style output (e.g. val x : int = 3)
stdout string Anything printed to stdout
stderr string Warnings and errors
mime_vals MimeVal[] Rich output (HTML, SVG, images)

Other methods#

  • complete(code, pos) — autocompletion suggestions at a cursor position
  • typeAt(code, pos) — type of the expression at a position
  • errors(code) — check code for errors without executing it
  • createEnv(name) / destroyEnv(name) — manage isolated execution environments
  • terminate() — shut down the web worker

Rich output (MIME values)#

eval() results may include mime_vals — an array of objects with mime_type and data. Libraries that produce graphical output (plotting, diagrams) use this mechanism to display results in the browser. Common types: text/html, image/svg+xml, image/png.

Loading libraries at runtime#

In addition to preloading via findlib_requires, users can load libraries dynamically:

#require "str";;
Str.split (Str.regexp " ") "hello world";;

This works for any library present in the universe's findlib_index.json.

What's next#

  • Scrollycode tutorials — scroll-driven code walkthroughs using odoc-scrollycode-extension (already prototyped)
  • Interactive widgets — reactive UI elements (sliders, plots, mini-apps) driven by an FRP library running in the Worker (experimental)