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