Files
functional-programming-snake/course/reference.md
T
Lucas WintherandClaude Fable 5.2 edd57f0148 feat(course): audit fixes and twelve extensions (Modules 7b, 12, 13)
Audit pass:
- Frame rule counts frames since the last move, so every step lasts
  exactly movesEvery(score) frames, also right after a speed-up
- clampTime + MAX_GAP_MS: a hidden tab no longer replays the whole gap
- Pong Restart keeps held keys
- Fact corrections (mulberry32 output coverage, Haskell IO, uniform)
- New fixture cases from a mutation analysis

Extensions (all three tracks, shared fixtures, course text):
- Turn queue (Exercise 10.6), persisted high score (10.7),
  replay viewer (10.8), play() end-to-end helper (9.5)
- Optional State (4.7) and optional Module 7b, stack-safe IO
- Module 12, property-based testing with generators, shrinking,
  invariants and a model-based oracle
- Module 13, Conway's Life with a Click event
- JS type checking via jsconfig, JSDoc and types.d.ts
- Maintainer tools: scripts/mutate.mjs, scripts/typecheck-js.sh,
  Playwright end-to-end suite (e2e/, scripts/e2e.sh)

Docs: docs/contract.md is the canonical contract; specs and build log
under docs/superpowers.

Verified: JS 728, Scala 689, F# 691 unit tests; stubs and progression
M01-M13 and M07b; 176 end-to-end tests on Chromium and WebKit.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 12:05:29 +01:00

292 lines
32 KiB
Markdown

