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>
32 KiB
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) |
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 for setup, the method for the conversion recipe, the README for the module map, and MENTOR.md for AI-mentor mode.