A fork of mtelver's day10 project
at main2 325 lines 9.8 kB view raw view rendered
1# Interactive OCaml Tutorials — User's Guide 2 3This guide is for tutorial authors and web developers who want to create 4interactive OCaml content — live code cells, exercises, and widgets — served 5as static HTML pages with no server-side component. 6 7## How it works 8 9Authors write documentation in `.mld` files using odoc's tagged code block 10syntax. The odoc plugin translates `{@ocaml ...}` blocks into `<x-ocaml>` 11HTML elements. A WebComponent (`x-ocaml.js`) and a Web Worker (`worker.js`) 12handle all interactivity in the browser: editing, execution, autocompletion, 13and type feedback. 14 15``` 16Author writes .mld odoc plugin x-ocaml + worker 17───────────────────── ──> ──────────────── ──> ───────────────── 18{@ocaml exercise <x-ocaml WebComponent reads 19 id=factorial mode="exercise" data attrs, manages 20 [let facr n = ...]} data-id="factorial"> UI, sends code to 21 ... worker for execution 22 </x-ocaml> 23``` 24 25There is no server-side component beyond serving static files over HTTP. 26 27## Universes 28 29A **universe** is a self-consistent set of compiled OCaml packages. OCaml 30requires that all libraries in a universe are built with exactly the same 31versions of all transitive dependencies — you cannot mix libraries from 32different build environments. 33 34Each universe is identified by a content hash and has a single entry point: 35`findlib_index.json`. This file tells the runtime which compiler version to 36use and where to find every package's artifacts. 37 38### Content sources 39 40**The ocaml.org multiverse** — the OCaml package documentation site hosts 41pre-built universes for published opam packages. Each package version's 42documentation page links to the correct universe for that package and its 43dependencies. 44 45**Self-hosted** — you can generate and host your own universes for custom 46package sets or private libraries. See the Admin Guide for instructions on 47using `jtw opam` or `day10` to produce these artifacts. Serve the output 48directory over HTTP with appropriate CORS headers if loading cross-origin. 49 50### Directory layout 51 52``` 53compiler/<ocaml-version>/<hash>/ 54 worker.js -- the OCaml toplevel web worker 55 lib/ocaml/ 56 *.cmi, stdlib.cma.js -- stdlib artifacts 57 58p/<package>/<version>/<hash>/ 59 lib/<findlib-name>/ 60 META, *.cmi, *.cma.js -- package artifacts 61 62u/<universe-hash>/ 63 findlib_index.json -- entry point 64``` 65 66All paths include a content hash, making them safe to cache indefinitely. 67 68## Authoring tutorials 69 70### Page-level configuration 71 72Custom tags at the top of the `.mld` file configure the page: 73 74``` 75@x-ocaml.universe https://ocaml.org/universe/5.3.0 76@x-ocaml.requires cmdliner, astring 77@x-ocaml.auto-execute false 78@x-ocaml.merlin false 79``` 80 81| Tag | Default | Purpose | 82|-----|---------|---------| 83| `universe` | `./universe/` | URL where `findlib_index.json` lives | 84| `requires` | none | Packages to preload before any cells run | 85| `auto-execute` | `true` | Whether cells run automatically on page load | 86| `merlin` | `true` | Whether Merlin-based LSP feedback is enabled | 87 88### Cell types 89 90Code blocks use odoc's tagged code block syntax: 91`{@ocaml <attributes> [...code...]}`. 92 93| Attribute | Purpose | Editable? | Visible? | 94|---------------|---------------------------------|-----------|----------| 95| `interactive` | Demo or example cell | No | Yes | 96| `exercise` | Skeleton for the reader to edit | Yes | Yes | 97| `test` | Immutable test assertions | No | Yes | 98| `hidden` | Setup code, runs but not shown | No | No | 99 100### Per-cell attributes 101 102| Attribute | Purpose | 103|------------|----------------------------------------| 104| `id=name` | Name this cell for explicit linking | 105| `for=name` | Link a test cell to a specific exercise | 106| `env=name` | Named execution environment | 107| `merlin` | Override page-level merlin setting | 108 109### Execution environments 110 111Cells sharing an `env` attribute see each other's definitions. By default, 112all cells on a page share one environment. Named environments allow 113isolation when needed: 114 115``` 116{@ocaml hidden env=greetings [ 117let greeting = "Hello" 118]} 119 120{@ocaml interactive env=greetings [ 121Printf.printf "%s, world!\n" greeting 122]} 123 124{@ocaml interactive env=math [ 125(* This cell cannot see 'greeting' *) 126let pi = Float.pi 127]} 128``` 129 130### Exercise linking 131 132Test cells are linked to exercise cells by two mechanisms: 133 134- **Positional (default)** — a test cell applies to the nearest preceding 135 exercise cell. 136- **Explicit** — use `id` and `for` attributes when the test is distant or 137 ambiguous. 138 139## Examples 140 141### Interactive tutorial 142 143A step-by-step walkthrough where cells build on each other: 144 145``` 146@x-ocaml.universe https://ocaml.org/universe/5.4.0 147@x-ocaml.requires fmt 148 149{1 Working with Fmt} 150 151The [Fmt] library provides composable pretty-printing combinators. 152 153{@ocaml interactive [ 154let pp_greeting ppf name = 155 Fmt.pf ppf "Hello, %s!" name 156]} 157 158Try it: 159 160{@ocaml interactive [ 161Fmt.pr "%a@." pp_greeting "world" 162]} 163``` 164 165### Assessment worksheet 166 167An exercise with hidden setup, editable skeleton, and visible tests: 168 169``` 170{@ocaml hidden [ 171(* Setup code the student doesn't see *) 172let check_positive f = 173 assert (f 0 = 1); 174 assert (f 1 = 1) 175]} 176 177Write an OCaml function [facr] to compute the factorial by recursion. 178 179{@ocaml exercise id=factorial [ 180let rec facr n = 181 (* YOUR CODE HERE *) 182 failwith "Not implemented" 183]} 184 185{@ocaml test for=factorial [ 186assert (facr 10 = 3628800);; 187assert (facr 11 = 39916800);; 188]} 189``` 190 191### Generated HTML 192 193The odoc plugin maps attributes directly to HTML data attributes: 194 195```html 196<x-ocaml mode="hidden"> 197(* Setup code the student doesn't see *) 198let check_positive f = ... 199</x-ocaml> 200 201<p>Write an OCaml function <code>facr</code> to compute the factorial 202by recursion.</p> 203 204<x-ocaml mode="exercise" data-id="factorial"> 205let rec facr n = 206 (* YOUR CODE HERE *) 207 failwith "Not implemented" 208</x-ocaml> 209 210<x-ocaml mode="test" data-for="factorial"> 211assert (facr 10 = 3628800);; 212assert (facr 11 = 39916800);; 213</x-ocaml> 214``` 215 216The plugin also injects a `<script>` tag for `x-ocaml.js` (once per page) 217and `<meta>` tags for page-level configuration. 218 219## Using x-ocaml outside odoc 220 221The `<x-ocaml>` WebComponent works in any HTML page — it doesn't require 222odoc. You can write the elements by hand: 223 224```html 225<!DOCTYPE html> 226<html> 227<head> 228 <meta name="x-ocaml-universe" 229 content="https://ocaml.org/universe/5.4.0"> 230 <meta name="x-ocaml-requires" content="str"> 231 <script src="https://ocaml.org/jtw/x-ocaml.js" type="module"></script> 232</head> 233<body> 234 <x-ocaml mode="interactive"> 235 let words = Str.split (Str.regexp " ") "hello world" 236 </x-ocaml> 237 238 <x-ocaml mode="exercise" data-id="reverse"> 239 let reverse lst = 240 (* YOUR CODE HERE *) 241 failwith "todo" 242 </x-ocaml> 243 244 <x-ocaml mode="test" data-for="reverse"> 245 assert (reverse [1;2;3] = [3;2;1]);; 246 </x-ocaml> 247</body> 248</html> 249``` 250 251This makes the system usable with other documentation generators, static 252site builders, or hand-written HTML. 253 254## Client library (advanced) 255 256For custom integrations that need direct control over the Web Worker, the 257`OcamlWorker` JavaScript class provides a programmatic API. 258 259### Creating a worker 260 261```javascript 262import { OcamlWorker } from './ocaml-worker.js'; 263 264const indexUrl = './u/UNIVERSE_HASH/findlib_index.json'; 265const { worker, stdlib_dcs, findlib_index } = 266 await OcamlWorker.fromIndex(indexUrl, '.', { timeout: 120000 }); 267 268await worker.init({ 269 findlib_requires: ['fmt'], 270 stdlib_dcs, 271 findlib_index, 272}); 273``` 274 275### Evaluating code 276 277```javascript 278const result = await worker.eval('List.map (fun x -> x * 2) [1;2;3];;'); 279console.log(result.caml_ppf); 280// val x : int list = [2; 4; 6] 281``` 282 283The result object contains: 284 285| Field | Type | Description | 286|-------------|-------------|------------------------------------------------| 287| `caml_ppf` | `string` | Toplevel-style output (e.g. `val x : int = 3`) | 288| `stdout` | `string` | Anything printed to stdout | 289| `stderr` | `string` | Warnings and errors | 290| `mime_vals` | `MimeVal[]` | Rich output (HTML, SVG, images) | 291 292### Other methods 293 294- **`complete(code, pos)`** — autocompletion suggestions at a cursor position 295- **`typeAt(code, pos)`** — type of the expression at a position 296- **`errors(code)`** — check code for errors without executing it 297- **`createEnv(name)` / `destroyEnv(name)`** — manage isolated execution 298 environments 299- **`terminate()`** — shut down the web worker 300 301### Rich output (MIME values) 302 303`eval()` results may include `mime_vals` — an array of objects with 304`mime_type` and `data`. Libraries that produce graphical output (plotting, 305diagrams) use this mechanism to display results in the browser. Common types: 306`text/html`, `image/svg+xml`, `image/png`. 307 308### Loading libraries at runtime 309 310In addition to preloading via `findlib_requires`, users can load libraries 311dynamically: 312 313```ocaml 314#require "str";; 315Str.split (Str.regexp " ") "hello world";; 316``` 317 318This works for any library present in the universe's `findlib_index.json`. 319 320## What's next 321 322- **Scrollycode tutorials** — scroll-driven code walkthroughs using 323 `odoc-scrollycode-extension` (already prototyped) 324- **Interactive widgets** — reactive UI elements (sliders, plots, mini-apps) 325 driven by an FRP library running in the Worker (experimental)