go-notebook — a cell is a function

Reactive notebooks that compile. Drag something below — it's compiled Go running in your browser, no server.

Capacity — an M/M/c queue

Drag the sliders. Every downstream value recomputes; the cost/latency curve moves.

The thing you just dragged is this file. No framework import. No .ipynb. Nothing from this project — just Go and the standard library. A cell is a top-level function; the graph is its parameter names.

//go:notebook
package capacity

// Incoming jobs per hour.
//notebook:slider min=0 max=5000 step=50
func arrivalRate() (lambda PerHour) { return 1200 }

// Servers in the fleet.
//notebook:slider min=1 max=256
func servers() (c int) { return 80 }

// Offered load in Erlangs. `lambda` and `mu` wire in by name+type — that's the edge.
func offeredLoad(lambda, mu PerHour) (a Erlangs) {
    return Erlangs(float64(lambda) / float64(mu))
}

// Server utilization. `a` and `c` come from the cells above, by name.
func utilization(a Erlangs, c int) (rho float64) {
    return float64(a) / float64(c)
}

Curve fit — drag the data

Drag a point, or the polynomial degree. The least-squares fit follows, live.

Dragging a control point is an edit to a leaf — the renderer reads it, the runtime writes it, a write is not an edge. No two-way binding to wire, because the cycle is unrepresentable.

Bayesian regression — watch the posterior form

Drag n up: the posterior over the line collapses onto the truth. Drag it back down — it re-widens, because nothing here is stateful.

Scrubbing n backward works because the posterior is a pure function of sufficient statistics — sums, not a fold. A fold could only go forward. The type checker and the graph make that the natural way to write it.

The numbers — measured, including the ones that don't flatter

~1 MB
gzipped .wasm (whole notebook + engine)
~40 ms
cold load to an interactive slider
~300 µs
slider → repaint, in-browser (p50)
0
servers, kernels, or notebook files

A Pyodide-based notebook ships 10 MB+ of interpreter before your code and cold-starts in seconds — because it ships an interpreter, and this ships a compiled program.

The honest caveat: in the browser, GOOS=js is single-threaded. The scheduler's goroutine fan-out — the one place Go's concurrency pays a real dividend — is absent here; independent cells run serially. It's invisible on these demos and would matter on a compute-heavy notebook. Native builds (your laptop, an HPC node) do fan out across cores.

The same file is a job

The demos above are the notebook compiled to WebAssembly. Compile it for your cluster instead and it's a single static binary — no Python environment to reconstitute, no kernel, no spawner. The artifact that produced the figure is the artifact you rerun in six months.

$ go tool notebook build ./capacity     # one static binary, no cgo
$ scp capacity hpc:~ && ssh hpc
$ sbatch ./capacity --headless \
      --set servers=120 --set lambda=1400 --json
{"cost": 120.72, "meets": true, "wq": 4.1, ...}

Interactive on your laptop, a slider in the browser, and a batch job on the cluster — the same file, distinguished only by where you put the compiler. That last part is the thing no interpreter-based notebook can say.