# 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, queued}` (`queued`: a direction or `null`) | `GameState(width, height, snake: List[Point], dir, nextDir, apple: Option[Point], seed: Long, score, status, frame, queued: Option[Direction])` | `{ Width; Height; Snake: Point list; Dir; NextDir; Apple: Point option; Seed: uint32; Score; Status; Frame; Queued: Direction option }` |
| 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").
Optional (Exercise 4.7): the same pattern for any state type `S`. Nothing in the game uses it.
| Concept | JavaScript (`js/src/state.js`) | Scala (`scala/src/State.scala`) | F# (`fsharp/src/State.fs`) |
|---|---|---|---|
| the type | a function `s => [value, nextS]` | `final case class State[S, A](run: S => (A, S))` | `type State<'s, 'a> = State of ('s -> 'a * 's)` |
| run it | `run(st, s)` → `[a, s2]` | `st.run(s)` → `(a, s2)` (the field) | `State.run s st` → `(a, s2)` |
| constant | `pure(a)` | `State.pure(a)` | `State.pure a` |
| transform the value | `map(st, f)` | `st.map(f)` | `State.map f st` |
| chain | `flatMap(st, f)` | `st.flatMap(f)` | `State.bind f st` |
| read the state | `get` (a value) | `State.get[S]: State[S, S]` | `State.get () : State<'s, 's>` |
| replace the state | `put(s)` (value `undefined`) | `State.put(s): State[S, Unit]` | `State.put s : State<'s, unit>` |
| change the state | `modify(f)` (value `undefined`) | `State.modify(f: S => S): State[S, Unit]` | `State.modify (f: 's -> 's) : State<'s, unit>` |
| 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` |
| turn with the queue (m10) | `steer(state, dir)` | `Game.steer(state: GameState, dir: Direction): GameState` | `Game.steer (state: GameState) (dir: Direction) : GameState` |
## 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'}`; m10: `{tag: 'LoadBest'}`, `{tag: 'SaveBest', score}` | `enum Op`: `Op.Draw(shape)`, `Op.RandomSeed`; m10: `Op.LoadBest`, `Op.SaveBest(score: Int)` | `type Op = Draw of Shape \| RandomSeed`; m10: `\| LoadBest \| SaveBest of int` |
| answers to operations | plain values: `undefined` for a draw or a save, a number for a seed or a best score | `enum OpResult`: `OpResult.Done`, `OpResult.Seed(value: Long)`; m10: `OpResult.Best(value: Int)` | `type OpResult = Done \| Seed of uint32`; m10: `\| Best of int` |
| 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>` |
| load the best score (m10) | `loadBest` (a value) | `IO.loadBest: IO[Int]` (a value) | `IO.loadBest () : IO<int>` (a function of unit) |
| save the best score (m10) | `saveBest(score)` | `IO.saveBest(score: Int): IO[Unit]` | `IO.saveBest (score: int) : IO<unit>` |
| pure test interpreter | `trace(io, seed)` → `{result, ops}`; m10: `trace(io, seed, best = 0)` answers `LoadBest` with `best` | `IO.trace(io, seed): Traced[A]`, `Traced(result, ops)`; m10: `IO.trace(io, seed, best: Int = 0)` | `IO.trace seed io : Traced<'a>`, `{ Result; Ops }`; m10: `IO.traceWith (seed: uint32) (best: int) io`, with `trace seed io = traceWith seed 0 io` |
## Module 7b — stack-safe IO (optional)
Uses the `Op`, `OpResult` and `Traced` of Module 7; independent of the `IO` type itself.
| Concept | JavaScript (`js/src/safeio.js`) | Scala (`scala/src/SafeIO.scala`) | F# (`fsharp/src/SafeIO.fs`, module `Snake.SafeIO`) |
|---|---|---|---|
| the type | `{tag: 'Pure', value}`, `{tag: 'Effect', op, k}` or `{tag: 'FlatMap', io, f}` | `enum SafeIO[+A]`: `Pure(a: A)`, `Effect(op: Op, k: OpResult => SafeIO[A])`, `FlatMap[X, B](io: SafeIO[X], f: X => SafeIO[B]) extends SafeIO[B]` | `type SafeIO<'a> = Pure of 'a \| Effect of IO.Op * (IO.OpResult -> SafeIO<'a>) \| FlatMap of SafeIO<obj> * (obj -> SafeIO<'a>)` |
| finished program | `pure(value)` | `SafeIO.pure(a)` | `SafeIO.pure a` |
| one operation | `effect(op)` | `SafeIO.effect(op): SafeIO[OpResult]` | `SafeIO.effect op : SafeIO<IO.OpResult>` |
| chain (only builds a node) | `flatMap(io, f)` | `io.flatMap(f)` | `SafeIO.bind f io` (via the given `erase`, private) |
| transform the result | `map(io, f)` | `io.map(f)` | `SafeIO.map f io` |
| run in order | `sequence(ios)` | `SafeIO.sequence(ios: List[SafeIO[Unit]]): SafeIO[Unit]` | `SafeIO.sequence (ios: SafeIO<unit> list) : SafeIO<unit>` |
| one rewrite (re-association) | `step(io)` | `SafeIO.step(io: SafeIO[A]): SafeIO[A]` | `SafeIO.step (io: SafeIO<'a>) : SafeIO<'a>` |
| loop-free driver (given) | `drive(start, isDone, next)` → `[last, outs]` | none (`@tailrec` instead) | none (tail recursion instead) |
| one interpreter move | `traceStep(io, seed)` → `[next, ops]` | none | none |
| pure test interpreter | `trace(io, seed)` → `{result, ops}` | `SafeIO.trace(io, seed): Traced[A]` | `SafeIO.trace seed io : IO.Traced<'a>` |
## 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}`; m13: `{tag: 'Click', x, y}` (canvas pixels) | `enum Event`: `Event.Time(t: Long)`, `Event.KeyDown(key)`, `Event.KeyUp(key)`; m13: `Event.Click(x: Int, y: Int)` | `type Event = Time of int64 \| KeyDown of string \| KeyUp of string`; m13: `\| Click of int * int` |
| 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` |
| mouse clicks (m13, given) | `clicks` (emits `{x, y}`) | `Stream.clicks: Stream[(Int, Int)]` | `Stream.clicks : Stream<int * int>` |
| 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` |
| limit clock jumps (m10) | `clampTime(s, maxGap)` | `s.clampTime(maxGap: Long)` | `Stream.clampTime (maxGap: int64) 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`) |
| longest accepted time step (m10) | `MAX_GAP_MS` = 250 | `App.MAX_GAP_MS: Long` | `App.MAX_GAP_MS` (`250L`) |
| 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>` |
| a session (m10) | `{game, best, improved}` | `final case class Session(game: GameState, best: Int, improved: Boolean)` | `type Session = { Game: GameState; Best: int; Improved: bool }` |
| one session step (m10) | `sessionUpdate(session, action)` | `App.sessionUpdate(session: Session, action: Action): Session` | `App.sessionUpdate (session: Session) (action: Action) : Session` |
| sessions over time (m10) | `sessions(seed, best, actions)` | `App.sessions(seed: Long, best: Int, actions: Stream[Action]): Stream[Session]` | `App.sessions (seed: uint32) (best: int) (actions: Stream<Action>) : Stream<Session>` |
| picture of a session (m10) | `sessionView(session)` | `App.sessionView(session: Session): Scene` | `App.sessionView (session: Session) : Scene` |
| draw a session, save a new best (m10) | `renderSession(session)` | `App.renderSession(session: Session): IO[Unit]` | `App.renderSession (session: Session) : 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 program as data | `play(main, seed, events)` → one list of ops per drawing; m10: `play(main, seed, events, best = 0)` | `App.play(main, seed: Long, events: List[Event]): List[List[Op]]`; m10: `best: Int = 0` added | `App.play (seed: uint32) (events: Event list) main : Op list list`; m10: `App.playWith (seed: uint32) (best: int) events main`, with `play seed events main = playWith seed 0 events main` |
| 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)`; m10: `loadBest()` → number, `saveBest(score)`; m13: `onClick(cb)`, `cb(x, y)` in canvas pixels | `trait Platform` with the same method names; `randomSeed(): Long`, `startClock(onTime: Long => Unit)`; m10: `loadBest(): Int`, `saveBest(score: Int): Unit`; m13: `onClick(cb: (Int, Int) => Unit): Unit` (given) | `type Platform` record of functions: `FillRect`, `FillText`, `RandomSeed: unit -> uint32`, `StartClock: (int64 -> unit) -> unit`, `OnKeyDown`, `OnKeyUp`; m10: `LoadBest: unit -> int`, `SaveBest: int -> unit`; m13: `OnClick: (int -> int -> unit) -> unit` (given) |
| interpret an IO | `runIO(io, platform)` (`runtime.js`) | `Runtime.runIO(io, p)` | `Runtime.runIO p io` |
| run the program | `runApp(main, platform)`; m13: also registers `onClick` | `Runtime.runApp(main, p)` | `Runtime.runApp p main` |
| browser platform | `browserPlatform(canvas, win)` (`browser.js`, you write it; m10: the best score in `localStorage` key `pure-snake-best`; m13: `onClick`, with the given helper `canvasPoint(canvas, clientX, clientY)` → `{x, y}`) | `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 10 — replay viewer (`replay`)
JS `js/src/replay.js`, Scala `scala/src/Replay.scala` (types at the top level of package `snake`, functions in `object Replay`), F# `fsharp/src/Replay.fs` (module `Snake.Replay`; `ViewerAction` is `[<RequireQualifiedAccess>]`).
| Concept | JavaScript | Scala | F# |
|---|---|---|---|
| a recording | `{seed, actions}` (e.g. `fixtures/replay-sample.json`) | `final case class Recording(seed: Long, actions: List[Action])` | `type Recording = { Seed: uint32; Actions: Action list }` |
| the viewer | `{index, last, playing}` | `final case class Viewer(index: Int, last: Int, playing: Boolean)` | `type Viewer = { Index: int; Last: int; Playing: bool }` |
| viewer actions | `ViewerAction.Next`, `.Prev`, `.First`, `.Last`, `.TogglePlay`, `.Beat`; values `{tag: 'Next'}`, ... | `enum ViewerAction { Next, Prev, First, Last, TogglePlay, Beat }` | `ViewerAction.Next`, ... `ViewerAction.Beat` |
| ms per autoplay frame | `REPLAY_MS` = 100 | `Replay.REPLAY_MS: Long` | `Replay.REPLAY_MS` (`100L`) |
| every state of the game | `frames(recording)` | `Replay.frames(recording: Recording): List[GameState]` | `Replay.frames (recording: Recording) : GameState list` |
| the first viewer | `viewerStart(last)` | `Replay.viewerStart(last: Int): Viewer` | `Replay.viewerStart (last: int) : Viewer` |
| one viewer step | `viewerUpdate(viewer, action)` | `Replay.viewerUpdate(viewer: Viewer, action: ViewerAction): Viewer` | `Replay.viewerUpdate (viewer: Viewer) (action: ViewerAction) : Viewer` |
| key to action | `viewerKey(key)` → action or `null` | `Replay.viewerKey(key: String): Option[ViewerAction]` | `Replay.viewerKey (key: string) : ViewerAction option` |
| picture | `viewerView(frames, viewer)` | `Replay.viewerView(frames: List[GameState], viewer: Viewer): Scene` | `Replay.viewerView (frames: GameState list) (viewer: Viewer) : Scene` |
| actions over time | `replayActions` | `Replay.replayActions: Stream[ViewerAction]` | `Replay.replayActions : Stream<ViewerAction>` |
| viewers over time | `viewerStates(last, actions)` | `Replay.viewerStates(last: Int, actions: Stream[ViewerAction]): Stream[Viewer]` | `Replay.viewerStates (last: int) (actions: Stream<ViewerAction>) : Stream<Viewer>` |
| the whole viewer | `replayMain(recording)` | `Replay.replayMain(recording: Recording): IO[Stream[IO[Unit]]]` | `Replay.replayMain (recording: Recording) : IO<Stream<IO<unit>>>` |
| entry point (given) | `js/src/replay-main.js` (fetches the recording), page `js/replay.html` | in `scala/web/`, page `scala/replay.html` | in `fsharp/web/`, page `fsharp/replay.html` |
## 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` |
## Module 12 — property-based testing (bonus)
| Concept | JavaScript (`js/src/check.js`) | Scala (`scala/src/Check.scala`) | F# (`fsharp/src/Check.fs`) |
|---|---|---|---|
| a generator | a `Random<A>` (Module 4) | `Random[A]` | `Random<'a>` |
| pick an element | `elementOf(xs)` | `Check.elementOf(xs: List[A]): Random[A]` | `Check.elementOf (xs: 'a list) : Random<'a>` |
| `n` draws in a row | `listOf(gen, n)` | `Check.listOf(gen, n): Random[List[A]]` | `Check.listOf (n: int) gen : Random<'a list>` |
| random actions (given) | `genActions(n)` | `Check.genActions(n): Random[List[Action]]` | `Check.genActions (n: int) : Random<Action list>` |
| the report | `{ok: true, runs}` or `{ok: false, run, seed, value}` | `CheckResult.Passed(runs)`, `CheckResult.Failed(run, seed, value)` | `Passed runs`, `Failed(run, seed, value)` |
| check a property | `forAll(gen, prop, runs, seed)` | `Check.forAll(gen, runs, seed)(prop)` | `Check.forAll runs seed prop gen` |
| one element removed | `removals(xs)` | `Check.removals(xs): List[List[A]]` | `Check.removals xs : 'a list list` |
| shrink a failing list | `minimize(xs, fails)` | `Check.minimize(xs)(fails)` | `Check.minimize fails xs` |
| broken invariants | `violations(state)` → names | `Check.violations(state): List[String]` | `Check.violations state : string list` |
| frames between moves | `moveGaps(states)` | `Check.moveGaps(states): List[Int]` | `Check.moveGaps states : int list` |
| a random snake | `genSnake(width, height, maxLen)` | `Check.genSnake(width: Int, height: Int, maxLen: Int): Random[List[Point]]` | `Check.genSnake (width: int) (height: int) (maxLen: int) : Random<Point list>` |
| a random sane state (given) | `genState(width, height)` | `Check.genState(width: Int, height: Int): Random[GameState]` | `Check.genState (width: int) (height: int) : Random<GameState>` |
| the step, as an oracle | `advanceSpec(state)` | `Check.advanceSpec(state: GameState): GameState` | `Check.advanceSpec (state: GameState) : GameState` |
## Module 13 — Conway's Life
JS files are in `js/src/life/`, Scala files in `scala/src/life/` (objects `LifeModel`, `LifeRules`, `LifeView`, `LifeApp` in package `snake.life`), F# files in `fsharp/src/Life/` (modules `Snake.Life.Model`, `Snake.Life.Rules`, `Snake.Life.View`, `Snake.Life.App`; `LifeAction` is `[<RequireQualifiedAccess>]`). The click at the edge is in the Module 8 and 9 tables above.
| Concept | JavaScript | Scala | F# |
|---|---|---|---|
| constants | `LW` = 40, `LH` = 30, `LCELL` = 16, `STRIP` = 24 (`model.js`) | `LifeModel.LW`, ... `LifeModel.STRIP` | `Life.Model.LW`, ... `Life.Model.STRIP` |
| world state | `{cells, running, seed, generation}`; `cells` sorted row-major, no duplicates | `LifeState(cells: List[Point], running: Boolean, seed: Long, generation: Int)` | `{ Cells: Point list; Running: bool; Seed: uint32; Generation: int }` |
| actions | `LifeAction.Toggle(cell)`, `LifeAction.Frame`, `.TogglePause`, `.Step`, `.Clear`, `.Randomize`; values `{tag: 'Toggle', cell}`, `{tag: 'Frame'}`, ... | `enum LifeAction`: `Frame`, `Toggle(cell: Point)`, `TogglePause`, `Step`, `Clear`, `Randomize` | `LifeAction.Frame`, `LifeAction.Toggle of Point`, ... |
| fresh world | `initialLife(seed)` | `LifeModel.initialLife(seed: Long): LifeState` | `Life.Model.initialLife (seed: uint32) : LifeState` |
| one generation | `lifeStep(cells)` (`rules.js`) | `LifeRules.lifeStep(cells: List[Point]): List[Point]` | `Life.Rules.lifeStep (cells: Point list) : Point list` |
| the update function | `lifeUpdate(state, action)` | `LifeRules.lifeUpdate(state: LifeState, action: LifeAction): LifeState` | `Life.Rules.lifeUpdate (state: LifeState) (action: LifeAction) : LifeState` |
| state to picture | `lifeView(state)` (`view.js`) | `LifeView.lifeView(state: LifeState): Scene` | `Life.View.lifeView (state: LifeState) : Scene` |
| ms per generation | `GEN_MS` = 150 (`app.js`) | `LifeApp.GEN_MS: Long` | `Life.App.GEN_MS` (`150L`) |
| key to action | `lifeKey(key)` → action or `null` | `LifeApp.lifeKey(key: String): Option[LifeAction]` | `Life.App.lifeKey (key: string) : LifeAction option` |
| click to action | `lifeClick(x, y)` → `Toggle` or `null` | `LifeApp.lifeClick(x: Int, y: Int): Option[LifeAction]` | `Life.App.lifeClick (x: int) (y: int) : LifeAction option` |
| actions over time | `lifeActions` | `LifeApp.lifeActions: Stream[LifeAction]` | `Life.App.lifeActions : Stream<LifeAction>` |
| states over time | `lifeStates(seed, actions)` | `LifeApp.lifeStates(seed: Long, actions: Stream[LifeAction]): Stream[LifeState]` | `Life.App.lifeStates (seed: uint32) (actions: Stream<LifeAction>) : Stream<LifeState>` |
| the whole program | `lifeMain` (a value) | `LifeApp.lifeMain: IO[Stream[IO[Unit]]]` | `Life.App.lifeMain () : IO<Stream<IO<unit>>>` |
| entry point (given) | `js/src/life/main.js`, page `js/life.html` | in `scala/web/`, page `scala/life.html` | in `fsharp/web/`, page `fsharp/life.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` |
| type checking (optional) | `npx -y -p typescript tsc -p js/jsconfig.json` from the repository root, or VS Code as it is; types in `js/src/types.d.ts` (see [Module 0](00-setup.md#if-your-editor-understands-types-javascript-optional)) | the compiler, on every test run | the compiler, on every test run |
| 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`, replay viewer: `/js/replay.html`, Life: `/js/life.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.