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
idandforattributes 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 positiontypeAt(code, pos)— type of the expression at a positionerrors(code)— check code for errors without executing itcreateEnv(name)/destroyEnv(name)— manage isolated execution environmentsterminate()— 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)