docs: README, mentor mode and reference

README front page (module map, checkpoint policy with per-track file map,
maintainer tools), MENTOR.md Socratic mentor instructions with AGENTS.md /
CLAUDE.md pointers, and course/reference.md listing every API side by side.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-29 01:17:35 +01:00
co-authored by Claude Opus 5.5
parent 673be0067e
commit a23fd07906
5 changed files with 441 additions and 1 deletions
+189
View File
@@ -0,0 +1,189 @@
# Reference: the course's API in three languages
This page lists every type, value and function you write (or are given) in the course, side by side in **JavaScript**, **Scala** and **F#**. The names come straight from the stub files in `js/src/`, `scala/src/` and `fsharp/src/`; when this page and a stub's doc comment disagree, the stub wins (and please report it).
Use it to look up a spelling or an argument order. It does not explain *why*; the modules do that.
## How the three tracks differ
| Topic | JavaScript | Scala | F# |
|---|---|---|---|
| Style of calls | standalone functions, **data first**: `map(io, f)` | methods on the value, `io.map(f)`, plus functions on the companion object: `IO.pure(a)` | module functions, **data last**, curried: `IO.map f io`, so `io \|> IO.map f` works |
| Where a function lives | named export of a file: `import { map } from './io.js'` | an `object` (`Rules.turn`) or a method of the type (`r.flatMap(f)`) | a module (`Rules.turn`); `open Snake.Model` to drop the prefix |
| flatMap | `flatMap` | `flatMap` | `bind` |
| filterMap on streams | `filterMap` (function returns a value or `null`) | `filterMap` (function returns an `Option`) | `choose` (function returns an `option`, like `List.choose`) |
| Field names | camelCase: `nextDir` | camelCase: `nextDir` | PascalCase: `NextDir` |
| Sum types | strings (`'Up'`) or tagged objects (`{tag: 'Turn', dir}`) | `enum`: `Direction.Up`, `Action.Turn(dir)` | discriminated unions: `Up`, `Turn dir` |
| "Nothing here" | `null` | `None` / `Some(x)` | `None` / `Some x` |
| Seeds and random values | `number` (an integer in `[0, 2^32)`) | `Long` (in `[0, 2^32)`) | `uint32` |
| Time in ms | `number` | `Long` | `int64` |
| "No result" | `undefined` | `()` (`Unit`) | `()` (`unit`) |
| Unimplemented stub | `todo('m03')` | `???` | `failwith "TODO m03"` |
F#'s `pure` triggers warning FS0046 ("reserved for future use"); the project files silence it, so the name is fine.
## Module 1 — util
| Concept | JavaScript (`js/src/util.js`) | Scala (`scala/src/Util.scala`) | F# (`fsharp/src/Util.fs`) |
|---|---|---|---|
| numbers `0..n-1` | `range(n)` | `Util.range(n: Int): List[Int]` | `Util.range (n: int) : int list` |
| all but the last | `dropLast(xs)` | `Util.dropLast[A](xs: List[A]): List[A]` | `Util.dropLast (xs: 'a list) : 'a list` |
| membership with a custom equality | `contains(xs, p, eq)`, true if some `eq(x, p)` | `Util.contains[A](xs: List[A], p: A, eq: (A, A) => Boolean): Boolean` | `Util.contains (xs: 'a list) (p: 'a) (eq: 'a -> 'a -> bool) : bool` |
| left-to-right application | `pipe(x, ...fns)` | built in: `f andThen g`, or `x.pipe(f)` with `import scala.util.chaining.*` | built in: `x \|> f`, `f >> g` |
| right-to-left composition | `compose(...fns)` | built in: `g compose f` | built in: `g << f` |
| deep freeze | `deepFreeze(obj)` (returns the same object) | not needed: data is immutable | not needed: data is immutable |
## Module 2 — model
| Concept | JavaScript (`js/src/model.js`) | Scala (`scala/src/Model.scala`) | F# (`fsharp/src/Model.fs`) |
|---|---|---|---|
| a cell | `{x, y}`, built with `point(x, y)` | `final case class Point(x: Int, y: Int)` | `type Point = { X: int; Y: int }` |
| point equality | `pointEquals(a, b)` | `==` (case classes compare structurally) | `=` (records compare structurally) |
| direction | `'Up'`, `'Down'`, `'Left'`, `'Right'` | `enum Direction { Up, Down, Left, Right }`: `Direction.Up` | `type Direction = Up \| Down \| Left \| Right` |
| unit step | `delta(dir)` | `Model.delta(dir: Direction): Point` | `Model.delta (dir: Direction) : Point` |
| reverse direction | `opposite(dir)` | `Model.opposite(dir: Direction): Direction` | `Model.opposite (dir: Direction) : Direction` |
| status | `'Playing'`, `'Paused'`, `'GameOver'`, `'Won'` | `enum Status`: `Status.Playing`, ... | `type Status = Playing \| Paused \| GameOver \| Won` |
| actions | `Action.Tick`, `Action.Turn(dir)`, `Action.TogglePause`, `Action.Restart`, `Action.Frame`; values `{tag: 'Tick'}`, `{tag: 'Turn', dir}`, ... | `enum Action`: `Action.Tick`, `Action.Turn(dir)`, `Action.TogglePause`, `Action.Restart`, `Action.Frame` | `type Action = Tick \| Turn of Direction \| TogglePause \| Restart \| Frame` |
| game state | `{width, height, snake, dir, nextDir, apple, seed, score, status, frame}` | `GameState(width, height, snake: List[Point], dir, nextDir, apple: Option[Point], seed: Long, score, status, frame)` | `{ Width; Height; Snake: Point list; Dir; NextDir; Apple: Point option; Seed: uint32; Score; Status; Frame }` |
| fresh game | `initialState(seed)` | `Model.initialState(seed: Long): GameState` | `Model.initialState (seed: uint32) : GameState` |
The snake is a list of points, **head first**. `apple` is `null` / `None` when there is no apple.
## Module 3 — rules
| Concept | JavaScript (`js/src/rules.js`) | Scala (`scala/src/Rules.scala`) | F# (`fsharp/src/Rules.fs`) |
|---|---|---|---|
| one step from a cell | `move(p, dir)` | `Rules.move(p: Point, dir: Direction): Point` | `Rules.move (p: Point) (dir: Direction) : Point` |
| on the board? | `inBounds(state, p)` | `Rules.inBounds(state: GameState, p: Point): Boolean` | `Rules.inBounds (state: GameState) (p: Point) : bool` |
| ask to turn | `turn(state, dir)` | `Rules.turn(state: GameState, dir: Direction): GameState` | `Rules.turn (state: GameState) (dir: Direction) : GameState` |
| one step of the game | `advance(state)` | `Rules.advance(state: GameState): GameState` | `Rules.advance (state: GameState) : GameState` |
## Module 4 — random and spawn
| Concept | JavaScript (`js/src/random.js`) | Scala (`scala/src/Random.scala`) | F# (`fsharp/src/Random.fs`) |
|---|---|---|---|
| the type | a function `seed => [value, nextSeed]` | `final case class Random[A](run: Long => (A, Long))` | `type Random<'a> = Random of (uint32 -> 'a * uint32)` |
| run it | `run(r, seed)` → `[a, seed2]` | `r.run(seed)` → `(a, seed2)` (the field) | `Random.run seed r` → `(a, seed2)` |
| constant | `pure(a)` | `Random.pure(a)` | `Random.pure a` |
| transform the value | `map(r, f)` | `r.map(f)` | `Random.map f r` |
| chain | `flatMap(r, f)` | `r.flatMap(f)` | `Random.bind f r` |
| the generator (mulberry32) | `nextUint32` (itself a `Random`: call `nextUint32(seed)`) | `Random.nextUint32: Random[Long]` | `Random.nextUint32 : Random<uint32>` |
| a number in `[0, n)` | `intBelow(n)` | `Random.intBelow(n: Int): Random[Int]` | `Random.intBelow (n: int) : Random<int>` |
In F#, do every `*` and `+` of the generator through `uint64` helpers so it also works in the browser (Module 4, "F# in the browser").
| Concept | JavaScript (`js/src/spawn.js`) | Scala (`scala/src/Spawn.scala`) | F# (`fsharp/src/Spawn.fs`) |
|---|---|---|---|
| empty cells, row-major | `freeCells(state)` | `Spawn.freeCells(state): List[Point]` | `Spawn.freeCells (state: GameState) : Point list` |
| place an apple | `spawnApple(state)` | `Spawn.spawnApple(state): GameState` | `Spawn.spawnApple (state: GameState) : GameState` |
| advance, then spawn | `tick(state)` | `Spawn.tick(state): GameState` | `Spawn.tick (state: GameState) : GameState` |
| fresh game with an apple | `newGame(seed)` | `Spawn.newGame(seed: Long): GameState` | `Spawn.newGame (seed: uint32) : GameState` |
## Module 5 — game (and its Module 10 additions)
| Concept | JavaScript (`js/src/game.js`) | Scala (`scala/src/Game.scala`) | F# (`fsharp/src/Game.fs`) |
|---|---|---|---|
| the update function | `update(state, action)` | `Game.update(state: GameState, action: Action): GameState` | `Game.update (state: GameState) (action: Action) : GameState` |
| final state of a game | `replay(state, actions)` | `Game.replay(state, actions: List[Action]): GameState` | `Game.replay (state: GameState) (actions: Action list) : GameState` |
| every state of a game | `history(state, actions)` (length `actions.length + 1`) | `Game.history(state, actions): List[GameState]` | `Game.history (state: GameState) (actions: Action list) : GameState list` |
| frames per step (m10) | `movesEvery(score)` | `Game.movesEvery(score: Int): Int` | `Game.movesEvery (score: int) : int` |
## Module 6 — view
| Concept | JavaScript (`js/src/view.js`) | Scala (`scala/src/View.scala`) | F# (`fsharp/src/View.fs`) |
|---|---|---|---|
| pixels per cell | `CELL` (20) | `View.CELL: Int` | `View.CELL` |
| a rectangle | `{tag: 'Rect', x, y, w, h, color}` | `Shape.Rect(x, y, w, h, color)` | `Rect(x, y, w, h, color)` (union case of `Shape`) |
| a text | `{tag: 'Text', x, y, text, color, size, align}` | `Shape.Text(x, y, text, color, size, align)` | `Text(x, y, text, color, size, align)` |
| a picture | an array of shapes | `type Scene = List[Shape]` | `type Scene = Shape list` |
| state to picture | `view(state)` | `View.view(state: GameState): Scene` | `View.view (state: GameState) : Scene` |
## Module 7 — IO
| Concept | JavaScript (`js/src/io.js`) | Scala (`scala/src/IO.scala`) | F# (`fsharp/src/IO.fs`) |
|---|---|---|---|
| operations | `{tag: 'Draw', shape}`, `{tag: 'RandomSeed'}` | `enum Op`: `Op.Draw(shape)`, `Op.RandomSeed` | `type Op = Draw of Shape \| RandomSeed` |
| answers to operations | plain values: `undefined` for a draw, a number for a seed | `enum OpResult`: `OpResult.Done`, `OpResult.Seed(value: Long)` | `type OpResult = Done \| Seed of uint32` |
| the IO type | `{tag: 'Pure', value}` or `{tag: 'Effect', op, k}` | `enum IO[+A]`: `IO.Pure(a)`, `IO.Effect(op, k: OpResult => IO[A])` | `type IO<'a> = Pure of 'a \| Effect of Op * (OpResult -> IO<'a>)` |
| finished program | `pure(a)` | `IO.pure(a)` | `IO.pure a` |
| one operation | `effect(op)` | `IO.effect(op): IO[OpResult]` | `IO.effect op : IO<OpResult>` |
| transform the result | `map(io, f)` | `io.map(f)` | `IO.map f io` |
| chain | `flatMap(io, f)` | `io.flatMap(f)` | `IO.bind f io` |
| run in order | `sequence(ios)` | `IO.sequence(ios: List[IO[Unit]]): IO[Unit]` | `IO.sequence (ios: IO<unit> list) : IO<unit>` |
| draw one shape | `draw(shape)` | `IO.draw(shape): IO[Unit]` | `IO.draw shape : IO<unit>` |
| ask for a seed | `randomSeed` (a value) | `IO.randomSeed: IO[Long]` (a value) | `IO.randomSeed () : IO<uint32>` (a function of unit) |
| draw a scene | `drawScene(scene)` | `IO.drawScene(scene): IO[Unit]` | `IO.drawScene scene : IO<unit>` |
| pure test interpreter | `trace(io, seed)` → `{result, ops}` | `IO.trace(io, seed): Traced[A]`, `Traced(result, ops)` | `IO.trace seed io : Traced<'a>`, `{ Result; Ops }` |
## Module 8 — streams
| Concept | JavaScript (`js/src/stream.js`) | Scala (`scala/src/Stream.scala`) | F# (`fsharp/src/Stream.fs`) |
|---|---|---|---|
| inputs | plain objects `{tag: 'Time', t}`, `{tag: 'KeyDown', key}`, `{tag: 'KeyUp', key}` | `enum Event`: `Event.Time(t: Long)`, `Event.KeyDown(key)`, `Event.KeyUp(key)` | `type Event = Time of int64 \| KeyDown of string \| KeyUp of string` |
| the stream type | `{ step: input => [outputs, next] }` | `final case class Stream[A](step: Event => (List[A], Stream[A]))` | `type Stream<'a> = Stream of (Event -> 'a list * Stream<'a>)` |
| feed one input | `s.step(input)` | `s.step(event)` | `Stream.step e s` |
| key presses / releases | `keyDowns`, `keyUps` | `Stream.keyDowns`, `Stream.keyUps` | `Stream.keyDowns`, `Stream.keyUps` |
| a clock | `interval(ms)` | `Stream.interval(ms: Long): Stream[Long]` | `Stream.interval (ms: int64) : Stream<int64>` |
| transform outputs | `map(s, f)` | `s.map(f)` | `Stream.map f s` |
| keep some outputs | `filter(s, p)` | `s.filter(p)` | `Stream.filter p s` |
| transform and drop | `filterMap(s, f)` (drop `null`) | `s.filterMap(f)` (drop `None`) | `Stream.choose f s` (drop `None`) |
| combine two streams | `merge(a, b)` | `a.merge(b)` | `Stream.merge a b` |
| running fold | `scan(s, f, z)` | `s.scan(z)(f)` | `Stream.scan f z s` |
| emit a first value | `startWith(s, v)` | `s.startWith(v)` | `Stream.startWith v s` |
| run on a list of inputs | `run(s, inputs)` | `s.run(events)` | `Stream.run events s` |
## Module 9 — input, app, runtime (and Module 10 additions)
| Concept | JavaScript | Scala | F# |
|---|---|---|---|
| key to action | `keyToAction(key)` → action or `null` (`input.js`) | `Input.keyToAction(key: String): Option[Action]` | `Input.keyToAction (key: string) : Action option` |
| ms per step | `TICK_MS` = 150 (`app.js`) | `App.TICK_MS: Long` | `App.TICK_MS` (`150L`) |
| ms per frame (m10) | `FRAME_MS` = 25 | `App.FRAME_MS: Long` | `App.FRAME_MS` (`25L`) |
| actions, classic clock | `classicActions` | `App.classicActions: Stream[Action]` | `App.classicActions : Stream<Action>` |
| actions, frame clock (m10) | `speedActions` | `App.speedActions: Stream[Action]` | `App.speedActions : Stream<Action>` |
| states over time | `states(seed, actions)` | `App.states(seed: Long, actions): Stream[GameState]` | `App.states (seed: uint32) (actions: Stream<Action>) : Stream<GameState>` |
| draw one state | `render(state)` | `App.render(state): IO[Unit]` | `App.render (state: GameState) : IO<unit>` |
| the whole program | `main` (a value) | `App.main: IO[Stream[IO[Unit]]]` (a value) | `App.main () : IO<Stream<IO<unit>>>` (a function of unit) |
| the outside world | `Platform` object: `fillRect(x, y, w, h, color)`, `fillText(x, y, text, color, size, align)`, `randomSeed()`, `startClock(onTime)`, `onKeyDown(cb)`, `onKeyUp(cb)` | `trait Platform` with the same method names; `randomSeed(): Long`, `startClock(onTime: Long => Unit)` | `type Platform` record of functions: `FillRect`, `FillText`, `RandomSeed: unit -> uint32`, `StartClock: (int64 -> unit) -> unit`, `OnKeyDown`, `OnKeyUp` |
| interpret an IO | `runIO(io, platform)` (`runtime.js`) | `Runtime.runIO(io, p)` | `Runtime.runIO p io` |
| run the program | `runApp(main, platform)` | `Runtime.runApp(main, p)` | `Runtime.runApp p main` |
| browser platform | `browserPlatform(canvas, win)` (`browser.js`, you write it) | `BrowserPlatform(canvas)` (`scala/web/`, given) | `BrowserPlatform.create canvas`, `BrowserPlatform.gameCanvas ()` (`fsharp/web/`, given) |
| entry point | `js/src/main.js` (given): `runApp(main, browserPlatform(...))` | `scala/web/Main.scala` (given): `SnakeMain` | `fsharp/web/SnakeMain.fs` (given) |
## Module 11 — Pong
JS files are in `js/src/pong/`, Scala files in `scala/src/pong/` (objects `PongModel`, `PongRules`, `PongView`, `PongApp` in package `snake.pong`), F# files in `fsharp/src/Pong/` (modules `Snake.Pong.Model`, `Snake.Pong.Rules`, `Snake.Pong.View`, `Snake.Pong.App`). In F#, the Pong unions are marked `[<RequireQualifiedAccess>]`, so you always write `PongStatus.Playing`, `Paddle.Up`, `PongAction.Frame`; that keeps them apart from the snake's `Playing`, `Up` and `Frame`.
| Concept | JavaScript | Scala | F# |
|---|---|---|---|
| constants | `W`, `H`, `PCELL`, `PADDLE`, `PLAYER_X`, `CPU_X`, `WIN` (`model.js`) | `PongModel.W`, ... `PongModel.WIN` | `Pong.Model.W`, ... `Pong.Model.WIN` |
| velocity | `vel: {dx, dy}` | `Velocity(dx, dy)` | `{ Dx: int; Dy: int }` (type `Velocity`) |
| held keys | `held: {up, down}` | `Held(up, down)` | `{ Up: bool; Down: bool }` (type `Held`) |
| score | `score: {player, cpu}` | `Score(player, cpu)` | `{ Player: int; Cpu: int }` (type `Score`) |
| status | `'Playing'`, `'Over'` | `enum PongStatus`: `PongStatus.Playing`, `PongStatus.Over` | `PongStatus.Playing`, `PongStatus.Over` |
| paddle keys | `'Up'`, `'Down'` | `enum Paddle`: `Paddle.Up`, `Paddle.Down` | `Paddle.Up`, `Paddle.Down` |
| match state | `{ball: {x, y}, vel, player, cpu, held, score, status, seed, frame}` | `PongState(ball: Point, vel, player, cpu, held, score, status, seed: Long, frame)` | `{ Ball: Point; Vel; Player; Cpu; Held; Score; Status; Seed: uint32; Frame }` |
| actions | `PongAction.Press(key)`, `PongAction.Release(key)`, `PongAction.Frame`, `PongAction.Restart`; values `{tag: 'Press', key}`, ... | `enum PongAction`: `Press(key: Paddle)`, `Release(key: Paddle)`, `Frame`, `Restart` | `PongAction.Press of Paddle`, `PongAction.Release of Paddle`, `PongAction.Frame`, `PongAction.Restart` |
| fresh match | `initialPong(seed)` (`model.js`) | `PongModel.initialPong(seed: Long): PongState` | `Pong.Model.initialPong (seed: uint32) : PongState` |
| keep in range | `clamp(lo, hi, x)` (`rules.js`) | `PongRules.clamp(lo: Int, hi: Int, x: Int): Int` | `Pong.Rules.clamp (lo: int) (hi: int) (x: int) : int` |
| the update function | `pongUpdate(state, action)` | `PongRules.pongUpdate(state: PongState, action: PongAction): PongState` | `Pong.Rules.pongUpdate (state: PongState) (action: PongAction) : PongState` |
| state to picture | `pongView(state)` (`view.js`) | `PongView.pongView(state: PongState): Scene` | `Pong.View.pongView (state: PongState) : Scene` |
| ms per frame | `FRAME_MS_PONG` = 30 (`app.js`) | `PongApp.FRAME_MS_PONG: Long` | `Pong.App.FRAME_MS_PONG` (`30L`) |
| key to action | `pongKey(isDown, key)` → action or `null` | `PongApp.pongKey(isDown: Boolean, key: String): Option[PongAction]` | `Pong.App.pongKey (isDown: bool) (key: string) : PongAction option` |
| actions over time | `pongActions` | `PongApp.pongActions: Stream[PongAction]` | `Pong.App.pongActions : Stream<PongAction>` |
| states over time | `pongStates(seed, actions)` | `PongApp.pongStates(seed: Long, actions): Stream[PongState]` | `Pong.App.pongStates (seed: uint32) (actions: Stream<PongAction>) : Stream<PongState>` |
| the whole program | `pongMain` (a value) | `PongApp.pongMain: IO[Stream[IO[Unit]]]` | `Pong.App.pongMain () : IO<Stream<IO<unit>>>` |
| entry point (given) | `js/src/pong/main.js`, page `js/pong.html` | `PongMain` in `scala/web/Main.scala`, page `scala/pong.html` | `fsharp/web/PongMain.fs`, page `fsharp/pong.html` |
## Tests and tools
| Task | JavaScript (`cd js`) | Scala (`cd scala`) | F# (`cd fsharp`) |
|---|---|---|---|
| one module's tests | `node --test "test/m03-*.test.js"` or `./test.sh M03` | `./test.sh M03` | `./test.sh M03` |
| all tests (with the purity audit) | `./test.sh` | `./test.sh` | `./test.sh` |
| test files | `test/m03-rules.test.js` | `test/M03Rules.test.scala` | `tests/M03Rules.fs` |
| purity audit | `test/purity.test.js` | `test/Purity.test.scala` (`PuritySuite`) | `tests/Purity.fs` |
| build for the browser | nothing to build | `./build-web.sh` (Scala.js, writes `web/dist/`) | `./build-web.sh` (Fable, writes `web/dist/`) |
| play | `node scripts/serve.mjs` from the repository root, then <http://localhost:8000/js/> (Pong: `/js/pong.html`) | same server, <http://localhost:8000/scala/> | same server, <http://localhost:8000/fsharp/> |
See also: [Module 0](00-setup.md) for setup, [the method](method.md) for the conversion recipe, the [README](../README.md) for the module map, and [MENTOR.md](../MENTOR.md) for AI-mentor mode.