From 038a375027a402a3a1811b0f25e97690b04d9608 Mon Sep 17 00:00:00 2001 From: Lucas Winther Date: Thu, 29 Oct 2026 01:00:23 +0100 Subject: [PATCH] =?UTF-8?q?docs(course):=20modules=2006=E2=80=9311=20and?= =?UTF-8?q?=20method?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- course/06-rendering-as-data.md | 346 ++++++++++++++++ course/07-the-io-type.md | 696 ++++++++++++++++++++++++++++++++ course/08-event-streams.md | 505 +++++++++++++++++++++++ course/09-wiring-it-together.md | 529 ++++++++++++++++++++++++ course/10-extensions.md | 325 +++++++++++++++ course/11-capstone-pong.md | 395 ++++++++++++++++++ course/method.md | 376 +++++++++++++++++ 7 files changed, 3172 insertions(+) create mode 100644 course/06-rendering-as-data.md create mode 100644 course/07-the-io-type.md create mode 100644 course/08-event-streams.md create mode 100644 course/09-wiring-it-together.md create mode 100644 course/10-extensions.md create mode 100644 course/11-capstone-pong.md create mode 100644 course/method.md diff --git a/course/06-rendering-as-data.md b/course/06-rendering-as-data.md new file mode 100644 index 0000000..bae62bb --- /dev/null +++ b/course/06-rendering-as-data.md @@ -0,0 +1,346 @@ +# Module 6 — Rendering as data + +> **Time:** ~2 h · **You'll edit:** JS `js/src/view.js` · Scala `scala/src/View.scala` · F# `fsharp/src/View.fs` +> **Tests:** JS `cd js && node --test "test/m06-*.test.js"` · Scala `cd scala && ./test.sh M06` · F# `cd fsharp && ./test.sh M06` +> **Prerequisites:** Modules 0–5 (you have a complete, pure game: `update`, `replay`, `newGame`) + +## Why this module + +At the end of Module 5 you had a whole game of Snake that nobody can see. `replay(newGame(42), actions)` gives you a final `GameState`, and the tests tell you that it is the right one, but there is no picture yet. + +The usual way to draw a game looks like this: + +```js +// The usual, stateful way (do NOT write this in the course) +function draw(ctx, state) { + ctx.fillStyle = '#111111' + ctx.fillRect(0, 0, 400, 400) + for (const p of state.snake) { + ctx.fillStyle = '#43a047' + ctx.fillRect(p.x * 20 + 1, p.y * 20 + 1, 18, 18) + } + // ... apple, score, overlays +} +``` + +Look at what this function needs and what it gives back. It needs a canvas context `ctx` — a live object connected to a screen. It gives back nothing (`undefined`); its whole purpose is the changes it makes to `ctx`. Now ask yourself: **how would you test it?** You could open a browser, look at it, and say "looks right". You could build a fake `ctx` that records the calls. You could take a screenshot and compare pixels. All three are awkward, and all three exist only because `draw` *does* something instead of *returning* something. + +This module makes a small change with big consequences. Instead of drawing, your `view` function will **return a description of the drawing**: a list of shapes, as plain data. Checking that the picture is right becomes the same thing you have done in every module so far: compare a value with an expected value. + +``` + Module 6 Modules 7 and 9 + GameState ───────────▶ Scene (data) ───────────▶ pixels on a canvas + view (pure) an interpreter (the edge) +``` + +## Learning objectives + +By the end of this module you can: + +- **explain** why a function that returns a description of a picture is easier to test and reuse than a function that draws; +- **implement** `view : GameState → Scene` exactly to a specification, including the drawing order; +- **convert** between board coordinates (cells) and screen coordinates (pixels); +- **compare** this design with the virtual DOM used by React and the `view` function of the Elm Architecture; +- **predict** the scene for a small state by hand, before running the tests. + +## Concepts + +### A picture is a list of shapes + +What is the smallest vocabulary you need to draw Snake? Look at the game: a dark background, coloured squares for the snake and the apple, a line of text for the score, and sometimes a big message in the middle ("Paused", "Game Over"). Two kinds of shape are enough: + +- a **rectangle**: position `x, y`, size `w, h`, a `color`; +- a **text**: position `x, y`, the `text` itself, a `color`, a font `size`, and an `align`ment (`'left'` or `'center'`, meaning "x is the left edge" or "x is the middle of the text"). + +A **scene** is an ordered list of shapes. The order matters because later shapes are painted over earlier ones — this is called the [painter's algorithm](https://en.wikipedia.org/wiki/Painter%27s_algorithm). The background must come first, otherwise it would cover everything else. + +Here is how the three tracks spell a shape. In JS a shape is a plain object with a `tag`, exactly like the `Action`s of Module 2. In Scala and F# it is a tagged union that you already have in the stub file — you will not have to write these types, only use them. + +**JavaScript** + +```js +// Plain objects; no type declaration is needed. +const background = { tag: 'Rect', x: 0, y: 0, w: 400, h: 400, color: '#111111' } +const label = { tag: 'Text', x: 6, y: 18, text: 'hello', color: '#ffffff', size: 16, align: 'left' } +const scene = [background, label] // a Scene is an array of shapes, drawn in order +``` + +**Scala** + +```scala +// Already in scala/src/View.scala +enum Shape: + case Rect(x: Int, y: Int, w: Int, h: Int, color: String) + case Text(x: Int, y: Int, text: String, color: String, size: Int, align: String) + +type Scene = List[Shape] + +val scene: Scene = List(Shape.Rect(0, 0, 400, 400, "#111111"), Shape.Text(6, 18, "hello", "#ffffff", 16, "left")) +``` + +**F#** + +```fsharp +// Already in fsharp/src/View.fs +type Shape = + | Rect of x: int * y: int * w: int * h: int * color: string + | Text of x: int * y: int * text: string * color: string * size: int * align: string + +type Scene = Shape list + +let scene : Scene = [ Rect (0, 0, 400, 400, "#111111"); Text (6, 18, "hello", "#ffffff", 16, "left") ] +``` + +> **From Scala:** a `Scene` is a `List[Shape]` where `Shape` is a sum type with two cases. You would never think of a `List[Shape]` as "doing" anything; it just sits there. That is the whole point. In JS you get the same thing with an array of tagged objects; nothing stops you from mutating it, which is why the tests freeze the inputs and why you keep using the habits from Module 1. + +Notice what is *not* in a shape: no canvas, no context, no "current fill colour". In the stateful version, `ctx.fillStyle = color` sets a hidden variable that the next `fillRect` reads. In a shape, the colour travels *with* the rectangle. Every shape is complete on its own. That is the same move you made with `GameState` in Module 2: instead of state that lives somewhere and changes, a value that contains everything. + +### From cells to pixels + +The game lives on a grid of 20 × 20 **cells**; the screen is measured in **pixels**. One constant connects them: + +**JavaScript** + +```js +export const CELL = 20 // pixels per board cell +``` + +**Scala** + +```scala +val CELL: Int = 20 // View.CELL +``` + +**F#** + +```fsharp +let CELL = 20 // View.CELL +``` + +A cell at `(x, y)` covers the pixels from `x*CELL` to `x*CELL + CELL - 1` horizontally (and the same vertically). If you filled the whole cell, the snake would be one solid green blob and you could not tell its segments apart. So every square is drawn **1 pixel inside** its cell on each side: it starts at `x*CELL + 1` and is `CELL - 2` pixels wide. + +``` + pixel: 20 21 38 39 40 + │ │ │ │ │ + ▼ ▼ ▼ ▼ ▼ + ┌───┬─────────────────────────┬───┐ + │gap│ 18 px of colour │gap│ ← cell x = 1 (pixels 20..39) + └───┴─────────────────────────┴───┘ + x*CELL + 1 = 21 width CELL - 2 = 18 +``` + +The background is the full board: `width * CELL` by `height * CELL` pixels — 400 × 400 for the standard board. Take that from the state rather than hard-coding 400: the fixtures include other board sizes on purpose. + +### The specification of the scene + +Your `view` must produce exactly this list, in this order (all colours are strings like `'#111111'`): + +1. **Background:** `Rect(0, 0, width*CELL, height*CELL, '#111111')`. +2. **Apple**, only if there is one: `Rect(ax*CELL+1, ay*CELL+1, CELL-2, CELL-2, '#e53935')`. +3. **Snake**, one square per segment, in list order (head first): same position rule and size as the apple; colour `'#7cb342'` for the head (index 0) and `'#43a047'` for every other segment. +4. **Score:** `Text(6, 18, 'Score: ', '#ffffff', 16, 'left')`, e.g. `'Score: 3'`. +5. **Overlay**, depending on `status`, centred at `cx = width*CELL/2`, `cy = height*CELL/2`, all `'#ffffff'` and `'center'`-aligned: + - `Paused`: `Text(cx, cy, 'Paused', …, 32, …)` then `Text(cx, cy+30, 'Press Space to resume', …, 16, …)`; + - `GameOver`: `'Game Over'` (size 32) then `'Press R to restart'` (size 16, 30 px lower); + - `Won`: `'You Win!'` (size 32) then `'Press R to restart'` (size 16, 30 px lower); + - `Playing`: no overlay. + +Why should the specification be this exact? Because the scene is now a *value*, and values can be compared for equality. A vague specification ("draw the snake in green") cannot be tested by equality; a precise one can. In the stateful world you would accept "looks right"; here you can demand "is right". + +Predict before you go on: for `initialState(0)` (snake `[(10,10),(9,10),(8,10)]`, no apple, score 0, Playing), how many shapes does `view` return, and what are the `x` coordinates of the three snake squares? (Answer at the end of this section.) + +### A pure view is testable by equality + +Here is the test for the whole of this module, in JS (the Scala and F# suites do the same with their own fixture loaders): + +```js +const fx = loadFixture('m06-view') +fx.cases.forEach((c) => + test(`m06 ${fx.function}: ${c.name}`, () => assert.deepStrictEqual(view(...freeze(c.args)), c.expected))) +``` + +There is no canvas, no browser, no mock, no screenshot. The fixture `fixtures/m06-view.json` holds a few states and, for each, the exact list of shapes expected. Open it now and read the case `paused-overlay`: you can check by eye that it matches the specification above. That file is shared by all three tracks, so all three `view` functions must agree to the pixel. + +Compare the two worlds side by side: + +| | `draw(ctx, state)` (stateful) | `view(state)` (pure) | +|---|---|---| +| Returns | nothing | a `Scene` | +| Needs to run | a canvas | nothing | +| Test | fake `ctx`, screenshots, eyes | `deepEqual(view(s), expected)` | +| Call it twice | draws twice | same value twice | +| Reuse | only on a canvas | canvas, SVG, terminal, a test, a replay viewer… | + +That last row deserves a pause. Since a `Scene` is data, *anything* can consume it. In Module 7 you will turn a scene into an `IO` program; in Module 9 a runtime turns that into canvas calls. But you could equally write a function that turns a scene into SVG text, or into ASCII art for a terminal (see the optional exercise). The view does not know or care. This is the separation that John Hughes, in ["Why Functional Programming Matters"](https://www.cs.kent.ac.uk/people/staff/dat/miranda/whyfp90.pdf), calls *gluing*: one part produces data, another consumes it, and you can swap either side. + +### The interpreter is somebody else's job + +Something must eventually turn shapes into pixels. That something is an **interpreter**: a function that walks a description and performs it. You will meet this word a lot from now on. The plan for the next modules: + +- Module 7: a `Scene` becomes an `IO` value — still a description, now of *the act of drawing* (`drawScene`). +- Module 9: a tiny runtime interprets that `IO`, calling the canvas's `fillRect` and `fillText`. + +The canvas API itself ([MDN: Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API)) is a classic stateful API: you set `fillStyle`, `font`, `textAlign`, and then call `fillRect`/`fillText`, which read those settings. Our `Shape` was designed so that one shape maps to one such call, with all its settings attached. Look at the `fillRect(x, y, w, h)` and `fillText(text, x, y)` methods on MDN and convince yourself that every argument they need is in our shapes. + +### The same idea in React and Elm: the virtual DOM + +You have just reinvented one of the central ideas of modern web user interfaces. + +- In **React**, a component's render function does not touch the page. It returns *elements* — plain JavaScript objects that describe what should be on screen. React then compares the new description with the previous one and changes only what differs in the real page (the "DOM"). That description is called the **virtual DOM**. React's docs describe this as "render and commit": rendering computes the description, committing applies it ([React: Render and Commit](https://react.dev/learn/render-and-commit)). +- In **Elm**, every program has a function `view : Model -> Html Msg`. `Html Msg` is a value, not a side effect. The Elm runtime does the drawing ([The Elm Architecture](https://guide.elm-lang.org/architecture/)). F# has the same design in [Elmish](https://elmish.github.io/elmish/). + +Our `view : GameState → Scene` is exactly this, for a canvas instead of HTML. And because the description is data, the interpreter is free to be clever: React's diffing is only possible because the description is a value that can be compared with the previous one. Our interpreter will be simple — redraw everything every frame — but it *could* compare scenes and skip unchanged frames, without `view` ever knowing. + +*(Prediction answer: 5 shapes — background, three snake squares, the score text; no apple, no overlay. The squares' `x` values are `10*20+1 = 201`, `181`, `161`.)* + +## Exercises + +### Exercise 6.1 — `view` + +Implement `view` so that it returns the scene described in [The specification of the scene](#the-specification-of-the-scene). + +**JavaScript** (`js/src/view.js`) + +```js +export const CELL = 20 +export const view = (state) => /* Scene: an array of {tag:'Rect',...} / {tag:'Text',...} */ +``` + +**Scala** (`scala/src/View.scala`) + +```scala +object View: + val CELL: Int = 20 + def view(state: GameState): Scene = ??? +``` + +**F#** (`fsharp/src/View.fs`) + +```fsharp +module 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. + +**Watch out for:** + +- The **order** of the list is part of the answer. `deepStrictEqual` on arrays (and `==` on Scala/F# lists) compares position by position. +- The apple is absent as `null` in JS and `None` in Scala/F#. Don't emit a rectangle at `(0,0)` or at `null*20`. +- The head is the segment at **index 0**. You need the index while mapping — in JS `map`'s callback receives it as a second argument; in Scala `zipWithIndex`; in F# `List.mapi`. +- The text arguments come in the order `x, y, text, color, size, align`. In JS the order of object keys does not matter for `deepStrictEqual`, but the key *names* must be exactly `x, y, text, color, size, align`. +- Integer division: `width*CELL/2` is a whole number for even widths. In Scala and F# `/` on `Int` is already integer division; in JS it is not, but the fixture boards make the result whole anyway. +- Don't build the list with `push` (the purity audit will reject it in JS, and it is not the style of the course). Build it with spreads/concatenation of smaller lists. + +**Run:** +- JS: `cd js && node --test "test/m06-*.test.js"` +- Scala: `cd scala && ./test.sh M06` +- F#: `cd fsharp && ./test.sh M06` + +### Exercise 6.2 (optional, untested) — a second interpreter + +To feel what "the scene is data" buys you, write — in a scratch file outside `src/`, or in a REPL — a function `sceneToAscii(scene, width, height)` that turns a scene into a string of `width × height` characters: `#` for snake squares, `@` for the apple, `.` elsewhere (you can recognise squares by their colour and recover the cell as `(x - 1) / CELL`). Print `sceneToAscii(view(newGame(42)), 20, 20)`. + +You did not change `view` at all, and yet you have a new way of displaying the game. Ask yourself what you would have had to do to get this from the stateful `draw(ctx, state)`. + +## Hints + +
Exercise 6.1 — nudge + +Split the problem the same way the specification does: five small pieces (background, apple, snake, score, overlay), each of which is a *list* of zero or more shapes. Then the scene is those lists concatenated in order. A missing apple is then an empty list, and `Playing` gives an empty overlay list. Can you write each piece as its own small function or constant? + +
+ +
Exercise 6.1 — stronger hint + +Write one helper that turns a board cell and a colour into the inset square: it takes a point and a colour and returns `Rect(p.x*CELL+1, p.y*CELL+1, CELL-2, CELL-2, color)`. Use it for the apple and for every snake segment. + +For the overlay, write a helper that takes the big message and the small message and returns the two centred texts. Then the overlay is a `switch`/`match` on `status` that picks which messages to pass, with `Playing` returning an empty list. + +
+ +
Exercise 6.1 — near-solution (try the others first) + +**JavaScript** + +```js +const square = (p, color) => ({ tag: 'Rect', x: /* ? */, y: /* ? */, w: CELL - 2, h: CELL - 2, color }) +const text = (x, y, s, size, align) => ({ tag: 'Text', x, y, text: s, color: '#ffffff', size, align }) + +const overlay = (state) => { + const cx = /* ? */, cy = /* ? */ + const two = (big, small) => [text(cx, cy, big, 32, 'center'), text(/* ? */)] + switch (state.status) { + case 'Paused': return two('Paused', 'Press Space to resume') + /* GameOver, Won ... */ + default: return [] + } +} + +export const view = (state) => [ + { tag: 'Rect', x: 0, y: 0, w: /* ? */, h: /* ? */, color: '#111111' }, + ...(state.apple === null ? [] : [/* ? */]), + ...state.snake.map((p, i) => square(p, /* head or body colour? */)), + text(6, 18, /* ? */, 16, 'left'), + ...overlay(state), +] +``` + +**Scala** + +```scala +private def square(p: Point, color: String): Shape = Shape.Rect(/* ? */, /* ? */, CELL - 2, CELL - 2, color) + +def view(state: GameState): Scene = + val apple = state.apple.toList.map(/* ? */) + val snake = state.snake.zipWithIndex.map { case (p, i) => /* ? */ } + val overlay: List[Shape] = state.status match + case Status.Paused => /* two texts */ + /* ... */ + case Status.Playing => Nil + List(Shape.Rect(0, 0, /* ? */, /* ? */, "#111111")) ++ apple ++ snake ++ List(/* score */) ++ overlay +``` + +**F#** + +```fsharp +let private square (p: Point) color = Rect (p.X * CELL + 1, (* ? *), CELL - 2, CELL - 2, color) + +let view (state: GameState) : Scene = + let apple = state.Apple |> Option.map (fun a -> (* ? *)) |> Option.toList + let snake = state.Snake |> List.mapi (fun i p -> (* ? *)) + let overlay = + match state.Status with + | Paused -> (* two texts *) + | (* ... *) + | Playing -> [] + [ Rect (0, 0, (* ? *), (* ? *), "#111111") ] @ apple @ snake @ [ (* score *) ] @ overlay +``` + +
+ +## Check your understanding + +1. `view` takes a state and returns a value. Name two things that were *hidden inputs* or *hidden outputs* of the stateful `draw(ctx, state)` and say where each went in the new design. +2. Predict: what is the first shape of `view(s)` for a 10 × 6 board? What is the last shape if `s.status` is `GameOver`? +3. Suppose you swapped steps 1 and 3 of the specification (snake first, then background). Would the tests notice? Would a player notice? Which one is the more important observation? +4. Why does the specification put the colour inside every shape, instead of having a "set colour" shape followed by rectangles? What would go wrong in a test, or in a second interpreter (like the ASCII one), with a "set colour" shape? +5. React re-renders the whole description and then *diffs* it against the previous one. Our runtime will redraw every shape every frame. Which part of our code would have to change to add diffing — `view`, or the interpreter? Why is that the right answer? +6. Is `view` total — does it return a scene for *every* possible `GameState`? Can you find a state for which it returns something surprising (think about a snake with zero segments, or an apple outside the board)? +7. Explain to a classmate, in two sentences, why "returning a description" makes rendering testable without a screen. + +## In production + +- **React** components return a description (elements) instead of touching the page; React's reconciler compares descriptions and applies the minimal changes ([React: Render and Commit](https://react.dev/learn/render-and-commit), [Preserving and resetting state](https://react.dev/learn/preserving-and-resetting-state) explains how the tree of descriptions is compared). +- **Elm** forces this design: `view : Model -> Html Msg` is the only way to show anything, and the runtime owns the screen ([The Elm Architecture](https://guide.elm-lang.org/architecture/)). **Elmish** brings the same model to F#, usually with React as the renderer ([Elmish](https://elmish.github.io/elmish/)). +- **Snapshot tests** in tools like Jest store a serialised description of what a component renders and fail when it changes ([Jest: Snapshot Testing](https://jestjs.io/docs/snapshot-testing)) — this is only possible because the render output is data. +- **Game engines** often build a *display list* or *command buffer* each frame — a list of draw commands — that a separate renderer submits to the GPU. Same separation, much more performance pressure. + +## Further reading + +- [The Elm Architecture](https://guide.elm-lang.org/architecture/) — the shortest description of model / update / view; you now have the first and the last, and you will recognise all three by Module 9. +- [React: Render and Commit](https://react.dev/learn/render-and-commit) — how a mainstream UI library separates describing from doing. +- [MDN: Canvas API](https://developer.mozilla.org/en-US/docs/Web/API/Canvas_API) — the stateful API our shapes are designed to map onto. +- [Painter's algorithm (Wikipedia)](https://en.wikipedia.org/wiki/Painter%27s_algorithm) — why the order of a scene matters. +- [John Hughes, "Why Functional Programming Matters"](https://www.cs.kent.ac.uk/people/staff/dat/miranda/whyfp90.pdf) — the "glue" argument: producing data and consuming it separately is what makes programs modular. diff --git a/course/07-the-io-type.md b/course/07-the-io-type.md new file mode 100644 index 0000000..a427ec0 --- /dev/null +++ b/course/07-the-io-type.md @@ -0,0 +1,696 @@ +# Module 7 — The IO type + +> **Time:** ~5 h (take a break halfway; this is the deepest module of the course) · **You'll edit:** JS `js/src/io.js` · Scala `scala/src/IO.scala` · F# `fsharp/src/IO.fs` +> **Tests:** JS `cd js && node --test "test/m07-*.test.js"` · Scala `cd scala && ./test.sh M07` · F# `cd fsharp && ./test.sh M07` +> **Prerequisites:** Modules 0–6, especially Module 4 (the state-passing pattern, `pure`/`map`/`flatMap` on `Random`) and Module 6 (`Scene`) + +## Why this module + +Here is a question that sounds like a contradiction: **what does it mean for a program that draws on the screen to be pure?** + +A pure function, as you saw in Module 1, gives the same result for the same arguments and does nothing else. Drawing *is* "something else". If `drawSquare()` paints a square, then calling it twice paints twice; you can't replace the call with its result (`undefined`) without changing what the program does. Asking the browser for a random seed is worse: the same call gives a different answer each time. So it seems a game must be impure somewhere in the middle, wherever it draws or asks the outside world for something. + +Module 6 already showed the way out for one half of the problem. `view` doesn't draw; it returns a `Scene`, a *description* of a drawing. This module takes that idea to its conclusion: **every effect becomes a value that describes it**. "Draw this rectangle" becomes a value. "Ask for a random seed, and then, with the answer, do this" becomes a value. A whole program becomes a value. Building those values is pure. Something, at the very edge, still has to *perform* the description — but that will be one small function (Module 9), and everything you write before it stays pure and testable. + +The type of such descriptions is called **`IO`**. You will build it from scratch, understand why it has exactly the shape it has, write a pure interpreter for it so you can test programs without a screen, and check that it obeys the three laws that make it a **monad**. + +## Learning objectives + +By the end of this module you can: + +- **explain** the difference between performing an effect and describing it, and why descriptions keep a program referentially transparent; +- **explain** why an `IO` needs a *continuation* `k` and cannot be a plain list of operations; +- **implement** `pure`, `effect`, `map`, `flatMap`, `sequence`, `draw`, `randomSeed`, `drawScene` and a pure interpreter `trace`; +- **state** the three monad laws and **explain** how the tests check them using an interpreter; +- **predict** when a function given to `map`/`flatMap` runs: when the program is built, or when it is interpreted; +- **compare** our `IO` with Haskell's `IO`, cats-effect, ZIO and free monads. + +## Concepts + +### 1. Performing versus describing + +Recall referential transparency from Module 1: an expression is referentially transparent when you can replace it by its value without changing the program. Try it on these two snippets: + +```js +const x = Math.random() +const pair1 = [x, x] + +const pair2 = [Math.random(), Math.random()] +``` + +If `Math.random()` were a value you could substitute, `pair1` and `pair2` would be the same. They are not: `pair1` has two equal numbers, `pair2` almost surely two different ones. `Math.random()` is not an expression that *has* a value; it is an *action* that *produces* one when performed. + +Now compare a different design. Suppose `randomSeed` is not an action but a **value that describes the action** "ask the outside world for a seed": + +```js +const randomSeed = { tag: 'Effect', op: { tag: 'RandomSeed' }, /* ... we'll see what goes here ... */ } +const program1 = [randomSeed, randomSeed] +``` + +This `randomSeed` is a plain object. Writing it twice or naming it once makes no difference at all — you can substitute freely. It does nothing until someone *interprets* it. That is the trick of the whole module, and it is also, essentially, how Haskell has been pure since the 1990s ([A Gentle Introduction to Haskell: Input/Output](https://www.haskell.org/tutorial/io.html)). + +You saw the same move twice already: + +| Module | The "doing" version | The "describing" version | +|---|---|---| +| 4 | `Math.random()` mutates a hidden seed | `Random = seed → [A, seed]`, a function you can pass around | +| 6 | `draw(ctx, state)` paints | `view(state)` returns a `Scene` | +| 7 | `ctx.fillRect(...)`, `crypto.getRandomValues(...)` | `IO`, a value describing a program that produces an `A` | + +### 2. First attempt: a program is a list of operations + +Let us design the description step by step. The simplest idea: a program is a list of **operations** (we'll call them `Op`s), and an interpreter performs them one after another. We need two operations for the whole course: + +**JavaScript** + +```js +// An Op is plain data: +// { tag: 'Draw', shape } draw a Rect or Text from Module 6 (answer: nothing, undefined) +// { tag: 'RandomSeed' } ask the outside world for a seed (answer: a uint32 number) +``` + +**Scala** + +```scala +// Already in scala/src/IO.scala +enum Op: + case Draw(shape: Shape) + case RandomSeed + +enum OpResult: // what the outside world answers + case Done // ... to a Draw + case Seed(value: Long) // ... to a RandomSeed +``` + +**F#** + +```fsharp +// Already in fsharp/src/IO.fs +type Op = + | Draw of Shape + | RandomSeed + +type OpResult = + | Done + | Seed of uint32 +``` + +> **From Scala:** notice that Scala and F# need an explicit `OpResult` type, because an interpreter must hand back *some* typed answer for any `Op`. JS is untyped, so the answer to `Draw` is just `undefined` and the answer to `RandomSeed` is just a number. + +A list-of-ops program and its interpreter would look like this (illustration only, not part of the exercises): + +```js +// A program as a list of ops, and an interpreter that performs them. +const program = [{ tag: 'Draw', shape: background }, { tag: 'Draw', shape: score }] + +const runAll = (ops, platform) => ops.forEach((op) => perform(op, platform)) +``` + +For drawing a scene, this is enough: `drawScene` would be `scene.map(shape => ({tag:'Draw', shape}))`. But now try to write this program: + +> Ask for a random seed. Then start a new game with that seed, and draw it. + +The shapes to draw depend on the seed, and the seed only exists *after* the first operation has been performed. A list must be complete before anybody performs it, so there is nowhere to put "and then, *with the answer*, decide what to do next". The list-of-ops design can describe programs whose steps are fixed in advance, and nothing else. + +### 3. Second attempt: an operation and a continuation + +The fix is to pair each operation with **a function that receives the answer and returns the rest of the program**: + +``` +Effect( op = RandomSeed, + k = seed => ) +``` + +That function is called the **continuation**, traditionally named `k`. "Continuation" just means "what to do next"; you have used them every time you passed a callback (`button.onclick = () => ...`), and the idea has a long history under the name [continuation-passing style](https://en.wikipedia.org/wiki/Continuation-passing_style). + +When the program is finished there is no operation left, only a final value. So a program is one of two things: + +- **`Pure(value)`** — done; the program's result is `value`; +- **`Effect(op, k)`** — perform `op`; give its answer to `k`; `k` returns the **rest of the program** (another `IO`). + +That's the whole type. Here it is in the three tracks (this definition is given to you in the stub files): + +**JavaScript** + +```js +// IO = +// { tag: 'Pure', value } value : A +// { tag: 'Effect', op, k } op : Op, k : (answer) => IO +``` + +**Scala** + +```scala +enum IO[+A]: + case Pure(a: A) + case Effect(op: Op, k: OpResult => IO[A]) +``` + +**F#** + +```fsharp +type IO<'a> = + | Pure of 'a + | Effect of Op * (OpResult -> IO<'a>) +``` + +> **From Scala:** this is a recursive algebraic data type, like `List` (`Nil | Cons(head, tail)`). The difference is that the "tail" is hidden behind a function: you can only get the rest of the program by supplying an answer. A list is a chain of values; an `IO` is a chain of *questions*. + +Let us write the program from the previous section *by hand*, with no helpers at all. (JS shown; it is the same tree in every language.) + +```js +// "Ask for a seed; then draw a square whose x depends on the seed; then finish with the seed." +const program = { + tag: 'Effect', + op: { tag: 'RandomSeed' }, + k: (seed) => ({ + tag: 'Effect', + op: { tag: 'Draw', shape: { tag: 'Rect', x: seed % 20, y: 0, w: 1, h: 1, color: '#ffffff' } }, + k: (_) => ({ tag: 'Pure', value: seed }), + }), +} +``` + +Drawn as a picture, the program is a chain where each link waits for an answer: + +``` + Effect(RandomSeed) ──answer: seed──▶ Effect(Draw(Rect at x = seed % 20)) ──answer: (none)──▶ Pure(seed) +``` + +Stop and check two things: + +1. **Building `program` performed nothing.** No seed was asked for; no square was drawn. `program` is an object, and the functions inside it have not been called. +2. **The second operation depends on the first answer.** That dependency is written as an ordinary JS function. This is the expressive power the list did not have. + +### 4. Interpreting a program by hand + +An **interpreter** is a function that walks this structure: if it sees `Pure`, it's done; if it sees `Effect(op, k)`, it performs `op` somehow, calls `k` with the answer, and continues with whatever `k` returns. + +The interesting phrase is "performs `op` *somehow*". Who decides what "performing" means? The interpreter does. That gives us the freedom to write two very different interpreters for the same programs: + +- **`trace(io, seed)`** (this module): a *pure* interpreter for tests. It doesn't draw anything. It answers every `RandomSeed` with the fixed `seed` you give it, answers every `Draw` with "done", and **records** the list of operations it saw. It returns `{ result, ops }`. +- **`runIO(io, platform)`** (Module 9): the real interpreter, which calls the canvas and the browser's random generator. + +Let us run `trace(program, 7)` by hand. Fill in the table yourself before reading the last row: + +| Step | Current program | Operation performed | Answer given | `ops` so far | +|---|---|---|---|---| +| 1 | `Effect(RandomSeed, k1)` | `RandomSeed` | `7` | `[RandomSeed]` | +| 2 | `k1(7)` = `Effect(Draw(Rect x=7), k2)` | `Draw(Rect x=7)` | `undefined` (Scala/F#: `Done`) | `[RandomSeed, Draw(Rect x=7)]` | +| 3 | `k2(undefined)` = `Pure(7)` | — | — | finished: `{ result: 7, ops: [RandomSeed, Draw(Rect x=7)] }` | + +Now ask: *is `trace` a pure function?* Same `io`, same `seed` → same `{result, ops}`, every time; nothing outside is touched. Yes. So programs that "use randomness and draw on the screen" can be tested with `deepStrictEqual`, the same way you have tested everything so far. This is the single most important consequence of the module. + +> **From Scala:** an interpreter is a fold over a data structure, like `foldRight` over a `List`, except that to get from one node to the next you must supply an answer. You will write `trace` by **recursion** (no loops, no mutation): the ops of `Effect(op, k)` are `op` followed by the ops of whatever `k(answer)` traces to. + +### 5. Writing programs comfortably: `pure` and `effect` + +Nobody wants to write nested objects like `program` above. We need small building blocks and ways to glue them. The two simplest: + +- **`pure(a)`** — the program that does nothing and produces `a`. It is just `Pure(a)`. +- **`effect(op)`** — the program that performs exactly one `op` and produces its answer. What should its continuation be? It must take the answer and return "the rest of the program", and the rest of the program is "nothing more; the result is the answer". Which of our building blocks is exactly "take a value, return a finished program with that value"? (The docs in the stub say it outright: `Effect(op, pure)`.) + +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.) + +### 6. `map`: changing the result without running anything + +Suppose you have `randomSeed : IO` and you want `IO`: "ask for a seed, and the result is `newGame(seed)`". That's `map(randomSeed, newGame)`. + +What must `map(io, f)` do? Think case by case, and think about **when** `f` may run: + +- If `io` is `Pure(a)`, the value is right there. The result is a finished program whose value is `f(a)`. +- If `io` is `Effect(op, k)`, there is *no value yet*. It will only exist when an interpreter performs `op` and calls `k`. So `map` can't call `f` now. The new program must perform the same `op`, and its continuation must take the answer, compute the rest of the original program with `k`, and then **apply `f` to whatever that rest eventually produces**. That last part is itself a `map` — on a smaller program. + +That second case is the heart of the matter: `map` doesn't transform a value; it builds a *new description* that will transform the value once it exists. The tests check this directly: + +```js +test('m07 building map(io, throwingF) does not throw', ...) // building must not call f +test('m07 tracing map(io, throwingF) runs f and throws', ...) // interpreting must call f +``` + +> **From Scala:** you know `Option.map` and `List.map`: they apply `f` to the value(s) inside. `Future.map` is the closer analogue: it can't apply `f` until the value arrives, so it registers `f` to run later. Our `map` does the same thing with plain data instead of threads. + +**Language note.** JS functions are data-first, `map(io, f)`. Scala uses a method, `io.map(f)`. F# uses a data-last module function, `IO.map f io`, so it works with the pipe: `io |> IO.map f`. + +### 7. `flatMap`: "and then" + +`map` lets the *result* depend on an earlier answer. But we need more: the *next operations* must depend on an earlier answer ("ask for a seed, *then draw the game made from it*"). That's `flatMap(io, f)`, where `f` returns not a value but another program: + +``` +flatMap(io: IO, f: A → IO) : IO +``` + +Read it as "run `io`; with its result `a`, continue with the program `f(a)`". It's the semicolon of our little language. Again, case by case: + +- `Pure(a)`: the first program is already finished with `a`, so the combined program *is* `f(a)`. +- `Effect(op, k)`: perform the same `op`; the new continuation takes the answer, gets the rest of the original program from `k`, and then **continues that rest with `f`**. Again, the "and then continue with `f`" is itself a `flatMap` on a smaller program. + +A picture helps. `flatMap` takes the chain of `io` and grafts `f` onto its end — at the place where `io` would have said `Pure(a)`: + +``` +io: Effect(op1) ─▶ Effect(op2) ─▶ Pure(a) +flatMap(io, f): Effect(op1) ─▶ Effect(op2) ─▶ f(a) = Effect(op3) ─▶ Pure(b) +``` + +Two observations to make you comfortable: + +1. Once you have `flatMap` and `pure`, you get `map` for free: "transform the result with `f`" is the same as "and then finish with `f(a)`". Can you write `map` in one line using `flatMap` and `pure`? (This is a classic exercise; it is also a nice way to check your `flatMap`.) +2. You have seen this exact pair before. In Module 4, `Random` had `pure`, `map` and `flatMap` with the same shapes and the same meaning ("then, with the value, continue"). This is not a coincidence; it's a pattern with a name, which you will meet properly in section 10. + +> **From Scala:** `flatMap` is the method that `for`-comprehensions desugar to, and you've used it on `List`, `Option`, `Either` and `Future`. On `List` it means "for each element, a list; concatenate"; on `Option` "if there is a value, continue; otherwise stop"; on `IO` it means "when the answer arrives, continue with this program". + +**Language note.** In F# this function is called **`bind`** (`IO.bind f io`), which is the traditional name in ML and Haskell (`>>=`). Same function, different name. + +### 8. `sequence` and `drawScene` + +To draw a whole scene we need to run many `IO` programs one after another. `sequence(ios)` builds that combined program, left to right; `sequence([])` is a program that does nothing and produces unit. Then + +``` +drawScene(scene) = sequence(scene.map(draw)) +``` + +and every `Scene` from Module 6 becomes a program that draws it. `trace(drawScene(view(s)), 0).ops` is then exactly `view(s)` wrapped in `Draw` ops — which is what Module 9's `render` test checks. + +How should you build `sequence`? It's a fold: combine programs pairwise with `flatMap` ("run this one, then the rest"). There are two ways to fold, and they produce differently *shaped* programs: + +``` +right-nested: flatMap(io1, _ => flatMap(io2, _ => flatMap(io3, _ => pure()))) +left-nested: flatMap(flatMap(flatMap(pure(), _ => io1), _ => io2), _ => io3) +``` + +Both mean the same thing (the associativity law below guarantees it), and both pass this module's tests. But they don't cost the same. Keep the question "which one should I build?" in mind; section 12 answers it, and Module 9 has a test that cares. + +### 9. When does `f` run? An honest note about eagerness + +You should now be able to predict exactly when the function you pass to `flatMap` runs. Look at the `Pure` case again: `flatMap(Pure(a), f)` is `f(a)` — and our implementation computes `f(a)` **right away, while building the program**, not later during interpretation. + +Is that a problem? In a pure world, no. If `f` is pure, calling it now or later gives the same result and has no other effect; nobody can tell the difference. Building `flatMap(pure(3), n => draw(...))` doesn't draw anything either way, because `f` only *returns a description* of drawing. + +There are exactly two ways to notice the difference, and you should know them: + +- **Exceptions.** `flatMap(pure(1), _ => { throw new Error('boom') })` throws while *building*, whereas `flatMap(randomSeed, _ => { throw ... })` only throws when *interpreted*. +- **Non-termination or very expensive work.** If `f` loops forever or takes a minute, building the program hangs or takes a minute. + +Both are, in a sense, impurities in `f`, and our `f`s won't have them. Production libraries do care: cats-effect, ZIO and Haskell's `IO` add a case like `Suspend(() => io)` / `Delay` so that even the first step can be postponed until interpretation. Our tiny `IO` doesn't need it, and it is good to understand *why* it doesn't. + +### 10. The monad laws + +A type with `pure` and `flatMap` that satisfies three equations is called a **monad**. The equations are not bureaucracy; each one is a refactoring you want to be allowed to do without changing what your program does. + +Write `≡` for "behaves the same". For any value `a`, any program `m`, and any functions `f`, `g` that return programs: + +1. **Left identity:** `flatMap(pure(a), f) ≡ f(a)` + *"Wrapping a value and immediately continuing with it is the same as continuing with it directly."* +2. **Right identity:** `flatMap(m, pure) ≡ m` + *"Continuing with 'return it unchanged' adds nothing."* +3. **Associativity:** `flatMap(flatMap(m, f), g) ≡ flatMap(m, x => flatMap(f(x), g))` + *"It doesn't matter how you group a chain of 'and then's."* This is what lets you extract any part of a sequence into a helper function. + +In Scala and F# syntax: + +**Scala** + +```scala +IO.pure(a).flatMap(f) ≡ f(a) +m.flatMap(IO.pure) ≡ m +m.flatMap(f).flatMap(g) ≡ m.flatMap(x => f(x).flatMap(g)) +``` + +**F#** + +```fsharp +IO.pure a |> IO.bind f ≡ f a +m |> IO.bind IO.pure ≡ m +m |> IO.bind f |> IO.bind g ≡ m |> IO.bind (fun x -> f x |> IO.bind g) +``` + +**How can a test check "behaves the same"?** You can't compare two `IO` values with `==`: they contain functions, and functions can't be compared. But you *can* compare what they do under an interpreter! Two programs that produce the same result and perform the same operations, under the same answers, are indistinguishable to anyone using them. So the tests check: + +```js +assert.deepStrictEqual(trace(flatMap(pure(a), f), 5), trace(f(a), 5)) +``` + +for a few programs (`pure(4)`, `randomSeed`, and a draw followed by `randomSeed`). This is called **observational equality**: equal as far as any observer (here: the interpreter) can tell. It is also a lesson in why the pure interpreter is valuable: without it, you could not even *state* these tests. + +> **From Scala:** `Option`, `List`, `Either` and `Future` all have `pure`-like constructors (`Some`, `List(_)`, `Right`, `Future.successful`) and `flatMap`, and they all obey these laws. The `for` syntax relies on them: the compiler rewrites `for` into nested `flatMap`s in one particular grouping, and associativity is what makes that grouping irrelevant. + +### 11. Syntax that hides the plumbing + +Nested `flatMap`s become hard to read quickly. Scala and F# offer syntax for exactly this pattern; JS doesn't (but see the note). + +**Scala** — because `IO` has `map` and `flatMap`, it works in a `for`-comprehension with no extra work: + +```scala +val program: IO[Long] = + for + seed <- IO.randomSeed + _ <- IO.drawScene(View.view(Spawn.newGame(seed))) + yield seed +// desugars to: IO.randomSeed.flatMap(seed => IO.drawScene(...).map(_ => seed)) +``` + +**F#** — F# has *computation expressions*. You enable them by writing a small builder object that tells the compiler which functions mean "bind" and "return". This is optional and not tested; try it after the exercises: + +```fsharp +type IOBuilder() = + member _.Bind(io, f) = IO.bind f io + member _.Return(a) = IO.pure a + member _.ReturnFrom(io) = io + +let io = IOBuilder() + +let program : IO = + io { + let! seed = IO.randomSeed + do! IO.drawScene (View.view (Spawn.newGame seed)) + return seed + } +``` + +**JavaScript** has no such syntax. (Generators can imitate it — libraries such as redux-saga do this — but that's beyond our scope.) We'll keep our JS programs small enough that plain `flatMap` reads well. + +Read the two snippets above and say what they would *do* if interpreted by `trace(…, 42)`: what is the result, and what is the first element of `ops`? + +### 12. Stack safety (why the shape of the tree matters) + +Our `IO` is made of nested functions, and calling nested functions uses the call stack. Two places can blow the stack if a program is very long (tens of thousands of operations): + +- **The interpreter.** `trace` is written by recursion, one level per operation. That's fine for tests (a few hundred ops). The real interpreter in Module 9, `runIO`, must handle a full game for hours and is written as a **loop** instead (or, in Scala, a `@tailrec` function, which the compiler turns into a loop). +- **The continuations themselves.** Look again at what `flatMap` does to an `Effect`: its new continuation calls the old `k` and then calls `flatMap` again on the result. If you nest `flatMap` on the *left* — `flatMap(flatMap(flatMap(…)))` — then the first continuation, when called, must unwind every one of those levels at once before it can return the next step. A left-nested chain of 100 000 `flatMap`s overflows the stack *even if the interpreter is a loop*. + +That is the answer to the question from section 8: **build `sequence` right-nested** (fold from the right), so each continuation does a constant amount of work before handing back the next `Effect`. Module 9 has a test that runs `sequence` of 100 000 draws through `runIO`; a left-nested `sequence` will fail it with a stack overflow. + +Production libraries solve this once and for all with a technique called **trampolining**: they represent `flatMap` itself as a data constructor (`FlatMap(io, f)`) and the interpreter re-associates left-nested chains on the fly, in a loop, so no shape of program can overflow the stack. Rúnar Bjarnason's paper ["Stackless Scala With Free Monads"](http://blog.higher-order.com/assets/trampolines.pdf) explains it beautifully; see also [Trampoline (computing)](https://en.wikipedia.org/wiki/Trampoline_%28computing%29). We keep the simpler representation and the rule "fold right". + +### 13. What we have built, and its family name + +Step back and look at the type: a program is either *done*, or *one instruction plus a function from the instruction's answer to the rest of the program*. The instructions (`Op`) are a separate, ordinary data type; the `IO` structure doesn't care what they are. This shape is known as a **free monad** (more precisely, a close cousin called the "operational" or "freer" monad): it gives you `pure`/`flatMap` and the monad laws *for free* on top of any set of instructions, and leaves the meaning of the instructions entirely to the interpreter. Gabriel Gonzalez's ["You Could Have Invented Free Monads"](https://www.haskellforall.com/2012/06/you-could-have-invented-free-monads.html) tells the same story from Haskell. + +Consequences you will use in the rest of the course: + +- **Adding an effect is adding an `Op`.** Want to play a sound, or save a high score? Add `{tag:'PlaySound', name}` or `{tag:'Save', key, value}` and teach the interpreters what to do with it (you'll think through exactly this in Module 10). +- **Many interpreters.** `trace` for tests, `runIO` for the browser, and you could write a third that logs every op, or one that answers `RandomSeed` from a recorded file to replay a bug. + +## Exercises + +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` + +**JavaScript** + +```js +export const pure = (a) => /* IO */ +export const effect = (op) => /* IO: Effect(op, pure) */ +``` + +**Scala** + +```scala +object IO: + def pure[A](a: A): IO[A] = ??? + def effect(op: Op): IO[OpResult] = ??? +``` + +**F#** + +```fsharp +module IO = + let pure (a: 'a) : IO<'a> = failwith "TODO m07" + let effect (op: Op) : IO = 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)`). + +### Exercise 7.2 — `map` and `flatMap` + +**JavaScript** + +```js +export const map = (io, f) => /* IO */ +export const flatMap = (io, f) => /* IO */ +``` + +**Scala** (methods on the `IO` enum) + +```scala +enum IO[+A]: + ... + def map[B](f: A => B): IO[B] = ??? + def flatMap[B](f: A => IO[B]): IO[B] = ??? +``` + +**F#** (data-last; `bind` is F#'s name for `flatMap`) + +```fsharp +let map (f: 'a -> 'b) (io: IO<'a>) : IO<'b> = failwith "TODO m07" +let bind (f: 'a -> IO<'b>) (io: IO<'a>) : IO<'b> = failwith "TODO m07" +``` + +**Proved by:** `map transforms the result without adding ops`, `flatMap passes the result of the first IO to the second`, `building map(io, throwingF) does not throw`, `tracing map(io, throwingF) runs f and throws`, and the nine monad-law tests (`left identity holds for …`, `right identity holds for …`, `associativity holds for …`). The law tests need `trace` (7.4) to run, so expect them to stay red until then. + +**Watch out for:** calling `f` in the `Effect` case while building. If your `map` ever calls `f` before an interpreter supplies an answer, the "does not throw" test catches it. In Scala, `flatMap` on the enum is a method, so you will `this match { ... }`. + +### Exercise 7.3 — `sequence`, `draw`, `randomSeed`, `drawScene` + +**JavaScript** + +```js +export const sequence = (ios) => /* IO, left to right */ +export const draw = (shape) => /* IO */ +export const randomSeed = /* IO: a value, not a function */ +export const drawScene = (scene) => /* sequence(scene.map(draw)) */ +``` + +**Scala** + +```scala +def sequence(ios: List[IO[Unit]]): IO[Unit] = ??? +def draw(shape: Shape): IO[Unit] = ??? +lazy val randomSeed: IO[Long] = ??? +def drawScene(scene: Scene): IO[Unit] = ??? +``` + +**F#** + +```fsharp +let sequence (ios: IO list) : IO = failwith "TODO m07" +let draw (shape: Shape) : IO = failwith "TODO m07" +let randomSeed : IO = (* a value *) +let drawScene (scene: Scene) : IO = failwith "TODO m07" +``` + +**Proved by:** `randomSeed is answered with the seed given to trace`, `randomSeed records a RandomSeed op`, `draw records a Draw op and produces undefined` (Scala/F#: `()`), `drawScene draws the shapes in order`, `drawScene of an empty scene has no ops`, `sequence of nothing has no ops`, `sequence runs effects left to right`. + +**Watch out for:** building `sequence` *right-nested* (section 12). The m07 tests won't notice a left-nested one, but Module 9's 100 000-draw test will. + +### Exercise 7.4 — `trace`, the pure interpreter + +**JavaScript** + +```js +export const trace = (io, seed) => /* { result, ops } */ +``` + +**Scala** + +```scala +final case class Traced[A](result: A, ops: List[Op]) +def trace[A](io: IO[A], seed: Long): Traced[A] = ??? +``` + +**F#** + +```fsharp +type Traced<'a> = { Result: 'a; Ops: Op list } +let trace (seed: uint32) (io: IO<'a>) : Traced<'a> = failwith "TODO m07" +``` + +Answer every `RandomSeed` with the seed (`Seed(seed)` in Scala/F#), every `Draw` with unit (`Done`), and collect the ops in the order they were performed. Use recursion, not mutation. + +**Proved by:** `trace of pure(3) has result 3 and no ops`, and every other test in the module (they all observe programs through `trace`). + +**Run (all of Module 7):** +- JS: `cd js && node --test "test/m07-*.test.js"` +- Scala: `cd scala && ./test.sh M07` +- F#: `cd fsharp && ./test.sh M07` + +## Hints + +
Exercise 7.1 — nudge + +`pure` builds the "finished" case. `effect` builds the other case: which `op`? And which function, among those you've already written, takes an answer and returns "a finished program whose result is that answer"? + +
+ +
Exercise 7.1 — near-solution (try the others first) + +```js +export const pure = (a) => ({ tag: 'Pure', value: /* ? */ }) +export const effect = (op) => ({ tag: 'Effect', op, k: /* a function you just wrote */ }) +``` + +In Scala, `IO.Pure(a)` and `IO.Effect(op, /* ? */)`; in F#, `Pure a` and `Effect (op, (* ? *))`. + +
+ +
Exercise 7.2 — nudge + +Do `flatMap` first; `map` is easier afterwards. Split on the two cases of `io`. In the `Pure` case you have the value: what do you do with `f`? In the `Effect` case you have `op` and `k` but no value: what must the *new* `Effect` contain? Its `op` is the same. Its continuation receives an answer `r` … what can you compute from `r` with `k`, and what must happen to that afterwards? + +
+ +
Exercise 7.2 — stronger hint + +For `Effect(op, k)`, `k(r)` is "the rest of the original program". You want "the rest of the original program, *and then* `f`". You're writing the function that means "and then `f`" right now — call it recursively on `k(r)`. The recursion happens *inside* the new continuation, so it only runs when an interpreter calls it. + +For `map`, use `flatMap` and `pure`: "and then finish with `f` of the value". + +
+ +
Exercise 7.2 — near-solution (try the others first) + +**JavaScript** + +```js +export const flatMap = (io, f) => + io.tag === 'Pure' + ? /* ? */ + : { tag: 'Effect', op: io.op, k: (r) => /* ? uses io.k, r, flatMap and f */ } + +export const map = (io, f) => flatMap(io, (a) => /* ? */) +``` + +**Scala** + +```scala +def flatMap[B](f: A => IO[B]): IO[B] = this match + case Pure(a) => /* ? */ + case Effect(op, k) => Effect(op, r => /* ? */) + +def map[B](f: A => B): IO[B] = flatMap(a => /* ? */) +``` + +**F#** + +```fsharp +let rec bind (f: 'a -> IO<'b>) (io: IO<'a>) : IO<'b> = + match io with + | Pure a -> (* ? *) + | Effect (op, k) -> Effect (op, fun r -> (* ? *)) + +let map (f: 'a -> 'b) (io: IO<'a>) : IO<'b> = io |> bind (fun a -> (* ? *)) +``` + +
+ +
Exercise 7.3 — nudge + +`draw` and `randomSeed` are one `effect` each (plus, in Scala/F#, a `map` to turn the `OpResult` into the right type). `sequence` is a fold over the list that starts from "the program that does nothing" and glues programs with `flatMap`, ignoring the unit results. Which fold keeps the tree right-nested (section 12)? + +
+ +
Exercise 7.3 — near-solution (try the others first) + +**JavaScript** + +```js +export const sequence = (ios) => ios.reduceRight((rest, io) => flatMap(/* ? */, () => /* ? */), /* ? */) +export const draw = (shape) => effect({ tag: 'Draw', shape }) +export const randomSeed = /* ? one effect */ +``` + +**Scala** + +```scala +def sequence(ios: List[IO[Unit]]): IO[Unit] = ios.foldRight(/* ? */)((io, rest) => /* ? */) +lazy val randomSeed: IO[Long] = effect(Op.RandomSeed).map { + case OpResult.Seed(v) => /* ? */ + case OpResult.Done => throw new IllegalStateException("RandomSeed must be answered with a Seed") +} +``` + +**F#** + +```fsharp +let sequence (ios: IO list) : IO = List.foldBack (fun io rest -> (* ? *)) ios (* ? *) +``` + +
+ +
Exercise 7.4 — nudge + +`trace` of a `Pure` is easy: a result and no ops. For an `Effect(op, k)`, which answer do you give `k`? Once you have `k(answer)`, you have a smaller program; trace it (recursion!) and combine its result with `op`. + +
+ +
Exercise 7.4 — near-solution (try the others first) + +**JavaScript** + +```js +const answer = (op, seed) => /* seed for RandomSeed, undefined for Draw */ + +export const trace = (io, seed) => { + if (io.tag === 'Pure') return { result: io.value, ops: [] } + const rest = trace(/* ? */, seed) + return { result: rest.result, ops: [/* ? */, ...rest.ops] } +} +``` + +**Scala** (recursion that is not in tail position is fine here; `@tailrec` with an accumulator is a nice extra) + +```scala +def trace[A](io: IO[A], seed: Long): Traced[A] = io match + case IO.Pure(a) => Traced(a, Nil) + case IO.Effect(op, k) => + val rest = trace(k(/* ? answer for op */), seed) + Traced(/* ? */, /* ? */) +``` + +**F#** + +```fsharp +let rec trace (seed: uint32) (io: IO<'a>) : Traced<'a> = + match io with + | Pure a -> { Result = a; Ops = [] } + | Effect (op, k) -> + let rest = trace seed (k ((* ? answer for op *))) + { rest with Ops = (* ? *) } +``` + +
+ +## Check your understanding + +1. In one sentence each: what is the difference between `Math.random()` and `randomSeed`? Between `ctx.fillRect(...)` and `draw(rect)`? +2. Why can't an `IO` be a plain list of `Op`s? Give a concrete program from the Snake game that a list could not express. +3. Predict `trace(flatMap(randomSeed, (s) => map(draw(r), () => s * 2)), 21)`: what are `result` and `ops`? +4. Predict: does `const p = map(randomSeed, (s) => { throw new Error('x') })` throw? Does `trace(p, 0)`? Does `flatMap(pure(1), () => { throw new Error('x') })`? Explain each answer using the structure of `flatMap`. +5. Why do the tests compare `trace(...)` results instead of comparing `IO` values directly? What is "observational equality", and why is it the right notion of "the same program" here? +6. State the associativity law in words. Which everyday refactoring (think "extract function") would be unsafe if it didn't hold? +7. You build `sequence` with a *left* fold and every m07 test passes. What breaks later, and why doesn't the loop in the interpreter save you? +8. To add sound to the game you'd add `{tag:'PlaySound', name}`. List every place in the code that has to change. Which place does *not* have to change at all, and why is that the point of the design? + +## In production + +- **Haskell's `IO`** is the original: `main :: IO ()` is a value that the runtime performs; every effectful function returns an `IO` description. Haskell's `do`-notation is the same sugar as Scala's `for` and F#'s computation expressions ([Haskell wiki: IO inside](https://wiki.haskell.org/IO_inside)). +- **cats-effect** (Scala) gives you `IO[A]` with `pure`, `map`, `flatMap`, `IO.delay`/`IO.blocking` to suspend side-effecting code, and a runtime that interprets it on a fiber scheduler with stack safety, cancellation and concurrency ([cats-effect](https://typelevel.org/cats-effect/)). The book [*Functional Programming in Scala*](https://www.manning.com/books/functional-programming-in-scala-second-edition), chapter 13, builds an `IO` very much like ours and then makes it stack-safe. +- **ZIO** (Scala) uses `ZIO[R, E, A]`: a description of a program that needs an environment `R`, may fail with `E`, or succeeds with `A` ([ZIO](https://zio.dev/)). The `R` is a typed version of our `Platform`. +- **Free monads** are used directly to separate a program's instructions from their interpretation, e.g. to run the same business logic against a real database and an in-memory test double ([Gabriel Gonzalez, "You Could Have Invented Free Monads"](https://www.haskellforall.com/2012/06/you-could-have-invented-free-monads.html)). +- **Elm commands** (`Cmd msg`) are effect descriptions returned by `update`; the Elm runtime performs them and feeds results back as messages ([Elm guide: Commands and Subscriptions](https://guide.elm-lang.org/effects/)). In F#, **Elmish** has the same `Cmd` type ([Elmish](https://elmish.github.io/elmish/)). + +## Further reading + +- [Gabriel Gonzalez, "You Could Have Invented Free Monads"](https://www.haskellforall.com/2012/06/you-could-have-invented-free-monads.html) — the same derivation as this module, in Haskell; read it to see the idea in its native habitat. +- [Rúnar Bjarnason, "Stackless Scala With Free Monads"](http://blog.higher-order.com/assets/trampolines.pdf) — how to make this structure stack-safe for arbitrarily long programs. +- [Haskell wiki: Monad laws](https://wiki.haskell.org/Monad_laws) — the three laws with examples and the intuition behind each. +- [Mostly Adequate Guide to Functional Programming, chapters 8–9](https://mostly-adequate.gitbook.io/mostly-adequate-guide/) — functors, `IO` and monads, in JavaScript. +- [Scala: for-comprehensions](https://docs.scala-lang.org/tour/for-comprehensions.html) and [F#: Computation expressions](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/computation-expressions) — the syntax for writing `flatMap` chains in each language. +- [Referential transparency (Wikipedia)](https://en.wikipedia.org/wiki/Referential_transparency) — the property this whole module protects. +- [Continuation-passing style (Wikipedia)](https://en.wikipedia.org/wiki/Continuation-passing_style) — where the `k` comes from. diff --git a/course/08-event-streams.md b/course/08-event-streams.md new file mode 100644 index 0000000..4cda507 --- /dev/null +++ b/course/08-event-streams.md @@ -0,0 +1,505 @@ +# Module 8 — Event streams + +> **Time:** ~4 h · **You'll edit:** JS `js/src/stream.js` · Scala `scala/src/Stream.scala` · F# `fsharp/src/Stream.fs` +> **Tests:** JS `cd js && node --test "test/m08-*.test.js"` · Scala `cd scala && ./test.sh M08` · F# `cd fsharp && ./test.sh M08` +> **Prerequisites:** Modules 0–7 (especially Module 5, "a game is a fold", and Module 7, "effects are values") + +## Why this module + +In Module 5 you showed that a whole game is a fold: `replay(newGame(seed), actions)`. That works beautifully when you have the list of actions *in advance* — in a test, or for a replay. But a real player doesn't hand you a list. Actions arrive **one at a time, over time**: the clock ticks, a key goes down, another key, more ticks. Where do they come from, and how do they become `Action`s? + +The usual answer is callbacks and a mutable variable: + +```js +// The usual, stateful way (do NOT write this in the course) +let state = newGame(seed) +setInterval(() => { state = update(state, Action.Tick); draw(ctx, state) }, 150) +window.addEventListener('keydown', (e) => { + const action = keyToAction(e.key) + if (action) { state = update(state, action); draw(ctx, state) } +}) +``` + +This works, and it is where most games start. But look at what we've lost. The logic "every 150 ms is a tick; arrow keys are turns; fold them into states; draw each state" is scattered across two callbacks that share a hidden `let`. You can't test the wiring without a browser and a real clock. You can't ask "what would happen if these five events arrived?" without actually making them happen. + +This module does for *events over time* what Module 7 did for effects: it turns them into **values**. A **stream** will be a pure description of how to react to events. You'll build a small library of streams and combinators (`map`, `filter`, `merge`, `scan`, …) from scratch, and you'll test any wiring by feeding it a list of events — no browser, no clock, no waiting. + +## Learning objectives + +By the end of this module you can: + +- **explain** a stream as a pure automaton (a Mealy machine): `step : Event → (outputs, nextStream)`; +- **implement** sources (`keyDowns`, `keyUps`, `interval`) and combinators (`map`, `filter`, `filterMap`/`choose`, `merge`, `scan`, `startWith`) and a pure `run`; +- **explain** where a stream "remembers" things when there are no variables, and **trace** `interval`'s `last` through a sequence of events; +- **predict** the output of a small stream pipeline for a given list of events; +- **compare** this design with RxJS observables (push-based, stateful subscriptions) and with classic functional reactive programming. + +## Concepts + +### 1. What a program receives from the outside world + +Our game only reacts to three kinds of event: + +- **time passing**: "it is now `t` milliseconds since the program started"; +- **a key going down**: which key (`'ArrowUp'`, `'w'`, `' '` …); +- **a key going up**: which key. + +As in every module so far, the first step is to make them data: + +**JavaScript** + +```js +// An input event is a plain object: +// { tag: 'Time', t } t: milliseconds since start +// { tag: 'KeyDown', key } key: the browser's KeyboardEvent.key, e.g. 'ArrowUp' +// { tag: 'KeyUp', key } +``` + +**Scala** + +```scala +// Already in scala/src/Stream.scala +enum Event: + case Time(t: Long) + case KeyDown(key: String) + case KeyUp(key: String) +``` + +**F#** + +```fsharp +// Already in fsharp/src/Stream.fs +type Event = + | Time of int64 + | KeyDown of string + | KeyUp of string +``` + +**Naming note.** The JS doc comments call this type `Input`; Scala and F# call it `Event`, because their m09 module is already named `Input` and the two names would clash. Same thing. + +Notice that time arrives as an event too. The program never *asks* for the time (`Date.now()` is banned by the purity audit); the outside world *tells* it what time it is, the same way it tells it about keys. Hidden time becomes an explicit input. Keep that in mind; it's the most important design decision in this module. + +### 2. A stream is a machine you feed one event at a time + +What is the smallest useful description of "something that reacts to events"? When an event arrives, it should be able to: + +1. produce **zero or more outputs** (a key press produces one turn; a clock event may produce no tick at all, or several); +2. **change how it will react to the next event** (an interval needs to remember when it last fired). + +Without variables, "change" must mean "give back a *new* machine". That gives the whole definition: + +``` +Stream
= { step : Event → [ A[], Stream ] } +``` + +Feed one event to `step`; get back the outputs it caused *and the stream to use for the next event*. + +**JavaScript** + +```js +// Stream = { step: (event) => [outputs: A[], next: Stream] } +``` + +**Scala** + +```scala +final case class Stream[A](step: Event => (List[A], Stream[A])) +``` + +**F#** + +```fsharp +type Stream<'a> = Stream of (Event -> 'a list * Stream<'a>) +``` + +This is a well-known object in computer science: a **Mealy machine**, a state machine whose output depends on its current state and its input ([Mealy machine](https://en.wikipedia.org/wiki/Mealy_machine)). The usual textbook definition has a set of states and a transition table; ours is the same idea where "the current state" is hidden inside the closure of `step`, and "the next state" is the next stream itself. In Haskell this is also called an *automaton arrow* or a "Mealy" type ([Justin Le, "Intro to Machines & Arrows"](https://blog.jle.im/entry/intro-to-machines-arrows-part-1-stream-and.html) is a gentle introduction). + +> **From Scala:** this is a recursive case class whose only field is a function. Think of it as the dual of a lazy list: a `LazyList` *gives* you the next element when you ask; a `Stream` *takes* an event when you push one, and gives you some outputs plus the rest of itself. + +Let's write one by hand to get a feel for it — not one of the exercises, but a cousin: a stream that counts key presses and emits the running count. + +```js +// counter(n): a stream that has seen n key presses so far. +const counter = (n) => ({ + step: (ev) => + ev.tag === 'KeyDown' + ? [[n + 1], counter(n + 1)] // output the new count; the next machine remembers n + 1 + : [[], counter(n)], // no output; nothing to remember changes +}) +const presses = counter(0) +``` + +Step it by hand: + +``` +presses = counter(0) +presses.step(KeyDown 'a') → [ [1], counter(1) ] +counter(1).step(Time 40) → [ [], counter(1) ] +counter(1).step(KeyDown 'b')→ [ [2], counter(2) ] +``` + +Where is the count stored? Not in a variable. It's the argument `n` of the function that built the current stream, captured in the closure of `step`. Each step returns a new stream that captures the new count. **The state lives in the next stream**, not in a cell that gets overwritten. + +### 3. No hidden state: run the same stream twice + +To test a stream, we need a function that feeds it a list of events and collects everything it emits. That's `run`: + +``` +run(s, events) → all outputs, in order +``` + +It is a **fold** over the events — exactly like `replay` in Module 5, except that each step also produces outputs, which you append. The accumulator is the pair `(outputs so far, current stream)`. + +Because `step` never mutates anything, a stream *value* is not "used up" by running it. The test suite checks precisely this: + +```js +test('m08 running the same stream value twice gives the same outputs', () => { + const s = scan(merge(interval(10), map(keyDowns, () => 100)), (a, x) => a + x, 0) + const xs = inputs(time(25), down('a'), time(40)) + assert.deepStrictEqual(run(s, xs), run(s, xs)) + assert.deepStrictEqual(run(s, xs), [10, 30, 130, 160, 200]) +}) +``` + +Take a minute and work out by hand why the second list is `[10, 30, 130, 160, 200]` (you'll be able to once you've read the sections on `interval`, `merge` and `scan`). Then think about why the *first* assertion is remarkable. In most event libraries, a stream object holds internal state that changes as events flow; running it a second time would continue where it left off, or not work at all. Here, `s` is a value like the number 3: you can use it as often as you like. + +### 4. Sources: where outputs come from + +**`keyDowns`** emits the `key` of every `KeyDown` event and ignores everything else. **`keyUps`** does the same for `KeyUp`. These two remember nothing, so the "next stream" can be the very same stream. (Ask yourself: is that safe? Why can a stateless stream return *itself*?) + +**`interval(ms)`** is the interesting one. It turns the clock into regular beats: on `Time t`, it emits every multiple of `ms` that has been *crossed* since the last time event — every `k*ms` with `last < k*ms ≤ t` — in ascending order. It starts with `last = 0` and afterwards `last` becomes `t`. The values it emits are those multiples themselves. + +Why "every multiple crossed" rather than "one beat per time event"? Because the outside world does not call us on a perfect schedule. The browser reports time about 60 times per second, but if the tab was in the background for two seconds, the next time event might jump from `t = 1000` to `t = 3000`. The game should then catch up on all the ticks it missed, not silently skip them. (This idea, separating game time from the rate at which the outside world calls you, is the classic "fixed timestep" pattern; see [Game Programming Patterns: Game Loop](https://gameprogrammingpatterns.com/game-loop.html).) + +Here is how `last` threads through a sequence of events for `interval(10)`: + +``` +event last before multiples k*10 in (last, t] outputs last after +────────── ─────────── ───────────────────────────── ───────── ────────── +Time 5 0 (0, 5] → none [] 5 +Time 10 5 (5, 10] → 10 [10] 10 +KeyDown 'a' 10 (ignored) [] 10 +Time 35 10 (10, 35] → 20, 30 [20, 30] 35 +Time 38 35 (35, 38] → none [] 38 +Time 41 38 (38, 41] → 40 [40] 41 +``` + +And as streams: each row hands its "last after" to the stream for the next row. + +``` +interval(10) = from(10, last=0) ──Time 5──▶ from(10, 5) ──Time 10──▶ from(10, 10) ──KeyDown──▶ from(10, 10) ──Time 35──▶ from(10, 35) ... + [] [10] [] [20, 30] +``` + +The stub docs tell you how: "write a private helper that carries `last`". `interval(ms)` itself is just that helper started at `last = 0`. Before you code it, answer: what are the outputs of `run(interval(10), [Time 35])`? (Answer: `[10, 20, 30]`; one big jump catches up on everything.) + +> **From Scala:** the helper is the familiar "loop with an accumulator" from recursive functions, e.g. `def go(acc: Int, xs: List[Int])`. Here the accumulator is carried from step to step *between events*, by building the next stream with the new value. + +### 5. Combinators: building streams from streams + +Each combinator takes a stream (or two) and gives a new stream. They all follow the same pattern, and once you see it, you'll be able to write any of them: + +> **On each event:** step the inner stream(s); transform the outputs; and return, as the next stream, **the same combinator applied to the inner stream(s)' next stream(s)**. + +That last part is what people get wrong at first. If `map(s, f)` returned `s.next` as its next stream, the mapping would be forgotten after one event. The next stream must again be "mapped", so it's `map(next, f)`. + +Here is what each one does. Pay attention to the order of outputs; the tests do. + +- **`map(s, f)`**: apply `f` to every output. +- **`filter(s, p)`**: keep the outputs for which `p` is true. +- **`filterMap(s, f)`**: apply `f`, which returns either a value or "nothing", and keep only the values. In JS "nothing" is `null`; in Scala `f` returns an `Option` (`Some(b)` / `None`); in F# it's an option, and the function is called **`choose`** like `List.choose`. It's the combination of `map` and `filter` you'll need for `keyToAction` in Module 9 (most keys mean nothing). +- **`merge(a, b)`**: *both* streams step on *every* event; the outputs of `a` come before the outputs of `b`. Both next streams are kept. +- **`scan(s, f, z)`**: fold the outputs into an accumulator starting at `z`, and emit **each new accumulator** (not `z` itself). If one event causes three outputs, `scan` emits three accumulators. The latest accumulator must be remembered for the next event — where? (Same answer as for `counter` and `interval`.) +- **`startWith(s, v)`**: emit `v` once, *before* the outputs of the first step; afterwards behave exactly like the inner stream. + +A small worked example to check your understanding of `merge` and ordering: + +``` +merge(interval(10), map(keyDowns, () => -1)) events: Time 10, KeyDown 'a', Time 20 + Time 10 → interval: [10] keys: [] → [10] + KeyDown 'a' → interval: [] keys: [-1] → [-1] + Time 20 → interval: [20] keys: [] → [20] +run → [10, -1, 20] +``` + +And `scan`, accumulating `+`: + +``` +scan(keyDowns, (acc, k) => acc + k, '') events: KeyDown 'a', KeyDown 'b' + KeyDown 'a' → outputs ['a'] → acc '' + 'a' = 'a' → emits ['a']; next remembers 'a' + KeyDown 'b' → outputs ['b'] → acc 'a' + 'b' = 'ab' → emits ['ab']; next remembers 'ab' +run → ['a', 'ab'] +``` + +> **From Scala:** these are the methods you know from `List` (`map`, `filter`, `collect`/`flatMap` with `Option`, `scanLeft`), lifted to "lists that haven't happened yet". `scan` is `scanLeft` without the initial element. In Scala they are **methods**: `s.map(f)`, `s.merge(other)`, and `s.scan(z)(f)` with the initial value in its own parameter list, like `foldLeft`. In F# they are data-last module functions: `Stream.map f s`, `Stream.scan f z s`, `Stream.startWith v s`, `Stream.run events s`. + +### 6. `startWith` and the "nothing happens until an event arrives" rule + +`startWith` has one subtle property worth understanding now, because Module 9 depends on it. A stream can only emit when an event arrives; there is no "at time zero, before anything" moment. So `startWith(s, v)` emits `v` **at the first event**, whatever it is, together with (before) that event's own outputs: + +``` +run(startWith(keyDowns, 'init'), [Time 1, KeyDown 'a']) → ['init', 'a'] (emitted at Time 1) +run(startWith(keyDowns, 'init'), []) → [] (no event, no output) +``` + +In the game, `startWith` will put the initial game state at the front of the stream of states, so the very first clock event draws the board before anything has moved. + +### 7. Two ways to think about streams: pull and push + +There are two classic implementation strategies for event streams, and it's worth knowing which one we chose and why. + +**Push (observables, RxJS).** A source *pushes* values to subscribers by calling their callbacks. `fromEvent(window, 'keydown').pipe(map(...), filter(...)).subscribe(cb)` builds a chain of objects, each of which holds a list of subscribers and its own internal state (a `scan` operator holds its accumulator in a field). Subscribing *starts* the machinery; you get a `Subscription` to stop it. It's powerful and practical — this is how [RxJS](https://rxjs.dev/) works — but the stream objects are stateful: you can't run "the same" stream twice and get a fresh run without care ("hot" vs "cold" observables), and testing needs special schedulers that fake time. + +**Pure automata (our design).** A stream is a value with a `step` function. Nobody subscribes; the *caller* pushes one event in and gets outputs back, plus the next stream. All the state is in the returned values. The price is that someone must hold "the current stream" between events and call `step` — that's the one mutable variable you'll write in the runtime in Module 9. The reward is that everything else is pure, and testing is `run(s, listOfEvents)`. + +| | RxJS observable | Our `Stream` | +|---|---|---| +| What is a stream? | an object with subscribers and internal state | a pure function `Event → (outputs, next)` | +| Where is a `scan`'s accumulator? | a field inside the operator, mutated | captured in the *next* stream value | +| Run it twice | depends (hot/cold); usually not "fresh" | always the same result | +| Test it | marble tests with a virtual scheduler | `run(s, events)` and `deepEqual` | +| Who drives it? | the source, via callbacks | the runtime, one `step` at a time | + +### 8. Where this comes from: functional reactive programming + +The idea of describing interactive programs as pure values that change over time is called **functional reactive programming (FRP)**. It was introduced by Conal Elliott and Paul Hudak in ["Functional Reactive Animation"](http://conal.net/papers/icfp97/) (ICFP 1997), which modelled animations as *behaviours* (values that vary continuously with time, `Time → a`) and *events* (streams of timestamped occurrences). Our streams are a discrete, simplified version of FRP events: we only see time at the moments the outside world reports it. + +Later work in Haskell, such as [Yampa](https://wiki.haskell.org/Yampa) (used for games, including a Haskell Quake clone), represents reactive programs as *signal functions*, which are exactly automata stepped by the runtime — very close to what you're building here. Elm started as an FRP language and later moved to the simpler model of *subscriptions* that deliver messages to `update` ([Elm guide: Time](https://guide.elm-lang.org/effects/time.html)) — which, as you'll see in Module 9, is what our stream of actions folded with `scan` amounts to. + +## Exercises + +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` + +**JavaScript** + +```js +export const run = (s, inputs) => /* A[]: every output, in order */ +``` + +**Scala** + +```scala +def run(events: List[Event]): List[A] = ??? // a method on Stream[A]: s.run(events) +``` + +**F#** + +```fsharp +let run (events: Event list) (Stream step: Stream<'a>) : 'a list = failwith "TODO m08" +``` + +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` + +**JavaScript** + +```js +export const keyDowns = /* Stream */ +export const keyUps = /* Stream */ +export const interval = (ms) => /* Stream */ +``` + +**Scala** (in `object Stream`) + +```scala +lazy val keyDowns: Stream[String] = ??? +lazy val keyUps: Stream[String] = ??? +def interval(ms: Long): Stream[Long] = ??? +``` + +**F#** + +```fsharp +let keyDowns : Stream = (* ... *) +let keyUps : Stream = (* ... *) +let interval (ms: int64) : Stream = failwith "TODO m08" +``` + +**Proved by:** `keyDowns emits keys of KeyDown inputs only`, `keyUps emits keys of KeyUp inputs only`, `interval emits each multiple once as time passes`, `interval emits several multiples after one big time jump`, `interval ignores key inputs`. + +**Watch out for:** a stream that refers to itself (`keyDowns`'s next stream is `keyDowns`). In JS a `const` object can mention itself inside a function body, because the body only runs later. In Scala that's why these are `lazy val`s. In F# a value can't refer to itself directly; write a small recursive function (`let rec`) that builds the stream and apply it. For `interval`, the private helper takes `ms` and `last`. Counting the multiples: the first multiple strictly after `last` is `(floor(last / ms) + 1) * ms`, and the last one not after `t` is `floor(t / ms) * ms`. + +### Exercise 8.3 — `map`, `filter`, `filterMap` + +**JavaScript** + +```js +export const map = (s, f) => /* Stream */ +export const filter = (s, p) => /* Stream */ +export const filterMap = (s, f) => /* Stream; f returns a value or null */ +``` + +**Scala** (methods) + +```scala +def map[B](f: A => B): Stream[B] = ??? +def filter(p: A => Boolean): Stream[A] = ??? +def filterMap[B](f: A => Option[B]): Stream[B] = ??? +``` + +**F#** (`choose` is F#'s name for `filterMap`) + +```fsharp +let rec map (f: 'a -> 'b) (s: Stream<'a>) : Stream<'b> = failwith "TODO m08" +let rec filter (p: 'a -> bool) (s: Stream<'a>) : Stream<'a> = failwith "TODO m08" +let choose (f: 'a -> 'b option) (s: Stream<'a>) : Stream<'b> = failwith "TODO m08" +``` + +**Proved by:** `map transforms every output`, `filter keeps outputs satisfying the predicate`, `filterMap transforms outputs and drops nulls` (Scala/F#: `... drops Nones`). + +### Exercise 8.4 — `merge`, `scan`, `startWith` + +**JavaScript** + +```js +export const merge = (a, b) => /* Stream */ +export const scan = (s, f, z) => /* Stream; f(acc, output) */ +export const startWith = (s, v) => /* Stream */ +``` + +**Scala** (methods) + +```scala +def merge(other: Stream[A]): Stream[A] = ??? +def scan[B](z: B)(f: (B, A) => B): Stream[B] = ??? +def startWith(v: A): Stream[A] = ??? +``` + +**F#** + +```fsharp +let rec merge (a: Stream<'a>) (b: Stream<'a>) : Stream<'a> = failwith "TODO m08" +let rec scan (f: 'b -> 'a -> 'b) (z: 'b) (s: Stream<'a>) : Stream<'b> = failwith "TODO m08" +let startWith (v: 'a) (s: Stream<'a>) : Stream<'a> = failwith "TODO m08" +``` + +**Proved by:** `merge puts outputs of the first stream before the second at the same input`, `merge interleaves sources by input order`, `scan emits each new accumulator, not the initial value`, `scan passes the output to the folding function`, `startWith emits the value once, before the outputs of the first step`, `startWith emits at the first input even if that step has no outputs`, `startWith emits nothing when no input ever arrives`, and `running the same stream value twice gives the same outputs`. + +**Watch out for:** in `scan`, one event can produce several outputs, so you fold *those* outputs too, emitting every intermediate accumulator, and the next stream remembers the *last* accumulator (or the old one if there were no outputs). In `startWith`, only the first step is special — what should the next stream be so that `v` is never emitted again? + +**Run (all of Module 8):** +- JS: `cd js && node --test "test/m08-*.test.js"` +- Scala: `cd scala && ./test.sh M08` +- F#: `cd fsharp && ./test.sh M08` + +## Hints + +
Exercise 8.1 — nudge + +`run` is `replay` from Module 5 with outputs: fold over the events with an accumulator that holds two things, the outputs so far and the current stream. At the end, return only the outputs. + +
+ +
Exercise 8.1 — near-solution (try the others first) + +```js +export const run = (s, inputs) => + inputs.reduce( + ([outs, stream], i) => { + const [out, next] = /* ? */ + return [/* ? */, next] + }, + [[], s], + )[0] +``` + +Scala: `events.foldLeft((List.empty[A], this)) { case ((outs, s), e) => ... }._1`. F#: `List.fold` with a tuple accumulator, then `fst`. + +
+ +
Exercise 8.2 — nudge + +`keyDowns` looks at the event's tag: one output for `KeyDown`, none otherwise; next stream: itself. For `interval`, write `from(ms, last)` returning a stream; on `Time t` it emits the multiples in `(last, t]` and returns `from(ms, t)`; on keys it emits nothing and returns a stream that still remembers the same `last`. + +
+ +
Exercise 8.2 — stronger hint + +To list the multiples without a loop: compute `first = floor(last / ms) + 1` and `count = max(0, floor(t / ms) - first + 1)`; the outputs are `(first + n) * ms` for `n` from `0` to `count - 1`. In JS `Array.from({ length: count }, (_, n) => ...)`; in Scala `(first to floor(t/ms)).map(_ * ms).toList` (Long arithmetic); in F# a list expression `[ first .. t / ms ] |> List.map (fun k -> k * ms)` (integer division on `int64` already floors for non-negative numbers). + +
+ +
Exercise 8.3 / 8.4 — nudge + +All combinators have the same shape: step the inner stream(s), compute the outputs, and return the **same combinator** around the inner next stream(s). Write `map` first and copy its shape. `filterMap` can be built from `map` and `filter` without writing a `step` at all. + +
+ +
Exercise 8.4 — stronger hint (scan and startWith) + +For `scan`: from the inner outputs `[o1, o2, o3]` and accumulator `z`, you need `[f(z,o1), f(f(z,o1),o2), …]` — that's Scala's `scanLeft` without its first element, which you can also write as a `reduce` that builds the list of accumulators. The next stream is `scan(next, f, lastAcc)` where `lastAcc` is the last emitted accumulator, or `z` if nothing was emitted. + +For `startWith`: on the first step, step the inner stream, put `v` in front of its outputs, and return the inner **next** stream unchanged, without wrapping it again. + +
+ +
Exercise 8.3 / 8.4 — near-solution (try the others first) + +**JavaScript** + +```js +export const map = (s, f) => ({ + step: (i) => { + const [out, next] = s.step(i) + return [/* ? */, map(/* ? */, f)] + }, +}) + +export const merge = (a, b) => ({ + step: (i) => { + const [outA, nextA] = a.step(i) + const [outB, nextB] = /* ? */ + return [/* ? */, /* ? */] + }, +}) +``` + +**Scala** + +```scala +def map[B](f: A => B): Stream[B] = Stream { e => + val (out, next) = step(e) + (/* ? */, next.map(f)) +} +``` + +**F#** + +```fsharp +let rec map (f: 'a -> 'b) (Stream step) : Stream<'b> = + Stream (fun e -> + let out, next = step e + ((* ? *), map f next)) +``` + +
+ +## Check your understanding + +1. Where does `interval` keep `last`? Where does `scan` keep its accumulator? Neither uses a variable; answer precisely. +2. Predict `run(interval(100), [Time 50, Time 250, KeyDown 'x', Time 300, Time 300])`. Why does the second `Time 300` emit nothing? +3. Predict `run(startWith(scan(keyDowns, (n, _) => n + 1, 0), 0), [Time 5, KeyDown 'a', KeyDown 'b'])`. (This exact shape, `startWith(scan(...))`, is how Module 9 builds the stream of game states.) +4. Why does `map(s, f)` have to return `map(next, f)` and not just `next`? Write down what `run` would produce with the wrong version on two key presses. +5. The test "running the same stream value twice gives the same outputs" would fail for a typical RxJS pipeline with a `scan`. Explain why in terms of where the accumulator lives. +6. `merge(a, b)` steps *both* streams on *every* event. What would go wrong with an `interval` inside `b` if `merge` only stepped `b` on events that `a` ignored? +7. The browser reports time about 60 times per second, and the snake ticks every 150 ms. Using `interval`, explain what happens if the browser stops calling us for 1 second (say, the tab was hidden) and then resumes. Is that what you want for this game? +8. Our streams never ask what time it is. Why is that important for testing? Where *will* the time come from? + +## In production + +- **RxJS** is the most widely used stream library in JavaScript: `fromEvent`, `interval`, `map`, `filter`, `merge`, `scan`, `startWith` all exist with the same meaning as ours, but as push-based observables with subscriptions and internal state ([RxJS](https://rxjs.dev/), [operators](https://rxjs.dev/guide/operators)). +- **fs2** (Scala) builds pure, pull-based streams on top of cats-effect: a `Stream[IO, A]` is a description that does nothing until compiled and run, and can be run any number of times ([fs2](https://fs2.io/)). It's the industrial version of our "a stream is a value" idea. +- **Elm subscriptions** turn the clock and the keyboard into messages delivered to `update` ([Elm guide: Time](https://guide.elm-lang.org/effects/time.html)); **Elmish** offers the same for F# ([Elmish](https://elmish.github.io/elmish/)). +- **Yampa** and other arrowized-FRP libraries in Haskell represent reactive systems as signal functions stepped by a driver — the same automaton idea, used for games and robotics ([Yampa](https://wiki.haskell.org/Yampa)). + +## Further reading + +- [Conal Elliott & Paul Hudak, "Functional Reactive Animation"](http://conal.net/papers/icfp97/) — the paper that started FRP; read sections 1–2 for the vision of interaction as pure values over time. +- [Mealy machine (Wikipedia)](https://en.wikipedia.org/wiki/Mealy_machine) — the formal definition of what each of our streams is. +- [Justin Le, "Intro to Machines & Arrows"](https://blog.jle.im/entry/intro-to-machines-arrows-part-1-stream-and.html) — builds the same `step`-returns-next-machine type in Haskell and shows how far it goes. +- [Functional reactive programming (Wikipedia)](https://en.wikipedia.org/wiki/Functional_reactive_programming) — an overview of the family of ideas and implementations. +- [Robert Nystrom, Game Programming Patterns: Game Loop](https://gameprogrammingpatterns.com/game-loop.html) — why games decouple game time from the rate at which the outside world calls them; the reason `interval` catches up. +- [RxJS: Overview](https://rxjs.dev/guide/overview) — the push-based alternative, to compare with what you built. diff --git a/course/09-wiring-it-together.md b/course/09-wiring-it-together.md new file mode 100644 index 0000000..a7157a4 --- /dev/null +++ b/course/09-wiring-it-together.md @@ -0,0 +1,529 @@ +# Module 9 — Wiring it together + +> **Time:** ~4 h · **You'll edit:** JS `js/src/input.js`, `js/src/app.js`, `js/src/runtime.js`, `js/src/browser.js` · Scala `scala/src/Input.scala`, `scala/src/App.scala`, `scala/src/Runtime.scala` · F# `fsharp/src/Input.fs`, `fsharp/src/App.fs`, `fsharp/src/Runtime.fs` +> **Tests:** JS `cd js && node --test "test/m09-*.test.js"` · Scala `cd scala && ./test.sh M09` · F# `cd fsharp && ./test.sh M09` +> **Prerequisites:** Modules 0–8. You need working `update` (m05), `newGame` (m04), `view` (m06), `IO` (m07) and streams (m08). + +## Why this module + +You now have every piece of a game, and none of them does anything: + +- `update(state, action)` says how the game changes (Module 5); +- `view(state)` describes a picture (Module 6); +- `IO` describes effects such as drawing and asking for a seed (Module 7); +- `Stream` describes how to react to time and keys (Module 8). + +In this module you connect them into **one value that is the whole program**, and then write the **one small function** that brings that value to life in a browser. At the end of the module, Snake is playable. + +The connection has a striking shape. Try to read this type out loud: + +``` +main : IO>> +``` + +"`main` is an effect (ask for a random seed) that produces a stream (react to time and keys) of effects (draw this frame)." Or, in one sentence that sums up the course so far: + +> **A program is an effect that produces a stream of effects.** + +Everything in that sentence is a value you can test with `trace` and `run`. The only thing that is *not* a value is the runtime that performs it, and you'll write it yourself and see how small it is: one loop, three callbacks and **one mutable variable**. + +## Learning objectives + +By the end of this module you can: + +- **implement** `keyToAction`, the action stream, the state stream (`startWith` + `scan`), `render` and `main` from the pieces of Modules 4–8; +- **explain** the type `IO>>` layer by layer, and why `main` is a value; +- **implement** an interpreter `runIO` as a loop and a driver `runApp` that holds the only mutable variable of the program; +- **explain** how a `Platform` interface lets the runtime be tested with a fake instead of a browser; +- **run** the game in a browser in your track, and **trace** one key press from the keyboard to the pixels. + +## Concepts + +### 1. From keys to actions + +The browser tells us which key went down as a string (`KeyboardEvent.key`: `'ArrowUp'`, `'w'`, `'W'`, `'x'` …). The game speaks in `Action`s. The translation is a pure function: + +- `'ArrowUp'`, `'w'`, `'W'` → `Turn Up`; `'ArrowDown'`, `'s'`, `'S'` → `Turn Down`; `'ArrowLeft'`, `'a'`, `'A'` → `Turn Left`; `'ArrowRight'`, `'d'`, `'D'` → `Turn Right`; +- any other key → *nothing* (`null` in JS, `None` in Scala/F#). + +It returns "maybe an action" because most keys mean nothing to the game. That's exactly the shape `filterMap` (F#: `choose`) from Module 8 wants. + +A small JS trap is tested on purpose: if you implement the table as an object lookup, `table['constructor']` and `table['toString']` are **not** `undefined` — every JS object inherits those from `Object.prototype`. The fixture cases `constructor-is-not-a-key` and `toString-is-not-a-key` catch it. A `switch`, a `Map`, or an object created with `Object.create(null)` avoids the problem. (Scala and F# pattern matches don't have this issue.) + +### 2. From events to actions: one stream of everything that can happen + +What can happen to the game? A tick every 150 ms, and the actions of recognised key presses. As streams: + +``` +ticks = map(interval(TICK_MS), _ => Tick) Stream +turns = filterMap(keyDowns, keyToAction) Stream +classicActions = merge(ticks, turns) Stream +``` + +Read it as a diagram: + +``` + events ──┬──▶ interval(150) ──map(→Tick)──────────────┐ + │ ├──merge──▶ classicActions : Stream + └──▶ keyDowns ──filterMap(keyToAction)───────┘ +``` + +Each `Time` event goes to both branches (that's what `merge` does); `interval` decides whether a tick happened, `keyDowns` ignores it. Each `KeyDown` also goes to both; `interval` ignores it, `keyToAction` decides whether it means something. + +Check that you can predict the test: + +```js +run(classicActions, [time(150), down('ArrowUp'), down('x'), time(300)]) + // → [Tick, Turn('Up'), Tick] 'x' produced nothing +``` + +### 3. From actions to states: a running fold + +Module 5's insight was that a game is a fold over actions: `replay(newGame(seed), actions)`. A **stream** of actions folds the same way, except we want every intermediate state, not only the last one, because we must draw each of them. That's `scan`: + +``` +scan(actions, update, newGame(seed)) Stream: one new state per action +``` + +There's one more thing: `scan` emits only *new* accumulators, never the initial one. If we stopped here, nothing would be drawn until the first action — 150 ms of blank screen, and a board that's never shown in its starting position. So we put the initial state in front with `startWith`: + +``` +states(seed, actions) = startWith(scan(actions, update, newGame(seed)), newGame(seed)) +``` + +> **From Scala:** `scan` is `scanLeft` on a list that hasn't happened yet. `List(a1, a2).scanLeft(s0)(update)` gives `List(s0, s1, s2)` *including* `s0`; our `startWith(scan(…), s0)` is how we get the same "include the start" behaviour. + +You've seen this exact shape elsewhere: **Redux** reducers are `(state, action) → state` folded over a stream of dispatched actions, and **Elm**'s `update : Msg -> Model -> Model` is the same fold run by the Elm runtime. (The argument order differs per library; the idea doesn't.) + +### 4. From states to drawings, and `main` + +To draw a state: describe the picture (`view`, Module 6) and turn the description into a drawing program (`drawScene`, Module 7): + +``` +render(state) = drawScene(view(state)) IO +``` + +Mapping `render` over the states gives a stream of drawing programs, `Stream>`. The only thing still missing is the seed, which must come from the outside world — that's an `IO` (`randomSeed`). Put together: + +``` +main = map(randomSeed, seed => map(states(seed, classicActions), render)) +``` + +Here the two `map`s are *different* functions: the outer one is `IO`'s `map`, the inner one is `Stream`'s `map`. (In JS they come from two modules, so you'll import one of them under another name, e.g. `import { map as mapIO } from './io.js'`. In Scala they're methods, so the types pick the right one. In F# they're `IO.map` and `Stream.map`.) Let's read the type from the outside in: + +``` +IO< an effect: ask the world for a seed, and then produce… + Stream< …a machine that, for every event… + IO …produces the effects that draw a frame. + >> +``` + +Why must the outer layer be an `IO`? Because the stream of states depends on the seed, and the seed isn't known until the outside world is asked. Why is the inner layer an `IO` and not a `Scene`? Because the runtime should only know how to perform `IO`s; turning scenes into effects is the program's business, not the runtime's. (In Module 10 you could make frames that also play sounds, without touching the runtime.) + +And `main` is a **value**. Defining it performs nothing — no seed is asked for, no listener is registered. Look at the test: + +```js +const { ops, result: stream } = trace(main, 42) +assert.deepStrictEqual(ops, [{ tag: 'RandomSeed' }]) // main asks for a seed, nothing else +const [outs] = stream.step(time(0)) // the first event… +assert.equal(outs.length, 1) // …produces exactly one drawing: the new game +``` + +The whole program, with its randomness, time and keyboard, is tested with the pure interpreter from Module 7 and `step` from Module 8. No browser. + +**About `classicActions` and Module 10.** In Module 10, `main` will switch from `classicActions` to `speedActions`. The m09 tests that check the 150 ms behaviour therefore use a copy of `main` built from `classicActions` (`classicMain` in the test helpers), so they keep passing after Module 10. + +### 5. The edge: a Platform and a runtime + +Now somebody has to *do* it. Let's list everything the outside world must provide: + +- draw a rectangle; draw a text; +- give a random seed; +- call us back with the time, again and again; +- call us back when a key goes down; when a key goes up. + +That list is an interface, which the course calls a **Platform**: + +**JavaScript** + +```js +// Platform = { +// fillRect(x, y, w, h, color), +// fillText(x, y, text, color, size, align), +// randomSeed() → number (uint32), +// startClock(onTime: (t) => void), +// onKeyDown(cb: (key) => void), +// onKeyUp(cb: (key) => void), +// } +``` + +**Scala** (`scala/src/Runtime.scala`) + +```scala +trait Platform: + def fillRect(x: Int, y: Int, w: Int, h: Int, color: String): Unit + def fillText(x: Int, y: Int, text: String, color: String, size: Int, align: String): Unit + def randomSeed(): Long + def startClock(onTime: Long => Unit): Unit + def onKeyDown(cb: String => Unit): Unit + def onKeyUp(cb: String => Unit): Unit +``` + +**F#** (`fsharp/src/Runtime.fs`) — a record of functions + +```fsharp +type Platform = + { FillRect: int -> int -> int -> int -> string -> unit + FillText: int -> int -> string -> string -> int -> string -> unit + RandomSeed: unit -> uint32 + StartClock: (int64 -> unit) -> unit + OnKeyDown: (string -> unit) -> unit + OnKeyUp: (string -> unit) -> unit } +``` + +The runtime only ever talks to a `Platform`. The browser implementation (`browser.js` in JS, `web/BrowserPlatform.scala`, `fsharp/web/BrowserPlatform.fs`) wraps a canvas, `requestAnimationFrame` and `crypto.getRandomValues`. The tests use a **fake platform** that records every call in a list and lets the test fire events by hand — a test double in the sense of [Martin Fowler's "Test Double"](https://martinfowler.com/bliki/TestDouble.html): + +```js +const fakePlatform = (seed = 42) => { + const calls = []; const handlers = {} + return { + calls, + fillRect: (...args) => calls.push(['fillRect', ...args]), + fillText: (...args) => calls.push(['fillText', ...args]), + randomSeed: () => { calls.push(['randomSeed']); return seed }, + startClock: (cb) => { handlers.time = cb }, + onKeyDown: (cb) => { handlers.down = cb }, + onKeyUp: (cb) => { handlers.up = cb }, + fireTime: (t) => handlers.time(t), + fireKey: (k) => handlers.down(k), + fireKeyUp: (k) => handlers.up(k), + } +} +``` + +(Yes, the fake uses `push` and mutation; it's test code, which is allowed to be impure. It plays the role of the outside world.) + +### 6. `runIO`: the real interpreter + +`runIO(io, platform)` is `trace` from Module 7 with real answers: + +- `Draw` of a `Rect` → `platform.fillRect(x, y, w, h, color)`; of a `Text` → `platform.fillText(x, y, text, color, size, align)`; the answer is unit (`Done`); +- `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`. + +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. + +### 7. `runApp`: one mutable variable + +`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>`). +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.) +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; + - run each IO in `ios` with `runIO`, in order. + +That's the whole runtime. Go back to the stateful code at the start of Module 8, with its `let state` and logic in callbacks, and compare. There, the mutable variable held the *game state*, and game logic lived in the callbacks. Here, the variable holds the *current stream* — the runtime doesn't even know there's a game. It has no idea what a snake, a tick or a score is. All the game logic is in pure values; the runtime is a few lines of plumbing. That is what "pushing effects to the edge" means in practice. + +``` + ┌──────────────────────── runtime (impure, ~20 lines) ─────────────────────────┐ + 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 tests describe the behaviour precisely; read them before writing the code: + +- `runApp asks the platform for a seed once, at start, and draws nothing before the first input` — after `runApp`, the only call is `['randomSeed']`. Nothing is drawn until an event arrives (remember `startWith`). +- `runApp draws the initial scene on the first clock tick, then the ticked scene after 150 ms` — a single `fireTime(150)` produces two drawings: the `startWith` state and the state after one `Tick`. Can you explain why one event produces two frames? +- `runApp feeds key presses to the game`, and `runApp ignores key releases and unmapped keys (no redraw)`. + +### 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. + +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. + +### 9. Running the game + +**JavaScript.** You also write `browser.js` (see Exercise 9.4). Then, from the repository root: + +``` +node scripts/serve.mjs +``` + +and open . (A static server is needed because browsers refuse to load ES modules from `file://` URLs.) + +**Scala.** The browser glue is given (`scala/web/BrowserPlatform.scala`, `scala/web/Main.scala`); read it — it's short. Compile your code to JavaScript with Scala.js: + +``` +cd scala && ./build-web.sh +``` + +The first run downloads the Scala.js linker and takes a while. It writes `scala/web/dist/snake.js`. Then serve the repository root (`node scripts/serve.mjs`) and open . + +**F#.** The browser glue is given in `fsharp/web/`. Compile with Fable: + +``` +cd fsharp && ./build-web.sh +``` + +then serve the repository root and open . + +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 + +### Exercise 9.1 — `keyToAction` + +**JavaScript** (`js/src/input.js`) + +```js +export const keyToAction = (key) => /* Action or null */ +``` + +**Scala** (`scala/src/Input.scala`) + +```scala +object Input: + def keyToAction(key: String): Option[Action] = ??? +``` + +**F#** (`fsharp/src/Input.fs`) + +```fsharp +module 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`). + +### Exercise 9.2 — the app: `classicActions`, `states`, `render`, `main` + +**JavaScript** (`js/src/app.js`) + +```js +export const TICK_MS = 150 +export const classicActions = /* Stream */ +export const states = (seed, actions) => /* Stream */ +export const render = (state) => /* IO */ +export const main = /* IO>> */ +``` + +**Scala** (`scala/src/App.scala`) + +```scala +object App: + val TICK_MS: Long = 150 + lazy val classicActions: Stream[Action] = ??? + def states(seed: Long, actions: Stream[Action]): Stream[GameState] = ??? + def render(state: GameState): IO[Unit] = ??? + lazy val main: IO[Stream[IO[Unit]]] = ??? +``` + +**F#** (`fsharp/src/App.fs`) + +```fsharp +module App = + let TICK_MS = 150L + let classicActions : Stream = (* ... *) + let states (seed: uint32) (actions: Stream) : Stream = failwith "TODO m09" + let render (state: GameState) : IO = failwith "TODO m09" + let main : IO>> = (* ... *) +``` + +**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`. + +**Watch out for:** the JS stubs of `classicActions` and `main` are placeholder objects so that the file loads; replace them with real values. In JS, import the two `map`s under different names. Each of these definitions is one line; if yours is longer, re-read sections 2–4. + +### Exercise 9.3 — the runtime: `runIO` and `runApp` + +**JavaScript** (`js/src/runtime.js` — `let` and loops allowed here) + +```js +export const runIO = (io, platform) => /* the final value */ +export const runApp = (main, platform) => /* undefined; registers callbacks */ +``` + +**Scala** (`scala/src/Runtime.scala` — exactly one `var`) + +```scala +object Runtime: + def runIO[A](io: IO[A], p: Platform): A = ??? + def runApp(main: IO[Stream[IO[Unit]]], p: Platform): Unit = ??? +``` + +**F#** (`fsharp/src/Runtime.fs` — `mutable` allowed here) + +```fsharp +module Runtime = + let runIO (p: Platform) (io: IO<'a>) : 'a = failwith "TODO m09" + let runApp (p: Platform) (main: IO>>) : 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. + +**Watch out for:** registering the callbacks *before* the stream exists, or reading the variable in a callback without updating it. Each callback must step the *current* stream and store the next one. In Scala, `runIO` with an `Op.Draw` must match on the shape inside (`Shape.Rect` / `Shape.Text`). + +### Exercise 9.4 (JS only) — `browserPlatform` + +**JavaScript** (`js/src/browser.js`) + +```js +export const browserPlatform = (canvas, win) => /* Platform */ +``` + +Build a `Platform` from a canvas and a window: `fillRect`/`fillText` use `canvas.getContext('2d')` (font `` `${size}px monospace` ``, `textAlign` from `align`, `fillStyle` from `color`); `startClock` calls `onTime(t)` on every `win.requestAnimationFrame`, with `t` = milliseconds since the first frame; `onKeyDown`/`onKeyUp` listen on `win` and call `preventDefault()` for arrow keys and space so the page doesn't scroll; `randomSeed` returns `crypto.getRandomValues(new Uint32Array(1))[0]`. There is no automated test for this file; the test is that the game runs. Scala and F# students: read the given `BrowserPlatform` in your track and compare it with this description. + +**Run:** +- JS: `cd js && node --test "test/m09-*.test.js"`, then `node scripts/serve.mjs` (from the repo root) and open +- Scala: `cd scala && ./test.sh M09`, then `./build-web.sh`, serve, open +- F#: `cd fsharp && ./test.sh M09`, then `./build-web.sh`, serve, open + +## Hints + +
Exercise 9.1 — nudge + +A `switch` (JS) or `match` (Scala/F#) with several keys per case is the most readable. In JS, `case 'ArrowUp': case 'w': case 'W': return Action.Turn('Up')`, and `default: return null`. Why is a `switch` safe from the `constructor` trap and a plain object lookup isn't? + +
+ +
Exercise 9.2 — nudge + +Each definition is a direct translation of a line in sections 2–4. Write them in the order `classicActions`, `states`, `render`, `main`, and run the tests after each one. + +
+ +
Exercise 9.2 — near-solution (try the others first) + +**JavaScript** + +```js +import { map as mapIO, randomSeed, drawScene } from './io.js' +import { interval, keyDowns, map as mapS, filterMap, merge, scan, startWith } from './stream.js' + +export const classicActions = merge(mapS(interval(TICK_MS), () => /* ? */), filterMap(/* ? */, /* ? */)) +export const states = (seed, actions) => startWith(scan(actions, /* ? */, /* ? */), /* ? */) +export const render = (state) => /* ? */ +export const main = mapIO(randomSeed, (seed) => /* ? */) +``` + +**Scala** + +```scala +lazy val classicActions: Stream[Action] = + Stream.interval(TICK_MS).map(_ => /* ? */).merge(Stream.keyDowns.filterMap(/* ? */)) +def states(seed: Long, actions: Stream[Action]): Stream[GameState] = + actions.scan(/* ? */)(Game.update).startWith(/* ? */) +lazy val main: IO[Stream[IO[Unit]]] = IO.randomSeed.map(seed => /* ? */) +``` + +**F#** + +```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 -> (* ? *)) +``` + +(F#'s `Game.update` takes `state` then `action`, which is the order `Stream.scan`'s folder wants.) + +
+ +
Exercise 9.3 — nudge + +`runIO`: keep a "current program" variable. While it's an `Effect`, perform its op on the platform and replace the current program with `k(answer)`. When it's `Pure`, return the value. Write a small helper `perform(op, platform)` that returns the answer. + +`runApp`: one variable for the stream, one helper `feed(event)`, three registrations. Which of the three registrations should come last, and does it matter? + +
+ +
Exercise 9.3 — near-solution (try the others first) + +**JavaScript** + +```js +export const runIO = (io, platform) => { + let current = io + while (current.tag === 'Effect') current = current.k(/* perform current.op, get the answer */) + return /* ? */ +} + +export const runApp = (main, platform) => { + let stream = /* ? */ + const feed = (input) => { + const [ios, next] = /* ? */ + stream = /* ? */ + ios.forEach(/* ? */) + } + platform.onKeyDown((key) => feed({ tag: 'KeyDown', key })) + platform.onKeyUp(/* ? */) + platform.startClock(/* ? */) +} +``` + +**Scala** + +```scala +@annotation.tailrec +def runIO[A](io: IO[A], p: Platform): A = io match + case IO.Pure(a) => a + case IO.Effect(op, k) => runIO(k(/* perform op on p, giving an OpResult */), p) + +def runApp(main: IO[Stream[IO[Unit]]], p: Platform): Unit = + var stream = /* ? */ + def feed(e: Event): Unit = + val (ios, next) = /* ? */ + stream = next + /* ? */ + p.onKeyDown(key => feed(Event.KeyDown(key))) + /* ... */ +``` + +**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 +``` + +(F#'s `runApp` follows the JS shape with one `let mutable stream`.) + +
+ +## Check your understanding + +1. Read `IO>>` aloud and explain each layer. Why can't the outer `IO` be removed? Why isn't the inner one a `Scene`? +2. Trace one key press: the player presses `ArrowUp`. Name, in order, every function and value it passes through until pixels change. When does the snake actually turn on screen, and why not immediately? +3. One call `fireTime(150)` right after `runApp` produces two full drawings. Explain exactly which outputs of which streams cause each. +4. The runtime's only variable holds a *stream*, not a *game state*. What would be worse if it held the game state instead? Which code would have to move into the runtime? +5. Why must `runIO` be a loop while `trace` may be recursive? Is there a program for which even the loop overflows? Whose fault would it be? +6. What makes the runtime testable without a browser? What would you have to do to test it if it called `canvas.getContext('2d')` directly? +7. Count the impure lines in your track (runtime + browser glue + main). Compare with the number of pure lines. What does that ratio tell you? +8. `main` is defined at module load time. Prove to yourself that defining it performs no effect. (Hint: what does `trace(main, 1).ops` contain, and when does the platform's `randomSeed` get called?) + +## In production + +- **The Elm Architecture** is this module, packaged: you provide `init`, `update`, `view` and `subscriptions` (Elm's version of our input streams); the Elm runtime holds the model, feeds it messages and draws your view. You never write the runtime ([The Elm Architecture](https://guide.elm-lang.org/architecture/)). **Elmish** gives F# the same shape ([Elmish](https://elmish.github.io/elmish/)). +- **Redux** is the `scan(actions, update, initial)` part: a store holds the current state, and a pure reducer computes the next one for each dispatched action ([Redux fundamentals: state, actions and reducers](https://redux.js.org/tutorials/fundamentals/part-3-state-actions-reducers)). +- **cats-effect `IOApp`** and **ZIO's `ZIOAppDefault`** are the "one call at the edge": your `run` method returns an effect value, and the library's runtime performs it ([cats-effect](https://typelevel.org/cats-effect/), [ZIO](https://zio.dev/)). An fs2 application is literally an `IO` that runs a `Stream` of effects ([fs2](https://fs2.io/)). +- **"Functional core, imperative shell"** is Gary Bernhardt's name for this architecture: keep decisions in pure functions and push the effects to a thin shell ([Functional Core, Imperative Shell](https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell)). Our shell is thinner than usual: even the *wiring* is pure, only the interpreter isn't. + +## Further reading + +- [The Elm Architecture](https://guide.elm-lang.org/architecture/) — compare `init`/`update`/`view`/`subscriptions` with `newGame`/`update`/`view`/`classicActions`. +- [Gary Bernhardt, "Functional Core, Imperative Shell"](https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell) — the architecture you just built, explained in 13 minutes. +- [MDN: `requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame) — where the browser platform's clock comes from. +- [MDN: `Crypto.getRandomValues`](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues) — where the seed comes from. +- [Martin Fowler, "Test Double"](https://martinfowler.com/bliki/TestDouble.html) — the vocabulary for fakes, stubs and mocks; our fake platform is a "fake". +- [Scala.js](https://www.scala-js.org/) and [Fable](https://fable.io/) — how the Scala and F# tracks run in the browser. diff --git a/course/10-extensions.md b/course/10-extensions.md new file mode 100644 index 0000000..3a0d29c --- /dev/null +++ b/course/10-extensions.md @@ -0,0 +1,325 @@ +# Module 10 — Extensions, all pure + +> **Time:** ~2–3 h · **You'll edit:** JS `js/src/game.js`, `js/src/input.js`, `js/src/app.js` · Scala `scala/src/Game.scala`, `scala/src/Input.scala`, `scala/src/App.scala` · F# `fsharp/src/Game.fs`, `fsharp/src/Input.fs`, `fsharp/src/App.fs` +> **Tests:** JS `cd js && node --test "test/m10-*.test.js"` · Scala `cd scala && ./test.sh M10` · F# `cd fsharp && ./test.sh M10` — and afterwards **all** tests: JS `./test.sh` · Scala `./test.sh` · F# `./test.sh` +> **Prerequisites:** Modules 0–9 (a playable game) + +## Why this module + +You have a playable Snake. Now come the features every player asks for: **pause**, **restart**, and a game that **speeds up** as you score. This module is about a question you should ask of any architecture: *when I add a feature, how much do I have to touch, and how much can break?* + +Think about how these features usually get added to a stateful game: + +```js +// The usual, stateful way (do NOT write this in the course) +let paused = false +let timer = setInterval(step, 150) +window.addEventListener('keydown', (e) => { + if (e.key === ' ') paused = !paused + if (e.key === 'r') { snake = [...]; apple = ...; score = 0; paused = false } // did we reset everything? +}) +function step() { + if (paused) return + // ... move, eat ... + if (ateApple && score % 3 === 0) { + clearInterval(timer) + timer = setInterval(step, Math.max(50, 150 - score * 5)) // change the clock itself + } +} +``` + +Every feature adds another mutable variable, another `if` in a callback, and another way to forget something (does restart reset the timer speed? does pressing an arrow while paused queue a turn?). Speeding up means *reaching out and changing the clock*, which is an effect hidden in the middle of the game logic. + +In our design, the same three features are **new cases in the `Action` data type, new cases in `update`, two more keys in `keyToAction`, and one different input stream**. The runtime doesn't change at all. The m09 tests keep passing. And every new behaviour is pinned down by fixtures that run without a browser. + +## Learning objectives + +By the end of this module you can: + +- **implement** `TogglePause`, `Restart` and `Frame` as pure cases of `update`; +- **explain** why speed-up is done with a frame counter and `movesEvery` instead of changing the clock, and **compute** the move interval for a given score; +- **predict**, using the fixtures, exactly which `Frame` actions move the snake; +- **explain** why features added this way cannot break earlier modules' tests, and which tests *would* catch a mistake; +- **design** (on paper) a persisted high score as a new effect, without writing impure code in the game logic. + +## Concepts + +### 1. Pause is a status, not a flag in a callback + +`GameState` has had a `status` field since Module 2, with the value `Paused` waiting for this moment. And since Module 3, `turn` and `advance` do nothing unless the status is `Playing`. So most of "pause" is already done: a paused game *ignores* ticks and turns, by construction. (This is Review Focus 4 of the course: keys pressed while paused or after game over must do nothing. The fixtures `turn-while-paused-is-ignored` and `tick-while-paused-is-ignored` check it.) + +What's left is the action that switches it: + +- `TogglePause`: `Playing` → `Paused`, `Paused` → `Playing`, and **any other status is unchanged** (you can't pause a game that is over, nor "unpause" it into `Playing`). + +Compare the two worlds. In the stateful version, "paused" is a separate boolean that every callback must remember to check. In ours, it's one value of a field that the rules already respect — there is no way to forget it, because the rules never run in the wrong status. + +### 2. Restart is "a new game", which is a value you already have + +- `Restart`: `newGame(state.seed)`, **from any status**. + +That's it, and it's the shortest bug-free restart you'll ever write. The stateful version had to *reset* every variable it had created, and forgetting one is a classic bug. Here there is nothing to reset, because there is nothing that persists except the value you return: the new game is a complete state built from scratch by the same function that built the first one. + +Notice which seed is used: `state.seed`, the *current* seed, which has advanced every time an apple was placed. So a restart is deterministic (the same state always restarts the same way) but it's a different game from the first one. Think about that choice in "Check your understanding". + +### 3. Speed: change the counting, not the clock + +How do you make the snake move faster as the score grows? The stateful answer is to change the timer: `clearInterval`, `setInterval` with a shorter delay. In our design that would mean the *clock stream* depends on the *game state* — the input would depend on the output — and the runtime would have to be told to restart its clock. That's an effect, right in the middle of the game. + +Here's the alternative that keeps everything pure: **run one fast clock that never changes, and let the game decide on which beats to move.** + +- A new action, `Frame`, arrives every `FRAME_MS = 25` ms. +- `GameState` has a `frame` counter (since Module 2, waiting for this moment). +- On `Frame`: if the game isn't `Playing`, nothing happens. Otherwise, `frame` goes up by one, and **if the new frame number is a multiple of `movesEvery(score)`, the game also does a `tick`** (on the state with the incremented frame). +- `movesEvery(score) = max(2, 8 - floor(score / 3))`. + +So the speed is a pure function of the score: + +| score | `movesEvery` | time per move (× 25 ms) | +|---|---|---| +| 0–2 | 8 | 200 ms | +| 3–5 | 7 | 175 ms | +| 6–8 | 6 | 150 ms | +| 9–11 | 5 | 125 ms | +| 12–14 | 4 | 100 ms | +| 15–17 | 3 | 75 ms | +| 18 and up | 2 | 50 ms | + +This is a general and very useful trick: **instead of changing the rate of an external effect, observe a fixed-rate effect and decide in pure code.** The clock stays dumb; the decision is in `update`, where it can be tested with fixtures. It's also how many real games work: a fixed simulation step, with the game deciding what happens on each one ([Game Programming Patterns: Game Loop](https://gameprogrammingpatterns.com/game-loop.html), [Glenn Fiedler, "Fix Your Timestep!"](https://gafferongames.com/post/fix_your_timestep/)). + +Predict with the fixtures before you code. Each line is "frame before → score → does this `Frame` tick?": + +``` +frame 0, score 0 → new frame 1: 1 % 8 ≠ 0 → no tick (frame-below-threshold-only-counts-the-frame) +frame 6, score 0 → new frame 7: 7 % 8 ≠ 0 → no tick (frame-just-before-threshold-…) +frame 7, score 0 → new frame 8: 8 % 8 = 0 → tick (frame-at-threshold-ticks-the-game) +frame 15, score 0 → new frame 16: 16 % 8 = 0 → tick (frame-at-second-multiple-…) +frame 6, score 3 → new frame 7: 7 % 7 = 0 → tick (frame-threshold-shrinks-as-score-grows) +frame 7, score 3 → new frame 8: 8 % 7 ≠ 0 → no tick (frame-after-threshold-shrinks-does-not-tick) +``` + +The last two lines show that the multiple is checked against the *current* score's `movesEvery`. When the score crosses a threshold, the next move may come a little earlier or later than a "clean" schedule would suggest; for a game this is invisible, and the rule is simple and testable. + +### 4. The clock stream changes, the runtime doesn't + +The input side needs one change. Instead of `Tick` every 150 ms, the clock now produces `Frame` every 25 ms; key presses go through `keyToAction` as before, which now also knows two more keys: + +- `' '` (Space) → `TogglePause`; `'r'` / `'R'` → `Restart`. + +``` +speedActions = merge(map(interval(FRAME_MS), _ => Frame), filterMap(keyDowns, keyToAction)) +``` + +and `main` switches from `classicActions` to `speedActions`. Compare it with `classicActions`: the same shape, a different clock and action. The runtime still holds one stream, steps it, runs the IOs. It has no idea that the game can now pause or speed up. + +### 5. Why the earlier tests keep passing + +Run all the tests of your track after this module. They should all be green, including Modules 5 and 9. Why? + +- `update` still handles `Tick` and `Turn` exactly as before; you only added cases. The m05 fixtures don't contain the new actions. +- `classicActions` is unchanged. The m09 tests that check "a tick every 150 ms" use `classicMain`, a copy of the program built from `classicActions`, defined in the test helpers. The one m09 test that uses the real `main` only checks that "time passing produces more drawings", which is true for both versions. +- The runtime is unchanged, so the runtime tests can't be affected. + +This is the "open for extension" property you get from data + a fold: a new feature is a new case, and old cases don't move. When something does break, it breaks in a fixture that names the exact situation (`restart-from-paused`, `frame-tick-can-end-the-game`), not in a browser session. + +> **From Scala:** in Scala, write `update` as a `match` with one case per `Action` and **no `case _`**. Then if you add a case to `Action` and forget it in `update`, the compiler warns you ("match may not be exhaustive"). The stub's doc comment asks for this. F# gives the same warning for incomplete matches (`FS0025`). JS has no such check; the fixtures are your safety net. + +### 6. A thought exercise: a high score that survives a reload + +Suppose you want to keep the best score in the browser's `localStorage` ([MDN: `localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage)) so it survives reloading the page. Reading and writing storage are effects. Where would they go? Work through these questions on paper; no code is required (and none is tested). + +1. **New operations.** Which `Op`s do you need? Perhaps `{tag:'LoadHigh'}` (answer: a number) and `{tag:'SaveHigh', score}` (answer: unit). In Scala/F#, which new case does `OpResult` need? +2. **New interpreter cases.** What does `trace` answer to `LoadHigh` — a fixed number passed in, like the seed? What does `runIO` do — and which new `Platform` methods does that need? +3. **Loading.** The high score must be known before the first frame is drawn. Where in `main` does it go? (Hint: `main` already starts with one effect, `randomSeed`. How do you do two effects, the second after the first? Which function from Module 7 combines them?) +4. **State.** Where does the high score live during the game? Is it a field of `GameState`? What does `Restart` do with it? +5. **Saving.** When should it be saved — on every frame, or only when a game ends with a new record? Our stream turns *each state* into an `IO`; to notice "the game just ended", you need to know the previous state too. Which combinator from Module 8 could carry the previous state along? (Hint: `scan` can accumulate pairs.) +6. **The test.** Write down, in words, a test that checks "after a game over with score 7 and a previous best of 5, a `SaveHigh 7` op is performed". Which interpreter would that test use? + +Notice what you did *not* have to do: touch the rules of the game, or let `update` call `localStorage`. The effect enters the program as one more `Op`; the decision of *when* to save is pure; and only the interpreter knows about the browser. That's the method of the whole course, applied to a feature you designed yourself. + +## Exercises + +### Exercise 10.1 — `movesEvery` + +**JavaScript** (`js/src/game.js`) + +```js +export const movesEvery = (score) => /* max(2, 8 - floor(score / 3)) */ +``` + +**Scala** (`scala/src/Game.scala`) + +```scala +def movesEvery(score: Int): Int = ??? +``` + +**F#** (`fsharp/src/Game.fs`) + +```fsharp +let movesEvery (score: int) : int = failwith "TODO m10" +``` + +**Proved by:** `fixtures/m10-moves-every.json` (`score-0`, `score-2`, `score-3`, `score-17`, `score-18`, `score-100`). + +### Exercise 10.2 — `update`: `TogglePause`, `Restart`, `Frame` + +Extend `update(state, action)` (same file as 10.1) with the three new actions, as specified in sections 1–3. Keep the old `Tick` and `Turn` cases as they are. + +**JavaScript** + +```js +export const update = (state, action) => /* GameState */ +``` + +**Scala** (one `case` per `Action`, no wildcard) + +```scala +def update(state: GameState, action: Action): GameState = ??? +``` + +**F#** + +```fsharp +let update (state: GameState) (action: Action) : GameState = failwith "TODO m05/m10" +``` + +**Proved by:** `fixtures/m10-update.json`, 22 cases: `toggle-pause-pauses-a-playing-game`, `toggle-pause-resumes-a-paused-game`, `toggle-pause-ignored-when-game-over`, `toggle-pause-ignored-when-won`, `turn-while-paused-is-ignored`, `tick-while-paused-is-ignored`, the four `restart-from-…` cases, the `frame-…` cases from section 3, `frame-at-top-speed-ticks-every-second-frame`, `frame-at-top-speed-odd-frame-does-not-tick`, `frame-tick-can-end-the-game`, and `frame-when-paused-is-unchanged` / `…-game-over-…` / `…-won-…`. + +**Watch out for:** in `Frame`, the tick happens on the state **with the incremented frame**, so the result has the new `frame` *and* the moved snake. And `movesEvery` uses the score *before* the tick (a tick that eats an apple raises the score for the *next* frame, not this one). + +### Exercise 10.3 — two more keys + +Extend `keyToAction` (`js/src/input.js`, `scala/src/Input.scala`, `fsharp/src/Input.fs`): `' '` → `TogglePause`; `'r'`, `'R'` → `Restart`. Signature unchanged. + +**Proved by:** `fixtures/m10-input.json` (`space-toggles-pause`, `r-restarts`, `R-restarts`, `ArrowUp-still-turns-up`, `d-still-turns-right`, `Enter-is-not-bound`, `empty-string-is-not-bound`), plus all of `m09-input.json` still passing. + +### Exercise 10.4 — the frame clock + +**JavaScript** (`js/src/app.js`) + +```js +export const FRAME_MS = 25 +export const speedActions = /* Stream */ +// and change `main` to use speedActions +``` + +**Scala** (`scala/src/App.scala`) + +```scala +val FRAME_MS: Long = 25 +lazy val speedActions: Stream[Action] = ??? +// and change `main` to use speedActions +``` + +**F#** (`fsharp/src/App.fs`) + +```fsharp +let FRAME_MS = 25L +let speedActions : Stream = (* ... *) +// and change `main` to use speedActions +``` + +**Proved by:** `the animation clock runs every 25 ms`, `speedActions turns the clock into Frame actions every 25 ms`, `speedActions still turns keys into actions, including pause and restart`, `main (seed 42) draws a moved snake after eight 25 ms frames`. + +**Run:** +- JS: `cd js && node --test "test/m10-*.test.js"`, then `./test.sh` for everything +- Scala: `cd scala && ./test.sh M10`, then `./test.sh` +- F#: `cd fsharp && ./test.sh M10`, then `./test.sh` + +Then play: JS — reload ; Scala — `./build-web.sh` again and reload ; F# — `./build-web.sh` again and reload . Press Space, press R, and eat apples until it gets hard. + +## Hints + +
Exercise 10.2 — nudge + +Write each new case as its own small function (`togglePause(state)`, `frame(state)`) and keep `update` a flat dispatch on `action.tag`. For `Frame`, do it in two steps: first build the state with `frame + 1`; then decide whether to `tick` it. + +
+ +
Exercise 10.2 — stronger hint + +For `TogglePause`, match on the *status*: two statuses change, the other two don't. For `Frame`, the guard "not Playing → unchanged" comes first. Then: `counted = { ...state, frame: state.frame + 1 }`, and the result is `counted.frame % movesEvery(state.score) === 0 ? tick(counted) : counted`. + +
+ +
Exercise 10.2 — near-solution (try the others first) + +**JavaScript** + +```js +const togglePause = (state) => + state.status === 'Playing' ? { ...state, status: 'Paused' } + : state.status === 'Paused' ? /* ? */ + : state + +const frame = (state) => { + if (state.status !== 'Playing') return state + const counted = /* ? */ + return /* tick(counted) or counted? */ +} + +export const update = (state, action) => { + switch (action.tag) { + case 'Tick': return tick(state) + case 'Turn': return turn(state, action.dir) + case 'TogglePause': return /* ? */ + case 'Restart': return /* ? */ + case 'Frame': return /* ? */ + } +} +``` + +**Scala** + +```scala +def update(state: GameState, action: Action): GameState = action match + case Action.Tick => Spawn.tick(state) + case Action.Turn(d) => Rules.turn(state, d) + case Action.TogglePause => /* ? */ + case Action.Restart => /* ? */ + case Action.Frame => /* ? */ +``` + +**F#** + +```fsharp +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 *) + | TogglePause -> (* ? *) + | Restart -> (* ? *) + | Frame -> (* ? *) +``` + +
+ +
Exercise 10.4 — nudge + +`speedActions` is `classicActions` with a different interval and a different constant action. Then `main` changes one word. + +
+ +## Check your understanding + +1. List every file you changed in this module. Which important file did you *not* change, and why didn't you need to? +2. In the stateful version, speed-up called `clearInterval`/`setInterval`. Why would doing that in our design require the input stream to depend on the game state, and why is that a problem? +3. Predict: from a fresh game (frame 0, score 0, Playing), how many `Frame` actions until the snake first moves? With score 18? Check against the table. +4. A player presses Space, then ArrowUp, then Space again, all within one move interval. Which way is the snake heading afterwards? Use the rules from Modules 3 and 10 to justify your answer. +5. `Restart` uses `newGame(state.seed)`. What would change if it used the seed of the very first game instead? Which would you prefer for a speedrun leaderboard, and which for casual play? +6. The m09 test "main draws again once time has passed" passes both before and after this module, but "the program built from classicActions draws the new game first, then the ticked game after 150 ms" uses a copy of `main`. Why was the test suite designed this way? +7. For the high-score thought exercise: which parts of your design are pure, which are interpreter code, and how would you test that a new record gets saved without a browser? + +## In production + +- **Elm** programs add features exactly like this: a new `Msg` variant, a new branch in `update`; the compiler's exhaustiveness check lists every place that must handle it ([The Elm Architecture](https://guide.elm-lang.org/architecture/)). +- **Redux** reducers grow by new action types; the Redux docs recommend keeping reducers pure and putting effects in middleware, the same split as our `update` vs. interpreter ([Redux fundamentals: state, actions and reducers](https://redux.js.org/tutorials/fundamentals/part-3-state-actions-reducers)). +- **Fixed-timestep game loops** separate the rate at which the platform calls you from the rate at which the simulation advances; our `Frame` + `movesEvery` is a minimal version ([Glenn Fiedler, "Fix Your Timestep!"](https://gafferongames.com/post/fix_your_timestep/)). +- **Event sourcing** stores the list of events and derives state by folding them — the same "state = fold(update, events)" idea, used for databases and audit logs; it is also why pause/restart cost nothing extra to replay ([Martin Fowler, "Event Sourcing"](https://martinfowler.com/eaaDev/EventSourcing.html)). + +## Further reading + +- [Robert Nystrom, Game Programming Patterns: Game Loop](https://gameprogrammingpatterns.com/game-loop.html) — fixed and variable time steps, and why games separate them. +- [Glenn Fiedler, "Fix Your Timestep!"](https://gafferongames.com/post/fix_your_timestep/) — the classic article on decoupling simulation from the clock. +- [Martin Fowler, "Event Sourcing"](https://martinfowler.com/eaaDev/EventSourcing.html) — what happens when you take "a program is a fold over events" seriously in business software. +- [MDN: `Window.localStorage`](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) — the API you would wrap in a new `Op` for the high-score exercise. diff --git a/course/11-capstone-pong.md b/course/11-capstone-pong.md new file mode 100644 index 0000000..3082f1c --- /dev/null +++ b/course/11-capstone-pong.md @@ -0,0 +1,395 @@ +# Module 11 — Capstone: Pong, by the method + +> **Time:** ~6–10 h (it's a whole game; spread it over several sessions) · **You'll edit:** JS `js/src/pong/model.js`, `rules.js`, `view.js`, `app.js` · Scala `scala/src/pong/Model.scala`, `Rules.scala`, `View.scala`, `App.scala` · F# `fsharp/src/Pong/Model.fs`, `Rules.fs`, `View.fs`, `App.fs` +> **Tests:** JS `cd js && node --test "test/m11-*.test.js"` · Scala `cd scala && ./test.sh M11` · F# `cd fsharp && ./test.sh M11` +> **Prerequisites:** Modules 0–10, and **[the method](method.md)** — read it before starting; this module is you applying it. + +## Why this module + +Everything so far was guided: each module told you which function to write and why. That's how you learn the pieces. But the goal of this course is not "can build Snake when told how". It is: **given a game written the usual way — with variables that change, timers and listeners — can you turn it into a purely functional one, and explain every step?** + +So in this module you get a different game, **Pong**, described the way you'd find it in the wild: a set of rules and a sketch of a stateful implementation. You convert it, following the recipe in [method.md](method.md). The scaffolding is deliberately lighter. The specification and the tests are complete (so you know when you're done), but the hints mostly ask questions instead of giving shapes. Where Snake modules gave you a near-solution, this one asks you to look back at your own Snake code — you've written every pattern you need already. + +You reuse, unchanged: `IO` (m07), streams (m08), the runtime and platform (m09), `nextUint32` (m04). Notice, as you go, how much of the course turns out to be **reusable infrastructure** and how little is Snake-specific. + +## Learning objectives + +By the end of this module you can: + +- **apply** the conversion method to an unfamiliar game: identify state, events, the update fold, the view, the effects and the wiring; +- **design** a state that turns a *held key* (keydown + keyup) into data, and **explain** why Snake didn't need that; +- **implement** a multi-step frame update as a pipeline of small pure functions, in a specified order; +- **reuse** the IO, stream and runtime modules for a second program without modifying them; +- **justify** your design choices against the course's canonical ones. + +## The game, as you'd find it + +Pong is a two-player tennis game: each side has a vertical paddle and must stop the ball from leaving the court on their side ([Pong on Wikipedia](https://en.wikipedia.org/wiki/Pong)). Our version: you play the left paddle with the keyboard against a simple computer player on the right; first to 5 points wins. + +Here is a typical stateful implementation sketch. Read it the way you'd read a colleague's code that you've been asked to convert. Don't run it; it isn't in the repository. + +```js +// The usual, stateful way (a sketch to convert; do NOT write this in the course) +let ball = { x: 40, y: 25 }, dx = 1, dy = 1 +let player = 21, cpu = 21, frame = 0 +let scorePlayer = 0, scoreCpu = 0, over = false +let upHeld = false, downHeld = false + +window.addEventListener('keydown', (e) => { + if (e.key === 'ArrowUp') upHeld = true + if (e.key === 'ArrowDown') downHeld = true + if (e.key === 'r') { /* reset everything... hopefully */ } +}) +window.addEventListener('keyup', (e) => { + if (e.key === 'ArrowUp') upHeld = false + if (e.key === 'ArrowDown') downHeld = false +}) + +setInterval(() => { + if (over) return + frame++ + if (downHeld) player++ + if (upHeld) player-- + player = Math.max(0, Math.min(42, player)) + if (frame % 2 === 0) cpu += Math.sign(ball.y - 4 - cpu) // cpu follows the ball, at half speed + // ... move the ball, bounce on walls and paddles, score, serve with Math.random() ... + ctx.fillRect(/* paddles, ball, score ... */) +}, 30) +``` + +Count the hidden things before reading on: how many mutable variables? Where is time? Where is randomness? Where are effects mixed into logic? (You'll answer these in step 1 below.) + +## The rules, precisely + +The tests are shared fixtures, so the rules must be exact. Here they are in prose. The court is measured in **cells**: `W = 80` wide, `H = 50` high; each cell is `PCELL = 8` pixels. Paddles are `PADDLE = 8` cells tall. The player's paddle is in column `PLAYER_X = 2`, the computer's in column `CPU_X = 77`. A paddle's position is the `y` of its **top** cell. `WIN = 5` points end the match. + +**A new match** (`initialPong(seed)`): ball at `(40, 25)` moving `dx = 1, dy = 1`; both paddles at `y = 21`; no keys held; score 0–0; status `Playing`; the given seed; frame 0. + +**Keys.** `ArrowUp`/`w`/`W` and `ArrowDown`/`s`/`S` are held down and released; while held, the paddle keeps moving. `r`/`R` pressed restarts. Everything else is ignored. + +**Each frame** (every 30 ms), if the match is being played, in this order: + +1. The frame counter goes up by one. +2. **The player's paddle** moves one cell down if "down" is held and one cell up if "up" is held (both held: they cancel out), and is kept within `[0, H - PADDLE]`. +3. **The computer's paddle** moves only when the *new* frame number is even. It moves one cell toward `ball.y - PADDLE / 2` (it aims to centre itself on the ball) and stays put if it's already there; also kept within `[0, H - PADDLE]`. It looks at the ball *before* the ball moves this frame. +4. **The ball moves vertically:** `ny = y + dy`. If `ny < 0` or `ny >= H`, it bounces off the wall: `dy` flips and `ny = y + dy` (with the new `dy`). +5. **The ball moves horizontally:** `nx = x + dx`. It bounces off a paddle — `dx` flips and `nx = x` (the ball stays in its column this frame) — when + - `nx == PLAYER_X`, the ball is moving left (`dx < 0`), and `player <= ny < player + PADDLE` (the player's **new** position, after step 2); or + - `nx == CPU_X`, the ball is moving right (`dx > 0`), and `cpu <= ny < cpu + PADDLE`. +6. **Scoring:** if `nx < 0`, the computer scores; if `nx >= W`, the player scores. A point **serves** a new ball: position `(40, 25)`, `dx` toward the side that conceded (computer scored → `-1`, player scored → `+1`), and `dy = 1` if `nextUint32(seed)` is even, `-1` otherwise — and the seed advances to the one `nextUint32` returned. +7. **End:** when a score reaches `WIN`, the status becomes `Over`. + +When the match is `Over`, frames change nothing. Key presses and releases are still recorded (so that a key held across a restart behaves sensibly). **Restart** gives `initialPong(state.seed)` at any time. + +**The picture** (`pongView`), drawn in order, all text `'#ffffff'` and `'center'`-aligned: + +- background `Rect(0, 0, 640, 400, '#111111')`; +- player paddle `Rect(PLAYER_X*8, player*8, 8, 64, '#ffffff')`, then the cpu paddle the same way at `CPU_X`; +- ball `Rect(x*8, y*8, 8, 8, '#ffeb3b')`; +- score `Text(320, 30, ' ', …, 24, …)` — three spaces between the numbers; +- when `Over`: `Text(320, 200, 'You Win!' or 'CPU Wins', …, 32, …)` (`'You Win!'` if the player has more points) and `Text(320, 230, 'Press R to restart', …, 16, …)`. + +Note that, unlike the Snake view, there's no 1-pixel inset: paddles and ball fill their cells. + +## Applying the method + +Open [method.md](method.md) next to this page. Each step below names the step of the method, asks you the questions you should answer *before* looking at the course's choice, then gives the canonical choice (fixed, because the fixtures depend on it). As in Module 2, **write your own answer first, then compare**. Where your design differs, ask whether yours is better, worse, or only different — and why. + +### Step 1 — Identify the state; make it one value + +Go through the stateful sketch and list every `let`. Then ask: + +- Which of them change during a frame? Which change only between frames because of keys? +- Is `frame` really state, or could it be computed? (Could the cpu's "every second frame" rule work without it?) +- Where is the randomness hiding? What must be in the state so that serving is a pure function? +- Is anything in the sketch *derived* (computable from other state) and therefore not needed in the state? + +**Canonical choice:** + +**JavaScript** (`js/src/pong/model.js`) + +```js +// PongState = { +// ball: {x, y}, vel: {dx, dy}, player, cpu, +// held: {up, down}, score: {player, cpu}, +// status: 'Playing' | 'Over', seed, frame +// } +``` + +**Scala** (`scala/src/pong/Model.scala`, given) + +```scala +final case class Velocity(dx: Int, dy: Int) +final case class Held(up: Boolean, down: Boolean) +final case class Score(player: Int, cpu: Int) +enum PongStatus: + case Playing, Over + +final case class PongState(ball: Point, vel: Velocity, player: Int, cpu: Int, held: Held, + score: Score, status: PongStatus, seed: Long, frame: Int) +``` + +**F#** (`fsharp/src/Pong/Model.fs`, given) + +```fsharp +type Velocity = { Dx: int; Dy: int } +type Held = { Up: bool; Down: bool } +type Score = { Player: int; Cpu: int } +type PongStatus = Playing | Over + +type PongState = + { Ball: Point; Vel: Velocity; Player: int; Cpu: int; Held: Held + Score: Score; Status: PongStatus; Seed: uint32; Frame: int } +``` + +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 + +What can happen to a match? List the events in the sketch's callbacks and timer. Then decide: what's the *smallest* set of actions that the update needs to know about? + +**Canonical choice:** four actions. + +**JavaScript** + +```js +// PongAction = {tag:'Press', key:'Up'|'Down'} | {tag:'Release', key:'Up'|'Down'} | {tag:'Frame'} | {tag:'Restart'} +// constructors: PongAction.Press(key), PongAction.Release(key), PongAction.Frame, PongAction.Restart +``` + +**Scala** (given) + +```scala +enum Paddle: + case Up, Down + +enum PongAction: + case Press(key: Paddle) + case Release(key: Paddle) + case Frame + case Restart +``` + +**F#** (given) + +```fsharp +type Paddle = PaddleUp | PaddleDown (* check the names in your Model.fs *) +type PongAction = + | Press of Paddle + | Release of Paddle + | Frame + | Restart +``` + +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: + +``` +pongKey(isDown, key): + ArrowUp / w / W → Press Up (isDown) | Release Up (not isDown) + ArrowDown / s / S → Press Down (isDown) | Release Down (not isDown) + r / R → Restart (only when isDown; releasing r does nothing) + anything else → nothing (null / None) +``` + +Question: why are `Press` and `Release` separate actions rather than one `SetHeld(key, bool)`? (There's no single right answer; think about what reads better in `update` and in fixtures.) + +### Step 3 — Write `update` as a fold + +`pongUpdate(state, action)` is your Pong `update`. `Press`/`Release` set a field of `held`; `Restart` is one call; `Frame` is the seven-step pipeline from [the rules](#the-rules-precisely). + +The `Frame` case is the biggest function you'll write in the course. The way to keep it readable is the way you kept Snake readable: **split it into small, pure, separately-understandable steps**, each taking a state and returning a state, applied in the order of the rules. You'll want a `clamp(lo, hi, x)` helper (it's tested on its own). + +Questions to answer before writing code: + +- The cpu "looks at the ball before the ball moves", and the player paddle bounce uses "the player's new position". What does that tell you about the order of your steps? +- Where does the serve's randomness come from? Which Module 4 function do you call, and what must you do with the seed it returns? +- Steps 4–6 compute `ny`, then `nx`, then maybe a score. Will you do the ball in one function or several? If several, how do they share `ny`? + +### Step 4 — Write the view as data + +`pongView(state)` returns a `Scene` — the same `Shape`s as Snake. You already know how to do this; follow [the picture spec](#the-rules-precisely) exactly. + +### Step 5 — Effects as values + +Which effects does Pong need? Drawing, and… is there anything else? The serve's randomness is *pure* (it's in the seed). The only impure randomness is the seed at the start, which `randomSeed` already describes. So: **no new `Op`, no change to `IO`.** That is worth pausing on. A whole second game with randomness, a clock, held keys and a computer opponent needs exactly the effects you already have. + +### Step 6 — Wire the streams + +Like Snake's `app`: + +- `pongActions`: `Frame` every `FRAME_MS_PONG = 30` ms (from `interval`), merged with `pongKey(true, key)` of every key press (`keyDowns`) and `pongKey(false, key)` of every key release (`keyUps`). That's three streams merged — in which order? (The test `pongActions turns the clock into Frames and keys into Press, Release and Restart` tells you what it expects; read it.) +- `pongStates(seed, actions)`: start from `initialPong(seed)`, fold with `pongUpdate`, emit the initial state first. +- `pongMain`: ask for a seed; turn the stream of states into a stream of drawings of `pongView`. + +### 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. + +## Exercises + +### Exercise 11.1 — model + +**JavaScript** (`js/src/pong/model.js`; the constants `W`, `H`, `PCELL`, `PADDLE`, `PLAYER_X`, `CPU_X`, `WIN` are given) + +```js +export const initialPong = (seed) => /* PongState */ +export const PongAction = { Press: (key) => ..., Release: (key) => ..., Frame: ..., Restart: ... } +``` + +**Scala** (`scala/src/pong/Model.scala`) + +```scala +object PongModel: + def initialPong(seed: Long): PongState = ??? +``` + +**F#** (`fsharp/src/Pong/Model.fs`) + +```fsharp +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`. + +### Exercise 11.2 — rules + +**JavaScript** (`js/src/pong/rules.js`) + +```js +export const clamp = (lo, hi, x) => /* number */ +export const pongUpdate = (state, action) => /* PongState */ +``` + +**Scala** (`scala/src/pong/Rules.scala`) + +```scala +object PongRules: + def clamp(lo: Int, hi: Int, x: Int): Int = ??? + def pongUpdate(state: PongState, action: PongAction): PongState = ??? +``` + +**F#** (`fsharp/src/Pong/Rules.fs`) + +```fsharp +let clamp (lo: int) (hi: int) (x: int) : int = failwith "TODO m11" +let pongUpdate (state: PongState) (action: PongAction) : PongState = failwith "TODO m11" +``` + +**Proved by:** `clamp keeps a number inside [lo, hi]`, `pongUpdate does not modify the state it is given`, `an idle match stays on the court and ends once someone reaches five`, and `fixtures/m11-pong-update.json` — 43 named cases. Read the case names: they're the specification broken into single facts (`player-stops-at-the-top-edge`, `cpu-does-not-move-on-an-odd-frame`, `paddle-bounce-uses-the-player-position-after-it-moved`, `serve-direction-comes-from-the-seed-odd-value-serves-upward`, `cpu-reaching-five-ends-the-match`, …). When one fails, open the fixture, find the case, and work it out by hand. + +**Strategy:** don't write all seven steps and then run the tests. Write `Press`/`Release`/`Restart` and run; then step 1 and 2 of `Frame` and run (watch the paddle cases go green); then the cpu; then the ball. Let the case names guide you. + +### Exercise 11.3 — view + +**JavaScript** (`js/src/pong/view.js`) + +```js +export const pongView = (state) => /* Scene */ +``` + +**Scala** (`scala/src/pong/View.scala`) + +```scala +object PongView: + def pongView(state: PongState): Scene = ??? +``` + +**F#** (`fsharp/src/Pong/View.fs`) + +```fsharp +let pongView (state: PongState) : Scene = failwith "TODO m11" +``` + +**Proved by:** `fixtures/m11-pong-view.json` (`new-match`, `paddles-and-ball-move-with-the-state`, `player-wins`, `cpu-wins`). + +### Exercise 11.4 — keys and wiring + +**JavaScript** (`js/src/pong/app.js`) + +```js +export const FRAME_MS_PONG = 30 +export const pongKey = (isDown, key) => /* PongAction or null */ +export const pongActions = /* Stream */ +export const pongStates = (seed, actions) => /* Stream */ +export const pongMain = /* IO>> */ +``` + +**Scala** (`scala/src/pong/App.scala`) + +```scala +object PongApp: + val FRAME_MS_PONG: Long = 30 + def pongKey(isDown: Boolean, key: String): Option[PongAction] = ??? + lazy val pongActions: Stream[PongAction] = ??? + def pongStates(seed: Long, actions: Stream[PongAction]): Stream[PongState] = ??? + lazy val pongMain: IO[Stream[IO[Unit]]] = ??? +``` + +**F#** (`fsharp/src/Pong/App.fs`) + +```fsharp +let FRAME_MS_PONG = 30L +let pongKey (isDown: bool) (key: string) : PongAction option = failwith "TODO m11" +let pongActions : Stream = (* ... *) +let pongStates (seed: uint32) (actions: Stream) : Stream = failwith "TODO m11" +let pongMain : IO>> = (* ... *) +``` + +**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 +- Scala: `cd scala && ./test.sh M11`; then `./build-web.sh`, serve, open +- F#: `cd fsharp && ./test.sh M11`; then `./build-web.sh`, serve, open + +Finally run the **whole** suite of your track (`./test.sh`). The purity audit now scans your Pong files too. + +## Hints + +The hints for this module are deliberately lighter. Before opening one, look at the equivalent piece of your Snake code; that's the real hint. + +
Exercise 11.2 — nudge + +Write `frame(state)` as a composition of steps, each `PongState → PongState`: `count`, `movePlayer`, `moveCpu`, `moveBall` (which may itself call `serve`), `checkOver`. In JS, `pipe` from Module 1 (or a chain of calls) reads top to bottom in rule order; in Scala `andThen`; in F# `|>`. Which of these steps need the guard "only when Playing"? (Only the whole `Frame`.) + +
+ +
Exercise 11.2 — stronger hint + +- `moveCpu` compares `cpu` with `target = ball.y - PADDLE / 2`: below target → +1, above → -1, equal → 0 (a "sign" function); clamp; and only on even `frame` — which, after `count`, is the *new* frame. +- In `moveBall`, compute `dy`/`ny` first, then `dx`/`nx` using the already-moved `player`/`cpu` from the state. Then: if `nx < 0` or `nx >= W`, return `serve(...)` with the right scorer; otherwise, the state with the new ball and velocity. +- `serve`: `[v, seed'] = nextUint32(state.seed)`; `dy = v % 2 === 0 ? 1 : -1`. In Scala the seed is a `Long`, in F# a `uint32`; the fixtures are the same in all tracks, so watch for sign problems. +- Did your serve test fail on the score but pass on the ball? Check that you add the point *and* serve, in the same step. + +
+ +
Exercise 11.4 — nudge + +Three sources, one `merge` chain: `merge(merge(frames, presses), releases)`. `presses` is `keyDowns` through `filterMap` with `pongKey(true, …)`; `releases` similarly. The rest is your Snake `app` with different names. + +
+ +## Check your understanding + +1. In your own words, apply the first two steps of the method to the stateful sketch: list its hidden state (including hidden time and randomness) and its events. +2. Why is `held` in `PongState` and not in the runtime? What would break in testing if the runtime tracked held keys and passed "is up held?" to `update`? +3. Predict: the player holds `ArrowUp` and, while still holding it, presses and releases `ArrowDown`. What is `held` after each of the three events? Which way does the paddle move on frames in between? +4. The cpu moves on even frames and looks at the ball before it moves. What would change in the fixtures if it looked *after* the ball moved? Find the case that pins this down (hint: its name mentions "before"). +5. You reused `IO`, `Stream`, `runIO`, `runApp` and `nextUint32` without changing a line. List what was Snake-specific in the course, and what was general infrastructure. What does that suggest about where effort should go in a functional codebase? +6. Pong's serve direction is random. Explain why `pongUpdate` is still a pure function, and how a test can check a "random" serve exactly. +7. Imagine a two-player mode: W/S for the left paddle, arrows for the right. Which steps of the method change, and which files? Is there any change to the runtime? +8. Compare your own design from steps 1–2 with the canonical one. Name one place where yours was different and argue for or against it. + +## In production + +- **The Elm Architecture** and **Elmish** are this method frozen into a framework: model (step 1), messages (step 2), update (step 3), view (step 4), commands (step 5), subscriptions (step 6), and a runtime you don't write (step 7) ([The Elm Architecture](https://guide.elm-lang.org/architecture/), [Elmish](https://elmish.github.io/elmish/)). +- **Redux** applies steps 1–3 to web apps; its "Redux Style Guide" rules (state is serializable, reducers are pure, no side effects in reducers) are the pitfalls list of the method ([Redux](https://redux.js.org/)). +- **Deterministic lockstep** multiplayer games (many real-time strategy games) send only *inputs* between machines and run the same pure update on each one; they depend on exactly the property you just built — same seed and same inputs give the same game ([Glenn Fiedler, "Deterministic Lockstep"](https://gafferongames.com/post/deterministic_lockstep/)). + +## Further reading + +- [The method](method.md) — the recipe you applied, with a worked example and a checklist. +- [Pong (Wikipedia)](https://en.wikipedia.org/wiki/Pong) — history and the original rules. +- [The Elm Architecture](https://guide.elm-lang.org/architecture/) — read it again now; every part should look familiar. +- [Glenn Fiedler, "Deterministic Lockstep"](https://gafferongames.com/post/deterministic_lockstep/) — what pure, deterministic game updates make possible in networked games. +- [John Hughes, "Why Functional Programming Matters"](https://www.cs.kent.ac.uk/people/staff/dat/miranda/whyfp90.pdf) — reread the conclusion with the whole course behind you. diff --git a/course/method.md b/course/method.md new file mode 100644 index 0000000..6ec096c --- /dev/null +++ b/course/method.md @@ -0,0 +1,376 @@ +# The method: from a stateful game to a functional one + +> **When to read this:** after Module 9 (you need `IO`, streams and the runtime), and before Module 11, which asks you to apply it to Pong on your own. Come back to it whenever you convert something. +> **What it is:** the course, compressed into a recipe you can carry to other programs — with a small worked example, a checklist and a list of the mistakes people make most often. + +## The recipe in one page + +You've built Snake in a particular order. That order wasn't arbitrary: each step removes one kind of hidden impurity and replaces it with a value. Here it is as a general method. + +| Step | Question to ask the stateful code | What you produce | Where you did it for Snake | +|---|---|---|---| +| 0 | What does it remember, react to, and do? | an inventory | (implicitly, Module 2) | +| 1 | **State → value.** Which variables change over time? | one immutable state type and an initial value | Module 2 (`GameState`, `initialState`) | +| 2 | **Events → data.** What can happen to it? | an action type (a tagged union) | Module 2 (`Action`), Module 9 (`keyToAction`) | +| 3 | **Update → fold.** How does each event change the state? | `update(state, action) → state`, pure | Modules 3–5, 10 | +| 4 | **View → data.** What does it show? | `view(state) → Scene` | Module 6 | +| 5 | **Effects → values.** What does it ask of or do to the world? | `Op`s, `IO` programs | Module 7 | +| 6 | **Wiring → streams.** Where do events come from over time? | action stream, `startWith(scan(…))`, `main` | Modules 8–9 | +| 7 | **One runtime at the edge.** Who performs it all? | one interpreter, one mutable variable | Module 9 | + +Two rules hold throughout: + +- **Hidden inputs become explicit inputs.** Time, randomness, keyboard: if the code *reads* it from somewhere, it must instead *receive* it — as an argument, a field of the state, or an event. +- **Hidden outputs become returned values.** Drawing, saving, logging: if the code *does* it, it must instead *return a description* of it. + +The rest of this page walks the recipe through a program small enough to see in one go. + +## A worked example: the auto-counter + +### The stateful version + +Here is a tiny program written the usual way. It shows a number on a canvas. Clicking a button adds one. Every second, it also adds one by itself. Pressing R resets it to zero. + +```html + + +``` + +```js +// The usual, stateful way: the program we are going to convert +let count = 0 +const ctx = document.getElementById('c').getContext('2d') + +function draw() { + ctx.fillStyle = '#111111' + ctx.fillRect(0, 0, 200, 100) + ctx.fillStyle = '#ffffff' + ctx.font = '32px monospace' + ctx.textAlign = 'center' + ctx.fillText(String(count), 100, 60) +} + +document.getElementById('bump').addEventListener('click', () => { count++; draw() }) +window.addEventListener('keydown', (e) => { if (e.key === 'r' || e.key === 'R') { count = 0; draw() } }) +setInterval(() => { count++; draw() }, 1000) +draw() +``` + +Fifteen lines, and it works. Let's convert it anyway, because the conversion is the point, and because every problem of a big game is already visible here in miniature. + +### Step 0 — Make an inventory + +Read the code and sort everything into three lists. Do it on paper before reading the answer. + +| Remembers (state) | Reacts to (events) | Does (effects) | +|---|---|---| +| `count` | a click on the button | draws on the canvas (5 calls that depend on hidden `ctx` settings) | +| *the phase of the timer* (when the next second will fire) | a key going down (only R matters) | starts a timer | +| | a second passing | registers two listeners | + +Did you get the second line of the first column? It's easy to miss: `setInterval` *remembers* when it last fired. That's state, too — **hidden time**. Here's a question the stateful code answers silently and you now have to answer on purpose: *when the user presses R at 2.9 s, should the next automatic increment come at 3.0 s (0.1 s later) or at 3.9 s (a full second later)?* The stateful code says 3.0 s, without anyone having decided it. Converting forces the decision into the open. We'll keep the stateful behaviour (the clock beats on its own, regardless of resets), because that's exactly what `interval` does. + +### Step 1 — State becomes a value + +All the program remembers is a number. We'll still wrap it in a record, because a game state always grows (a high score, a pause flag…), and adding fields to a record is easier than turning a number into a record later. + +**JavaScript** + +```js +// CounterState = { count: number } +export const initialCounter = { count: 0 } +``` + +**Scala** + +```scala +final case class CounterState(count: Int) +val initialCounter = CounterState(0) +``` + +**F#** + +```fsharp +type CounterState = { Count: int } +let initialCounter = { Count = 0 } +``` + +The timer's phase is *not* in the state. Our `interval` stream (Module 8) carries its own `last` from step to step; the game doesn't need to know it. + +### Step 2 — Events become data + +What can happen? Look at the "reacts to" column, and describe each event by what it *means* to the program, not by where it comes from: + +**JavaScript** + +```js +// CounterAction = {tag:'Bump'} | {tag:'Second'} | {tag:'Reset'} +export const CounterAction = { Bump: { tag: 'Bump' }, Second: { tag: 'Second' }, Reset: { tag: 'Reset' } } +``` + +**Scala** + +```scala +enum CounterAction: + case Bump, Second, Reset +``` + +**F#** + +```fsharp +type CounterAction = Bump | Second | Reset +``` + +Now the click. Our `Platform` from Module 9 knows keys and time, but not mouse clicks. There are two honest options: + +1. **Extend the edge.** Add a `{tag:'Click'}` event to the stream inputs, an `onClick(cb)` method to the `Platform`, a `clicks` source stream, and one line in `runApp` to register it. Notice that *every one of those changes is at the edge or in the stream library*; the counter's logic wouldn't change. +2. **Map another input to the same meaning.** Let Space mean "bump". + +We'll take option 2 here so that the example runs on your existing runtime unchanged — and that choice is only possible *because* we named the action by its meaning (`Bump`), not by its source (`Click`). The translation from keys is a pure function, like `keyToAction`: + +**JavaScript** + +```js +export const keyToCounterAction = (key) => { + switch (key) { + case ' ': return CounterAction.Bump + case 'r': case 'R': return CounterAction.Reset + default: return null + } +} +``` + +**Scala** + +```scala +def keyToCounterAction(key: String): Option[CounterAction] = key match + case " " => Some(CounterAction.Bump) + case "r" | "R" => Some(CounterAction.Reset) + case _ => None +``` + +**F#** + +```fsharp +let keyToCounterAction (key: string) : CounterAction option = + match key with + | " " -> Some Bump + | "r" | "R" -> Some Reset + | _ -> None +``` + +### Step 3 — The update is a fold + +Each callback in the stateful code did "change `count`, then draw". Keep the first half, drop the second (drawing is step 4's job), and turn "change `count`" into "return a new state": + +**JavaScript** + +```js +export const counterUpdate = (state, action) => { + switch (action.tag) { + case 'Bump': + case 'Second': return { ...state, count: state.count + 1 } + case 'Reset': return initialCounter + } +} +``` + +**Scala** + +```scala +def counterUpdate(state: CounterState, action: CounterAction): CounterState = action match + case CounterAction.Bump | CounterAction.Second => state.copy(count = state.count + 1) + case CounterAction.Reset => initialCounter +``` + +**F#** + +```fsharp +let counterUpdate (state: CounterState) (action: CounterAction) : CounterState = + match action with + | Bump | Second -> { state with Count = state.Count + 1 } + | Reset -> initialCounter +``` + +The first payoff is immediate: the whole behaviour can be tested with a list, no browser, no waiting a second: + +```js +const actions = [CounterAction.Bump, CounterAction.Second, CounterAction.Bump, CounterAction.Reset, CounterAction.Second] +assert.deepStrictEqual(actions.reduce(counterUpdate, initialCounter), { count: 1 }) +``` + +That's Module 5's "a program is a fold over its events", in one line. + +### Step 4 — The view returns data + +The stateful `draw` set three hidden context properties and made two calls. As a `Scene`, every setting travels with its shape: + +**JavaScript** + +```js +export const counterView = (state) => [ + { tag: 'Rect', x: 0, y: 0, w: 200, h: 100, color: '#111111' }, + { tag: 'Text', x: 100, y: 60, text: String(state.count), color: '#ffffff', size: 32, align: 'center' }, +] +``` + +**Scala** + +```scala +def counterView(state: CounterState): Scene = List( + Shape.Rect(0, 0, 200, 100, "#111111"), + Shape.Text(100, 60, state.count.toString, "#ffffff", 32, "center"), +) +``` + +**F#** + +```fsharp +let counterView (state: CounterState) : Scene = + [ Rect (0, 0, 200, 100, "#111111") + Text (100, 60, string state.Count, "#ffffff", 32, "center") ] +``` + +Test: `assert.deepStrictEqual(counterView({ count: 7 })[1].text, '7')`. + +### Step 5 — Effects become values + +Which effects remain? Drawing — and that's covered: `drawScene(counterView(state))` is an `IO` that draws a frame. Starting the timer and registering listeners aren't effects *of the program*; they're the runtime's job (step 7). And there's no randomness, so we don't need `randomSeed`. + +That last observation has a nice consequence. `main` must have type `IO>>`, but our counter needs nothing from the world to *build* its stream. The `IO` that does nothing and produces a value is `pure` from Module 7. So the outer layer will be `pure(…)`. + +### Step 6 — Wire the streams + +Exactly the shape of Snake's `app`: + +**JavaScript** + +```js +import { pure, drawScene } from './io.js' +import { interval, keyDowns, map, filterMap, merge, scan, startWith } from './stream.js' + +export const counterActions = merge( + map(interval(1000), () => CounterAction.Second), + filterMap(keyDowns, keyToCounterAction), +) +export const counterStates = startWith(scan(counterActions, counterUpdate, initialCounter), initialCounter) +export const counterMain = pure(map(counterStates, (s) => drawScene(counterView(s)))) +``` + +**Scala** + +```scala +lazy val counterActions: Stream[CounterAction] = + Stream.interval(1000).map(_ => CounterAction.Second).merge(Stream.keyDowns.filterMap(keyToCounterAction)) +lazy val counterStates: Stream[CounterState] = counterActions.scan(initialCounter)(counterUpdate).startWith(initialCounter) +lazy val counterMain: IO[Stream[IO[Unit]]] = IO.pure(counterStates.map(s => IO.drawScene(counterView(s)))) +``` + +**F#** + +```fsharp +let counterActions : Stream = + Stream.merge (Stream.interval 1000L |> Stream.map (fun _ -> Second)) (Stream.keyDowns |> Stream.choose keyToCounterAction) +let counterStates : Stream = + counterActions |> Stream.scan counterUpdate initialCounter |> Stream.startWith initialCounter +let counterMain : IO>> = IO.pure (counterStates |> Stream.map (counterView >> IO.drawScene)) +``` + +And the second payoff: the *timing* behaviour, which in the stateful version you could only check by watching a screen for a few seconds, is now a pure test: + +```js +const time = (t) => ({ tag: 'Time', t }), down = (key) => ({ tag: 'KeyDown', key }) +assert.deepStrictEqual( + run(counterStates, [time(500), down(' '), time(1000), down('r'), time(2100)]).map((s) => s.count), + [0, 1, 2, 0, 1]) +``` + +Work it out by hand: at 500 ms nothing ticks but `startWith` emits the initial state; Space bumps to 1; at 1000 ms the first second ticks (2); R resets (0); at 2100 ms the second "second" ticks (1). This test also *documents* the decision from step 0 — the reset didn't restart the clock. + +### Step 7 — One runtime at the edge + +```js +// counter-main.js — the only impure line of the program +runApp(counterMain, browserPlatform(document.getElementById('c'), window)) +``` + +Nothing to write: the runtime from Module 9 runs it. Compare with where we started. The fifteen stateful lines had a mutable `count`, three callbacks that each both changed state and drew, and a timer with hidden phase. Now there's a state type, an action type, three pure functions (`keyToCounterAction`, `counterUpdate`, `counterView`), three lines of wiring, and one line at the edge. It's longer — for a fifteen-line program, the functional version *is* longer. What you bought is that every behaviour, including the timing and the reset decision, is a value you can test with `deepEqual`, and the program can grow without anybody having to remember which callbacks touch which variables. + +*(Want to run it? Put the JS in a file next to `app.js`, copy `index.html` to `counter.html` with a 200 × 100 canvas with id `c`, and point its script tag to a `counter-main.js` containing the line above plus the three imports.)* + +## The checklist + +Use this when you convert anything. Each line is a yes/no question about your result. + +**State** +- [ ] Is everything the program remembers in one immutable state value (or derivable from it)? +- [ ] Is there a function that builds the initial state from explicit inputs (e.g. a seed)? +- [ ] Is anything stored twice (e.g. a `length` next to the list it measures)? Remove derived fields. + +**Events** +- [ ] Is every way the program can be influenced an action value? +- [ ] Are actions named by *meaning* (`Bump`, `Turn Up`), with a pure translation from raw input (`keyToAction`)? +- [ ] Is "a key being held" (if needed) tracked in the state from press/release events? + +**Update** +- [ ] Is `update(state, action)` pure: no reads of time, randomness, the DOM, or globals; no writes anywhere? +- [ ] Is randomness a seed in the state, threaded through a `Random` function? +- [ ] Does every action handle every status explicitly (paused, over…)? +- [ ] Can you `replay` a list of actions in a test? + +**View** +- [ ] Does `view(state)` return data (a `Scene`) and nothing else? + +**Effects** +- [ ] Is every interaction with the world an `Op`, described by an `IO` value? +- [ ] Can a pure interpreter (`trace`) observe every effect in a test? + +**Wiring and edge** +- [ ] Is `main` a value of type `IO>>` (or similar)? +- [ ] Is there exactly one impure runtime, holding exactly one mutable variable (the current stream), that knows nothing about the game? +- [ ] Is time an *input* (events), never a *query*? + +## Common pitfalls + +**Hidden time.** `Date.now()` or `performance.now()` inside the logic, "move faster after 30 seconds" implemented by reading a clock, or a speed that depends on how often the browser calls you. *Fix:* time arrives as `Time t` events; turn it into actions with `interval`; measure durations by counting frames in the state (Module 10's `frame` and `movesEvery`). + +**Hidden randomness.** `Math.random()` in the update ("spawn the apple somewhere"). It makes tests flaky and replays impossible. *Fix:* a seed in the state, `[value, nextSeed] = nextUint32(seed)`, and store the next seed (Module 4). Only the *first* seed comes from the world, through `randomSeed`. + +**Mutation in loops.** `for (…) result.push(…)`, `board[y][x] = …`, `list.sort()` (which sorts in place). It's tempting when the rule is naturally "go through all the bricks and remove the hit ones". *Fix:* `map`/`filter`/`reduce`, building new arrays; `[...xs].sort()` or a non-mutating `toSorted()` if you must sort; for grids, a list of rows rebuilt with `map`. + +**Effects in update.** A `console.log` for debugging that stays, a `localStorage.setItem` "just when the game ends", a sound played on eating. Each one makes `update` untestable and order-dependent. *Fix:* return data. For a sound or a save, add an `Op` and produce the `IO` from the state stream (see Module 10's high-score thought exercise). For debugging, use `history` and `trace` instead of logging. + +**State in the runtime.** "I'll just keep `upHeld` in the runtime and pass it in." Then the runtime knows about the game and half the behaviour can't be replayed. *Fix:* the runtime holds only the current stream; held keys are state derived from `Press`/`Release` actions (Module 11). + +**Changing the clock instead of counting.** `clearInterval`/`setInterval` to speed up. *Fix:* one fixed clock; decide in `update` which frames matter (Module 10). + +**Forgetting to wrap the next stream.** A combinator that returns the inner `next` instead of `combinator(next, …)` works for exactly one event. *Fix:* every combinator's next stream is the same combinator around the inner next stream(s) (Module 8). + +**Left-nested `flatMap` chains.** Building a long sequence with a left fold overflows the stack in the interpreter's continuations. *Fix:* fold right (Module 7, section 12). + +## Where to go next: more conversions + +Each of these is a good exercise in the method. Start from the inventory (step 0), write the state and action types first, and write fixtures for `update` before the code. + +**Tetris** ([Wikipedia](https://en.wikipedia.org/wiki/Tetris)). +- *State:* the board as a list of rows (each a list of cells, empty or a colour), the falling piece (shape, rotation, position), the next piece, score, lines, level, seed, frame, status. +- *Events:* `Left`, `Right`, `Rotate`, `SoftDrop`, `HardDrop`, `Frame`, `Pause`, `Restart`. +- *Hints:* gravity is `movesEvery(level)` on `Frame`, exactly like Snake's speed. Rotation is a pure function on a list of points; check collisions by asking "would the rotated piece overlap or leave the board?" and keep the old piece if so. Line clearing is `filter` (remove full rows) plus prepending empty rows. The "7-bag" randomizer (shuffle the seven pieces, deal them out) is a nice use of the `Random` state monad: a shuffle is a `Random`. + +**Breakout** ([Wikipedia](https://en.wikipedia.org/wiki/Breakout_%28video_game%29)). +- *State:* ball, velocity, paddle x, held keys, bricks, lives, score, status. +- *Events:* `Press`/`Release` (left/right), `Frame`, `Launch`, `Restart`. +- *Hints:* reuse Pong's held-key pattern and its step-by-step frame pipeline. Represent bricks as a list (or a set) of positions; a collision step returns the remaining bricks and the bounced velocity together. Decide, and write a fixture for, what happens when the ball hits two bricks in one frame. + +**Conway's Game of Life** ([Wikipedia](https://en.wikipedia.org/wiki/Conway%27s_Game_of_Life)). +- *State:* the set of live cells (a list of points, or a set of `"x,y"` strings in JS), generation, paused flag, speed. +- *Events:* `Frame`, `TogglePause`, `Step` (one generation while paused), `Clear`, `Randomize` (needs a seed), and — for editing — clicking a cell. +- *Hints:* `step(cells)` is the purest function you'll ever write: count neighbours with `map`/`filter`, keep cells with 2–3 neighbours, birth cells with exactly 3. Clicking needs a new input type and a new `Platform` method (`onClick(cb: (x, y) => void)`): that's a change to the stream inputs, the runtime and the browser platform, but *not* to the game logic. Doing it will teach you exactly where the edge is. + +## Further reading + +- [The Elm Architecture](https://guide.elm-lang.org/architecture/) — this method as a framework; compare its parts with the steps above. +- [Gary Bernhardt, "Functional Core, Imperative Shell"](https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell) — the architectural idea behind steps 5–7. +- [Redux: Style Guide](https://redux.js.org/style-guide/) — a production team's list of the pitfalls above (pure reducers, serializable state, no side effects in reducers). +- [*Functional Programming in Scala*, 2nd edition](https://www.manning.com/books/functional-programming-in-scala-second-edition) — chapters 6 (purely functional state), 13 (external effects and I/O) and 15 (stream processing) cover steps 3, 5 and 6 in depth. +- [Robert Nystrom, Game Programming Patterns](https://gameprogrammingpatterns.com/game-loop.html) — the "Game Loop" and "Update Method" chapters show the stateful designs you now know how to convert.