# 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` | | a number in `[0, n)` | `intBelow(n)` | `Random.intBelow(n: Int): Random[Int]` | `Random.intBelow (n: int) : Random` | 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` | | 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 list) : IO` | | draw one shape | `draw(shape)` | `IO.draw(shape): IO[Unit]` | `IO.draw shape : IO` | | ask for a seed | `randomSeed` (a value) | `IO.randomSeed: IO[Long]` (a value) | `IO.randomSeed () : IO` (a function of unit) | | draw a scene | `drawScene(scene)` | `IO.drawScene(scene): IO[Unit]` | `IO.drawScene scene : IO` | | load the best score (m10) | `loadBest` (a value) | `IO.loadBest: IO[Int]` (a value) | `IO.loadBest () : IO` (a function of unit) | | save the best score (m10) | `saveBest(score)` | `IO.saveBest(score: Int): IO[Unit]` | `IO.saveBest (score: int) : IO` | | 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 -> 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` | | 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 list) : SafeIO` | | 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` | | a clock | `interval(ms)` | `Stream.interval(ms: Long): Stream[Long]` | `Stream.interval (ms: int64) : Stream` | | 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` | | actions, frame clock (m10) | `speedActions` | `App.speedActions: Stream[Action]` | `App.speedActions : Stream` | | states over time | `states(seed, actions)` | `App.states(seed: Long, actions): Stream[GameState]` | `App.states (seed: uint32) (actions: Stream) : Stream` | | draw one state | `render(state)` | `App.render(state): IO[Unit]` | `App.render (state: GameState) : IO` | | 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) : Stream` | | 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` | | the whole program | `main` (a value) | `App.main: IO[Stream[IO[Unit]]]` (a value) | `App.main () : IO>>` (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 `[]`). | 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` | | viewers over time | `viewerStates(last, actions)` | `Replay.viewerStates(last: Int, actions: Stream[ViewerAction]): Stream[Viewer]` | `Replay.viewerStates (last: int) (actions: Stream) : Stream` | | the whole viewer | `replayMain(recording)` | `Replay.replayMain(recording: Recording): IO[Stream[IO[Unit]]]` | `Replay.replayMain (recording: Recording) : IO>>` | | 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 `[]`, 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` | | states over time | `pongStates(seed, actions)` | `PongApp.pongStates(seed: Long, actions): Stream[PongState]` | `Pong.App.pongStates (seed: uint32) (actions: Stream) : Stream` | | the whole program | `pongMain` (a value) | `PongApp.pongMain: IO[Stream[IO[Unit]]]` | `Pong.App.pongMain () : IO>>` | | 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` (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` | | 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` | | a random sane state (given) | `genState(width, height)` | `Check.genState(width: Int, height: Int): Random[GameState]` | `Check.genState (width: int) (height: int) : Random` | | 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 `[]`). 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` | | states over time | `lifeStates(seed, actions)` | `LifeApp.lifeStates(seed: Long, actions: Stream[LifeAction]): Stream[LifeState]` | `Life.App.lifeStates (seed: uint32) (actions: Stream) : Stream` | | the whole program | `lifeMain` (a value) | `LifeApp.lifeMain: IO[Stream[IO[Unit]]]` | `Life.App.lifeMain () : IO>>` | | 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 (Pong: `/js/pong.html`, replay viewer: `/js/replay.html`, Life: `/js/life.html`) | same server, | same server, | 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.