docs(course): fix review findings in modules 06–11

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-29 01:06:09 +01:00
co-authored by Claude Opus 5.5
parent a7a4cb5b2a
commit 673be0067e
6 changed files with 93 additions and 51 deletions
+5 -3
View File
@@ -195,6 +195,8 @@ Our `view : GameState → Scene` is exactly this, for a canvas instead of HTML.
## Exercises
> **About test names.** The test titles quoted under "Proved by" are the JavaScript ones. The Scala and F# suites test the same things one-to-one, but their titles use each track's vocabulary: "events" instead of "inputs", `None` instead of `null`, `()` instead of `undefined`, `bind`/`choose` instead of `flatMap`/`filterMap`, `IO.map throwingF io` instead of `map(io, throwingF)`, and so on. Failing tests are reported by name when you run the module's tests; to read all titles, open `scala/test/M06View.test.scala` or `fsharp/tests/M06View.fs`. Fixture-based tests have the same case names in every track.
### Exercise 6.1 — `view`
Implement `view` so that it returns the scene described in [The specification of the scene](#the-specification-of-the-scene).
@@ -217,9 +219,9 @@ object View:
**F#** (`fsharp/src/View.fs`)
```fsharp
module View =
let CELL = 20
let view (state: GameState) : Scene = failwith "TODO m06"
// module Snake.View
let CELL = 20
let view (state: GameState) : Scene = failwith "TODO m06"
```
**Proved by:** `fixtures/m06-view.json` (cases `playing-with-apple`, `playing-without-apple-has-no-apple-rect`, `paused-overlay`, `game-over-overlay-on-20x20-board`, `won-overlay`) and a test that `CELL` is 20.
+8 -6
View File
@@ -215,7 +215,7 @@ With `effect` you can build the two operations the game needs:
- **`draw(shape)`** — one `Draw` operation. In JS the answer is already `undefined`, so `draw(shape)` can be `effect(...)` directly. In Scala/F# `effect` gives an `IO[OpResult]`, but `draw` promises `IO[Unit]`: you'll need to turn the `Done` into `()`.
- **`randomSeed`** — one `RandomSeed` operation, producing the seed. In Scala/F# you must turn `Seed(value)` into `value`. What about `Done`? An interpreter that answers a `RandomSeed` with `Done` is broken; the honest thing to do is fail loudly (throw an exception with a clear message). That case cannot happen with a correct interpreter.
Notice that **`randomSeed` is a value, not a function**. In JS you write `randomSeed`, not `randomSeed()`. Why does that make sense? Because the *description* "ask for a seed" is always the same description. Using it twice in a program asks twice when interpreted, exactly as you would expect — but the value itself never changes. (In the JS stub it is a strange-looking object with a getter, only so that the stub fails loudly; replace it with a plain value.)
Notice that **`randomSeed` is a value, not a function**. In JS you write `randomSeed`, not `randomSeed()`. (F# is the exception in spelling only: there it is `randomSeed ()`, a function of unit, because F# evaluates module-level values as soon as the module is first used, and a failing stub would then break every test. Every call returns the same description, so it is still a value in every sense that matters.) Why does that make sense? Because the *description* "ask for a seed" is always the same description. Using it twice in a program asks twice when interpreted, exactly as you would expect — but the value itself never changes. (In the JS stub it is a strange-looking object with a getter, only so that the stub fails loudly; replace it with a plain value.)
### 6. `map`: changing the result without running anything
@@ -366,7 +366,7 @@ let io = IOBuilder()
let program : IO<uint32> =
io {
let! seed = IO.randomSeed
let! seed = IO.randomSeed ()
do! IO.drawScene (View.view (Spawn.newGame seed))
return seed
}
@@ -398,6 +398,8 @@ Consequences you will use in the rest of the course:
## Exercises
> **About test names.** The test titles quoted under "Proved by" are the JavaScript ones. The Scala and F# suites test the same things one-to-one, but their titles use each track's vocabulary: "events" instead of "inputs", `None` instead of `null`, `()` instead of `undefined`, `bind`/`choose` instead of `flatMap`/`filterMap`, `IO.map throwingF io` instead of `map(io, throwingF)`, and so on. Failing tests are reported by name when you run the module's tests; to read all titles, open `scala/test/M07IO.test.scala` or `fsharp/tests/M07IO.fs`. Fixture-based tests have the same case names in every track.
All exercises are in the one file per track. Write them in the order below; each one uses the previous ones. Remember the purity rules: no `let`, no loops with mutation, no `push` in JS; no `var`/`mutable`. (`trace` is recursive.)
### Exercise 7.1 — `pure` and `effect`
@@ -420,9 +422,9 @@ object IO:
**F#**
```fsharp
module IO =
let pure (a: 'a) : IO<'a> = failwith "TODO m07"
let effect (op: Op) : IO<OpResult> = failwith "TODO m07"
// module Snake.IO
let pure (a: 'a) : IO<'a> = failwith "TODO m07"
let effect (op: Op) : IO<OpResult> = failwith "TODO m07"
```
**Proved by:** `effect(op) is an Effect whose continuation is pure` (it calls the continuation with an answer and compares with `pure(answer)`).
@@ -481,7 +483,7 @@ def drawScene(scene: Scene): IO[Unit] = ???
```fsharp
let sequence (ios: IO<unit> list) : IO<unit> = failwith "TODO m07"
let draw (shape: Shape) : IO<unit> = failwith "TODO m07"
let randomSeed : IO<uint32> = (* a value *)
let randomSeed () : IO<uint32> = failwith "TODO m07"
let drawScene (scene: Scene) : IO<unit> = failwith "TODO m07"
```
+6 -1
View File
@@ -268,6 +268,8 @@ Later work in Haskell, such as [Yampa](https://wiki.haskell.org/Yampa) (used for
## Exercises
> **About test names.** The test titles quoted under "Proved by" are the JavaScript ones. The Scala and F# suites test the same things one-to-one, but their titles use each track's vocabulary: "events" instead of "inputs", `None` instead of `null`, `()` instead of `undefined`, `bind`/`choose` instead of `flatMap`/`filterMap`, `IO.map throwingF io` instead of `map(io, throwingF)`, and so on. Failing tests are reported by name when you run the module's tests; to read all titles, open `scala/test/M08Stream.test.scala` or `fsharp/tests/M08Stream.fs`. Fixture-based tests have the same case names in every track.
Do them in this order; later ones use earlier ones (`filterMap` can be written using `map` and `filter`; the tests for everything use `run`, so write `run` early).
### Exercise 8.1 — `run`
@@ -287,9 +289,12 @@ def run(events: List[Event]): List[A] = ??? // a method on Stream[A]: s.run(ev
**F#**
```fsharp
let run (events: Event list) (Stream step: Stream<'a>) : 'a list = failwith "TODO m08"
let step (e: Event) (s: Stream<'a>) : 'a list * Stream<'a> = failwith "TODO m08" // F# only, see below
let run (events: Event list) (s: Stream<'a>) : 'a list = failwith "TODO m08"
```
**F# only:** in JS and Scala you step a stream with `s.step(e)`. In F# the function is wrapped in the single-case union `Stream`, so the F# file has a small helper `Stream.step e s` that unwraps it and calls the function (one line with a pattern match, `let (Stream f) = s`). Write it first and use it everywhere else. It has its own test: `step feeds one event and returns its outputs and the next stream`.
A pure fold over the events (no `let` reassignments, no `push`). **Proved by:** `run of no inputs is empty` (Scala/F#: `run of no events is empty`), and every other test.
### Exercise 8.2 — sources: `keyDowns`, `keyUps`, `interval`
+35 -30
View File
@@ -211,7 +211,7 @@ const fakePlatform = (seed = 42) => {
- `RandomSeed` → `platform.randomSeed()`; the answer is that number (`Seed(n)`);
- pass the answer to `k`, and continue with the program it returns, until you reach `Pure(value)`; return `value`.
One requirement is new: **it must be a loop, not recursion.** A single frame is a few hundred draws, a game runs for minutes, and the test `runIO handles 100000 effects without overflowing the stack` makes sure. In JS and F# that means a `while` loop over a mutable "current program" (allowed here, and only here). In Scala, write a `@annotation.tailrec` recursive function: the compiler turns it into a loop, and you keep your one `var` for `runApp`.
One requirement is new: **it must be a loop, not recursion.** A single frame is a few hundred draws, a game runs for minutes, and the test `runIO handles 100000 effects without overflowing the stack` makes sure. In JS that means a `while` loop over a local "current program" variable (allowed here, and only here). In Scala, write a `@annotation.tailrec` recursive function: the compiler turns it into a loop, and you keep your one `var` for `runApp`. In F#, either a loop or a tail-recursive function works: F# compiles a self-recursive call in tail position into a loop, so `let rec runIO p io = match io with ... | Effect (op, k) -> runIO p (k ...)` doesn't grow the stack.
Remember section 12 of Module 7: the loop only helps if each continuation does a small, constant amount of work. If your `sequence` is left-nested, the 100 000-effect test overflows *inside* `k`, and the fix is in `io`, not in the runtime.
@@ -220,7 +220,7 @@ Remember section 12 of Module 7: the loop only helps if each continuation does a
`runApp(main, platform)` brings the program to life:
1. **Interpret `main`** with `runIO`. This asks the platform for a seed and gives you the stream (`Stream<IO<unit>>`).
2. **Keep the current stream in a mutable variable.** This is the *only* mutable variable in the whole program that outlives a function call. (The loop in `runIO` in JS and F# reassigns a local "current program" variable, but that variable exists only during one call and nobody else can see it; it is a loop counter, not state. The stream variable is the program's entire memory between events.)
2. **Keep the current stream in a mutable variable.** This is the *only* mutable variable in the whole program that outlives a function call. In F# it is a **ref cell** (`let stream = ref …`, read with `stream.Value`, written with `stream.Value <- next`), because F# closures can't capture a `let mutable`, and the callbacks are closures. (The loop in `runIO` in JS reassigns a local "current program" variable, but that variable exists only during one call and nobody else can see it; it is a loop counter, not state. The stream variable is the program's entire memory between events.)
3. **Register three callbacks** on the platform — key down, key up, clock — each of which turns what it receives into an event (`KeyDown key`, `KeyUp key`, `Time t`) and **feeds** it:
- step the current stream with the event → `(ios, next)`;
- store `next` in the variable;
@@ -233,7 +233,7 @@ That's the whole runtime. Go back to the stateful code at the start of Module 8,
browser ──key──▶│ feed(KeyDown key): [ios, next] = stream.step(ev); stream = next; ios.forEach(runIO) │──▶ canvas
──time─▶│ feed(Time t) │
└───────────────────────────────────────────────────────────────────────────────┘
▲ the one `let` / `var` / `mutable`: stream
▲ the one `let` / `var` / ref cell: stream
```
The tests describe the behaviour precisely; read them before writing the code:
@@ -244,7 +244,7 @@ The tests describe the behaviour precisely; read them before writing the code:
### 8. Who is impure, exactly?
Let's take inventory. In JS, three files are allowed to be impure and are excluded from the purity audit: `runtime.js` (`runIO`, `runApp`), `browser.js` (the canvas `Platform`) and `main.js` (one line: `runApp(main, browserPlatform(document.getElementById('game'), window))`). In Scala it's `Runtime.scala` plus the browser glue in `scala/web/`; in F#, `Runtime.fs` plus `fsharp/web/`. Everything else — rules, randomness, view, `IO`, streams, `main` — is pure and passes the audit.
Let's take inventory. In JS, three files are allowed to be impure and are excluded from the purity audit: `runtime.js` (`runIO`, `runApp`), `browser.js` (the canvas `Platform`) and `main.js` (one line: `runApp(main, browserPlatform(document.getElementById('game'), window))`). In Scala it's `Runtime.scala` plus the browser glue in `scala/web/`; in F#, `Runtime.fs` plus `fsharp/web/` (whose `SnakeMain.fs` is the one line `Runtime.runApp (BrowserPlatform.create (BrowserPlatform.gameCanvas ())) (App.main ())` — note `main ()`: in F#, `main` is a function of unit, like `IO.randomSeed ()`, for the reason explained in Module 7). Everything else — rules, randomness, view, `IO`, streams, `main` — is pure and passes the audit.
Count the lines of your runtime when you're done. It's probably around twenty. That's the whole imperative shell around a functional core.
@@ -272,12 +272,14 @@ The first run downloads the Scala.js linker and takes a while. It writes `scala/
cd fsharp && ./build-web.sh
```
then serve the repository root and open <http://localhost:8000/fsharp/>.
then serve the repository root (`node scripts/serve.mjs`, run from the repository root) and open <http://localhost:8000/fsharp/>.
Use the arrow keys or WASD. **You built a game in which every line of logic is pure.** Play a round. Then, before moving on, do the trace exercise in "Check your understanding": follow one key press from the keyboard to the pixels, naming each function it passes through.
## Exercises
> **About test names.** The test titles quoted under "Proved by" are the JavaScript ones. The Scala and F# suites test the same things one-to-one, but their titles use each track's vocabulary: "events" instead of "inputs", `None` instead of `null`, `()` instead of `undefined`, `bind`/`choose` instead of `flatMap`/`filterMap`, `IO.map throwingF io` instead of `map(io, throwingF)`, and so on. Failing tests are reported by name when you run the module's tests; to read all titles, open `scala/test/M09*.test.scala` or `fsharp/tests/M09*.fs`. Fixture-based tests have the same case names in every track.
### Exercise 9.1 — `keyToAction`
**JavaScript** (`js/src/input.js`)
@@ -296,8 +298,8 @@ object Input:
**F#** (`fsharp/src/Input.fs`)
```fsharp
module Input =
let keyToAction (key: string) : Action option = failwith "TODO m09"
// module Snake.Input
let keyToAction (key: string) : Action option = failwith "TODO m09"
```
**Proved by:** `fixtures/m09-input.json` (`ArrowUp-turns-up` … `D-turns-right`, `x-is-ignored`, `Enter-is-ignored`, `empty-string-is-ignored`, `constructor-is-not-a-key`, `toString-is-not-a-key`).
@@ -328,12 +330,12 @@ object App:
**F#** (`fsharp/src/App.fs`)
```fsharp
module App =
let TICK_MS = 150L
let classicActions : Stream<Action> = (* ... *)
let states (seed: uint32) (actions: Stream<Action>) : Stream<GameState> = failwith "TODO m09"
let render (state: GameState) : IO<unit> = failwith "TODO m09"
let main : IO<Stream<IO<unit>>> = (* ... *)
// module Snake.App
let TICK_MS = 150L
let classicActions : Stream<Action> = (* ... *)
let states (seed: uint32) (actions: Stream<Action>) : Stream<GameState> = failwith "TODO m09"
let render (state: GameState) : IO<unit> = failwith "TODO m09"
let main () : IO<Stream<IO<unit>>> = failwith "TODO m09"
```
**Proved by:** `the game ticks every 150 ms`, `classicActions turns Tick into actions on the clock and keys into Turn actions`, `states starts with the new game, then yields one state per action`, `render draws the whole view of the state`, `main asks for a random seed, then builds the stream from it`, `the program built from classicActions draws the new game first, then the ticked game after 150 ms`, `main draws again once time has passed`.
@@ -357,12 +359,12 @@ object Runtime:
def runApp(main: IO[Stream[IO[Unit]]], p: Platform): Unit = ???
```
**F#** (`fsharp/src/Runtime.fs` — `mutable` allowed here)
**F#** (`fsharp/src/Runtime.fs` — one ref cell allowed here)
```fsharp
module Runtime =
let runIO (p: Platform) (io: IO<'a>) : 'a = failwith "TODO m09"
let runApp (p: Platform) (main: IO<Stream<IO<unit>>>) : unit = failwith "TODO m09"
// module Snake.Runtime
let runIO (p: Platform) (io: IO<'a>) : 'a = failwith "TODO m09"
let runApp (p: Platform) (main: IO<Stream<IO<unit>>>) : unit = failwith "TODO m09"
```
**Proved by** (`m09-runtime`): `runIO of a pure value returns it without touching the platform`, `runIO maps Draw Rect to fillRect(x, y, w, h, color)`, `runIO maps Draw Text to fillText(x, y, text, color, size, align)`, `runIO answers RandomSeed with platform.randomSeed() and continues with it`, `runIO performs effects in order`, `runIO handles 100000 effects without overflowing the stack`, and the four `runApp …` tests from section 7.
@@ -381,8 +383,8 @@ Build a `Platform` from a canvas and a window: `fillRect`/`fillText` use `canvas
**Run:**
- JS: `cd js && node --test "test/m09-*.test.js"`, then `node scripts/serve.mjs` (from the repo root) and open <http://localhost:8000/js/>
- Scala: `cd scala && ./test.sh M09`, then `./build-web.sh`, serve, open <http://localhost:8000/scala/>
- F#: `cd fsharp && ./test.sh M09`, then `./build-web.sh`, serve, open <http://localhost:8000/fsharp/>
- Scala: `cd scala && ./test.sh M09`, then `./build-web.sh`; from the repository root run `node scripts/serve.mjs` and open <http://localhost:8000/scala/>
- F#: `cd fsharp && ./test.sh M09`, then `./build-web.sh`; from the repository root run `node scripts/serve.mjs` and open <http://localhost:8000/fsharp/>
## Hints
@@ -427,7 +429,7 @@ lazy val main: IO[Stream[IO[Unit]]] = IO.randomSeed.map(seed => /* ? */)
```fsharp
let classicActions = Stream.merge (Stream.interval TICK_MS |> Stream.map (fun _ -> (* ? *))) (Stream.keyDowns |> Stream.choose (* ? *))
let states seed actions = actions |> Stream.scan (* ? *) (* ? *) |> Stream.startWith (* ? *)
let main = IO.randomSeed |> IO.map (fun seed -> (* ? *))
let main () = IO.randomSeed () |> IO.map (fun seed -> (* ? *))
```
(F#'s `Game.update` takes `state` then `action`, which is the order `Stream.scan`'s folder wants.)
@@ -487,17 +489,20 @@ def runApp(main: IO[Stream[IO[Unit]]], p: Platform): Unit =
**F#**
```fsharp
let runIO (p: Platform) (io: IO<'a>) : 'a =
let mutable current = io
let mutable result = None
while result.IsNone do
match current with
| Pure a -> result <- Some a
| Effect (op, k) -> current <- k ((* perform op on p *))
result.Value
```
let rec runIO (p: Platform) (io: IO<'a>) : 'a =
match io with
| Pure a -> a
| Effect (op, k) -> runIO p (k ((* perform op on p, giving an OpResult *))) // tail call: compiled to a loop
(F#'s `runApp` follows the JS shape with one `let mutable stream`.)
let runApp (p: Platform) (main: IO<Stream<IO<unit>>>) : unit =
let stream = ref ((* ? *)) // the one ref cell
let feed (e: Event) =
let ios, next = (* step stream.Value with e *)
stream.Value <- next
(* run every io in ios, in order *)
p.OnKeyDown (fun key -> feed (KeyDown key))
(* ... *)
```
</details>
+3 -1
View File
@@ -139,6 +139,8 @@ Notice what you did *not* have to do: touch the rules of the game, or let `updat
## Exercises
> **About test names.** The test titles quoted under "Proved by" are the JavaScript ones. The Scala and F# suites test the same things one-to-one, but their titles use each track's vocabulary: "events" instead of "inputs", `None` instead of `null`, `()` instead of `undefined`, `bind`/`choose` instead of `flatMap`/`filterMap`, `IO.map throwingF io` instead of `map(io, throwingF)`, and so on. Failing tests are reported by name when you run the module's tests; to read all titles, open `scala/test/M10Extensions.test.scala` or `fsharp/tests/M10Extensions.fs`. Fixture-based tests have the same case names in every track.
### Exercise 10.1 — `movesEvery`
**JavaScript** (`js/src/game.js`)
@@ -286,7 +288,7 @@ def update(state: GameState, action: Action): GameState = action match
let update (state: GameState) (action: Action) : GameState =
match action with
| Tick -> Spawn.tick state
| Turn d -> Rules.turn d state (* use the argument order of your Rules.turn *)
| Turn d -> Rules.turn state d
| TogglePause -> (* ? *)
| Restart -> (* ? *)
| Frame -> (* ? *)
+36 -10
View File
@@ -135,13 +135,28 @@ final case class PongState(ball: Point, vel: Velocity, player: Int, cpu: Int, he
type Velocity = { Dx: int; Dy: int }
type Held = { Up: bool; Down: bool }
type Score = { Player: int; Cpu: int }
type PongStatus = Playing | Over
[<RequireQualifiedAccess>]
type PongStatus =
| Playing
| Over
type PongState =
{ Ball: Point; Vel: Velocity; Player: int; Cpu: int; Held: Held
Score: Score; Status: PongStatus; Seed: uint32; Frame: int }
{ Ball: Point
Vel: Velocity
Player: int
Cpu: int
Held: Held
Score: Score
Status: PongStatus
Seed: uint32
Frame: int }
// The court, in cells: W = 80, H = 50, PCELL = 8, PADDLE = 8, PLAYER_X = 2, CPU_X = 77, WIN = 5
```
> **F# note:** `[<RequireQualifiedAccess>]` means you must write `PongStatus.Playing`, never plain `Playing`. Snake's `Status` also has a `Playing` case (and, in step 2, Snake's `Action` has `Frame` and `Direction` has `Up`/`Down`). Without the attribute, whichever type was opened last would silently win, and `Playing` in a Pong file could mean Snake's status. Qualified access makes every use unambiguous.
The most interesting field is **`held`**. In Snake, a key press was an *instantaneous* event: pressing `ArrowUp` meant "turn up" once. In Pong, what matters is whether a key **is being held** at the moment a frame happens. The browser doesn't give us "is held"; it gives us two events, keydown and keyup. So "held" is *state* that we derive from those events, and it must live in `PongState` — not in a variable in the runtime, and certainly not by asking the browser during `update`. That's the stateful sketch's `upHeld`/`downHeld`, turned into data.
### Step 2 — Identify the events; make them data
@@ -173,7 +188,12 @@ enum PongAction:
**F#** (given)
```fsharp
type Paddle = PaddleUp | PaddleDown (* check the names in your Model.fs *)
[<RequireQualifiedAccess>]
type Paddle =
| Up
| Down
[<RequireQualifiedAccess>]
type PongAction =
| Press of Paddle
| Release of Paddle
@@ -181,6 +201,8 @@ type PongAction =
| Restart
```
In F# you write these as `Paddle.Up`, `PongAction.Press Paddle.Up`, `PongAction.Frame` — qualified, for the reason given in step 1.
And the translation from browser keys is a pure function, now with a second argument because the same key means different things going down and going up:
```
@@ -223,10 +245,12 @@ Like Snake's `app`:
### Step 7 — One runtime at the edge
Nothing to write. `js/src/pong/main.js` is one line: `runApp(pongMain, browserPlatform(document.getElementById('game'), window))`. In Scala, `scala/web/Main.scala` has a `PongMain` object; `build-web.sh` already compiles it to `web/dist/pong.js`. F# has the equivalent in `fsharp/web/`. Your m09 runtime runs a game it has never heard of. That's the reward for keeping it ignorant.
Nothing to write. `js/src/pong/main.js` is one line: `runApp(pongMain, browserPlatform(document.getElementById('game'), window))`. In Scala, `scala/web/Main.scala` has a `PongMain` object; `build-web.sh` already compiles it to `web/dist/pong.js`. F# has the equivalent in `fsharp/web/PongMain.fs`: `Runtime.runApp (BrowserPlatform.create (BrowserPlatform.gameCanvas ())) (Pong.App.pongMain ())`. Your m09 runtime runs a game it has never heard of. That's the reward for keeping it ignorant.
## Exercises
> **About test names.** The test titles quoted under "Proved by" are the JavaScript ones. The Scala and F# suites test the same things one-to-one, but their titles use each track's vocabulary: "events" instead of "inputs", `None` instead of `null`, `()` instead of `undefined`, `bind`/`choose` instead of `flatMap`/`filterMap`, `IO.map throwingF io` instead of `map(io, throwingF)`, and so on. Failing tests are reported by name when you run the module's tests; to read all titles, open `scala/test/M11Pong.test.scala` or `fsharp/tests/M11Pong.fs`. Fixture-based tests have the same case names in every track.
### Exercise 11.1 — model
**JavaScript** (`js/src/pong/model.js`; the constants `W`, `H`, `PCELL`, `PADDLE`, `PLAYER_X`, `CPU_X`, `WIN` are given)
@@ -249,7 +273,7 @@ object PongModel:
let initialPong (seed: uint32) : PongState = failwith "TODO m11"
```
**Proved by:** `the court is 80 by 50 cells of 8 pixels, with 8-cell paddles`, `initialPong puts the ball in the middle, both paddles centred, and nobody ahead`, `PongAction builds Press and Release with a key, and has Frame and Restart as plain values`.
**Proved by:** `the court is 80 by 50 cells of 8 pixels, with 8-cell paddles`, `initialPong puts the ball in the middle, both paddles centred, and nobody ahead`, `PongAction builds Press and Release with a key, and has Frame and Restart as plain values` (JS only; in Scala and F# the types are given, so there is nothing to test).
### Exercise 11.2 — rules
@@ -332,15 +356,17 @@ let FRAME_MS_PONG = 30L
let pongKey (isDown: bool) (key: string) : PongAction option = failwith "TODO m11"
let pongActions : Stream<PongAction> = (* ... *)
let pongStates (seed: uint32) (actions: Stream<PongAction>) : Stream<PongState> = failwith "TODO m11"
let pongMain : IO<Stream<IO<unit>>> = (* ... *)
let pongMain () : IO<Stream<IO<unit>>> = failwith "TODO m11"
```
In F#, `main` and `pongMain` are **functions of unit** (`pongMain ()`), not values. F# evaluates a module's top-level values when the module is first used, so a value whose definition fails (like a stub) would break every test that touches the module; a function of unit is only evaluated when called. The value it returns is still the same pure description.
**Proved by:** `fixtures/m11-pong-key.json` (21 cases, including `r-up-does-nothing` and the `constructor`/`toString` traps from Module 9), `the clock runs every 30 ms`, `pongActions turns the clock into Frames and keys into Press, Release and Restart`, `pongStates starts with the new match, then yields one state per action`, `pongMain asks for a seed, draws the new match first, and then the updated one`.
**Run and play:**
- JS: `cd js && node --test "test/m11-*.test.js"`; then `node scripts/serve.mjs` and open <http://localhost:8000/js/pong.html>
- Scala: `cd scala && ./test.sh M11`; then `./build-web.sh`, serve, open <http://localhost:8000/scala/pong.html>
- F#: `cd fsharp && ./test.sh M11`; then `./build-web.sh`, serve, open <http://localhost:8000/fsharp/pong.html>
- JS: `cd js && node --test "test/m11-*.test.js"`; then, **from the repository root**, `node scripts/serve.mjs` and open <http://localhost:8000/js/pong.html>
- Scala: `cd scala && ./test.sh M11`; then `./build-web.sh`; serve from the repository root (`node scripts/serve.mjs`) and open <http://localhost:8000/scala/pong.html>
- F#: `cd fsharp && ./test.sh M11`; then `./build-web.sh`; serve from the repository root (`node scripts/serve.mjs`) and open <http://localhost:8000/fsharp/pong.html>
Finally run the **whole** suite of your track (`./test.sh`). The purity audit now scans your Pong files too.