diff --git a/course/00-setup.md b/course/00-setup.md new file mode 100644 index 0000000..17bb215 --- /dev/null +++ b/course/00-setup.md @@ -0,0 +1,346 @@ +# Module 0 — Setup and how to use this course + +> **Time:** ~1 h · **You'll edit:** nothing yet · **Tests:** JS `cd js && node --test "test/m00-*.test.js"` · Scala `cd scala && ./test.sh M00` · F# `cd fsharp && ./test.sh M00` +> **Prerequisites:** one course in Scala (or similar): you know `map`, `filter`, `fold`, case classes and pattern matching + +## Why this module + +You probably already know how to write Snake. Something like this: + +```js +// The usual way. Every line changes something somewhere. +let snake = [{ x: 10, y: 10 }, { x: 9, y: 10 }] +let apple = { x: 3, y: 4 } +setInterval(() => { + const head = { x: snake[0].x + dx, y: snake[0].y + dy } + snake.unshift(head) + if (head.x === apple.x && head.y === apple.y) apple = randomCell() + else snake.pop() + draw() +}, 150) +document.addEventListener('keydown', (e) => { /* change dx, dy */ }) +``` + +It works. But try to answer these questions about it: + +- How would you write a test that checks "the snake grows when it eats"? You would have to set global variables, wait for a timer, and somehow control `randomCell()`. +- A player reports "the snake died when it shouldn't have". Can you reproduce exactly what happened? +- What does `draw()` depend on? What does it change? You have to read the whole program to know. + +In this course you write the same game in a way where every one of those questions has an easy answer. The game rules, the randomness, the drawing, the clock, the keyboard and even "the program" itself are ordinary values and pure functions. Only one tiny file at the very edge actually *does* anything. By the end you will have done the conversion yourself, step by step, and you will be able to explain why each step helps. + +This module gets your tools working and shows you how the course is organised. There is nothing to implement yet. + +## Learning objectives + +After this module you can: + +- **describe** the architecture you are going to build, and which part of it is allowed to have side effects; +- **run** the tests of one module, or all of them, in your chosen track; +- **read** a red test and find the fixture case it is checking; +- **explain** what the purity audit forbids and why; +- **choose** between solo mode and AI-mentor mode, and know when to use the checkpoints in `solutions/`. + +## Concepts + +### What you will build + +First **Snake**: a 20 x 20 board, a snake that moves one cell per tick, apples that appear on random free cells, a score, pause, restart, a speed-up as you score. It runs in the browser on a ``. + +Then, in the capstone (Module 11), **Pong**: you convert a second game on your own, applying a recipe you have practised through the whole course. + +### The big picture + +Here is the whole program you are going to build. Keep coming back to this picture; every module fills in one box. + +``` + +-------------------------------- PURE CORE ----------------------------------+ + | | + | m02 model m03 rules m04 random, spawn m05 game | + | GameState --> turn, advance --> spawnApple, tick --> update(state, a) | + | replay, history | + | | | + | m06 view v | + | view : State -> Scene (shapes) | + | | + | EFFECTS AS VALUES (descriptions, nothing happens yet) | + | m07 IO "draw this rectangle", "give me a random seed" | + | m08 Stream a pure automaton: Input -> (outputs, next stream) | + | m09 main : IO>> -- the whole program, as ONE value | + +-------------------------------------+---------------------------------------+ + | main + v + +---------------- THE EDGE (m09 runtime, the only impure file) ---------------+ + | runIO / runApp: interprets IO, feeds clock ticks and key presses into the | + | stream, performs the drawing. One mutable variable, about a page of code. | + +-------------------------------------+---------------------------------------+ + ^ keys, time | pixels + | v + browser (canvas, keyboard, clock, crypto) +``` + +Read it from the top. The **pure core** is plain data and plain functions: given a state and an action, `update` computes the next state; given a state, `view` computes a list of shapes. Below that, **effects as values**: an `IO` is a *description* of something to do (like a recipe), and a `Stream` is a *description* of how to react to inputs over time. Building these descriptions changes nothing in the world. Finally, at **the edge**, a small runtime takes the one value `main` and actually runs it. + +> **From Scala:** if you have heard of cats-effect or ZIO, this is the same idea in miniature. You will build `IO` and `Stream` yourself, from scratch, in about a hundred lines each, so there is nothing magic left. + +This design is sometimes called *functional core, imperative shell* (Gary Bernhardt). We push it further than usual: even the shell is mostly pure, and the truly imperative part is one function. + +### Choosing a track + +The course text is shared. Every code listing that you would write appears three times, always in this order: **JavaScript**, **Scala**, **F#**. Pick one track and stay with it; you can read the others for comparison. + +| Track | Why pick it | Tests with | Runs in the browser via | +|---|---|---|---| +| **JavaScript** (primary) | No compiler, no types: you must hold the discipline yourself, which teaches the most. Zero dependencies. | Node's built-in `node --test` | plain ES modules | +| **Scala 3** | Closest to what you already know. The compiler helps you (exhaustive matches, immutable by default). | munit via `scala-cli` | Scala.js | +| **F#** | An ML-family language: lighter syntax, data-last functions and pipelines. A good second functional language. | Expecto via `dotnet` | Fable | + +If you are unsure, take JavaScript. It is the primary track, and Module 1 bridges from Scala to JavaScript. The three tracks use the same names where they can. Where they differ, the text tells you the first time it matters, and `course/reference.md` has the full table. The most common differences: + +- JS functions take the data first: `map(io, f)`. Scala uses methods: `io.map(f)`. F# takes the data last so that pipelines work: `io |> IO.map f`. +- `flatMap` is called `bind` in F#. +- "No apple" is `null` in JS and `None` (an `Option`) in Scala and F#. +- Random seeds are `number` in JS, `Long` in Scala, `uint32` in F#. + +### Installing the tools + +You need Git and the toolchain for your track. You can install all three if you like. + +**JavaScript:** Node.js **22 or newer** (the course is tested on Node 24). Download it from [nodejs.org/en/download](https://nodejs.org/en/download). There are no npm packages to install. + +```sh +node --version # v22.x or higher +``` + +**Scala:** [scala-cli](https://scala-cli.virtuslab.org/install). It downloads a JDK, the Scala 3 compiler and the test library by itself the first time you run it, so the first run is slow; later runs are fast. + +```sh +scala-cli version +``` + +**F#:** the [.NET SDK 10](https://dotnet.microsoft.com/en-us/download/dotnet/10.0) (installation help: [learn.microsoft.com/dotnet/core/install](https://learn.microsoft.com/en-us/dotnet/core/install/)). The first test run restores the Expecto package from NuGet. + +```sh +dotnet --version # 10.x +``` + +An editor with language support helps a lot: VS Code with the Metals extension (Scala) or Ionide (F#), or IntelliJ / Rider. For JavaScript any editor is fine. + +### A tour of the repository + +``` +README.md front page +MENTOR.md instructions for an AI mentor (see "Solo or with a mentor") +course/ this text: 00-setup.md ... 11-capstone-pong.md, reference.md, method.md +fixtures/ shared test cases as JSON, used by all three tracks +js/ src/ YOUR code: stubs to fill in, one file per topic + solutions/ reference solutions, same file names (try first!) + test/ tests, one file per module: m03-rules.test.js, ... +scala/ src/ solutions/ test/ (same idea; files are PascalCase: Rules.scala) +fsharp/ src/ solutions/ tests/ (same idea: Rules.fs) +scripts/serve.mjs a tiny web server for playing the game in the browser +``` + +Four folders matter: + +- **`src/`** is where you work. Every file already exists with the final function names, signatures and a doc comment that says *what* the function must do. The bodies are placeholders: `todo('m03')` in JS, `???` in Scala, `failwith "TODO m03"` in F#. Your job is to replace them. +- **`test/`** (F#: `tests/`) holds the tests. Each module has its own test file, so finishing Module 3 does not depend on Module 6. +- **`fixtures/`** holds most of the pure-logic test cases as language-neutral JSON. For example `fixtures/m03-advance.json` contains cases named `advance-plain-move`, `advance-into-vacating-tail`, and so on, each with `args` and the `expected` result. All three tracks read the same files, so the game behaves identically in every language. When a test fails, the fixture tells you exactly which input was given and what was expected. +- **`solutions/`** holds a reference solution for every file in `src/`. It is there so you are never stuck for good (see the checkpoint policy below), and so a mentor can check your work. It is not there to read before you try. + +Each module tells you, in its header, which files you will edit. The map is the same in every track: + +| Module | JS file(s) in `js/src/` | Scala (`scala/src/`) | F# (`fsharp/src/`) | +|---|---|---|---| +| 01 | `util.js` | `Util.scala` | `Util.fs` | +| 02 | `model.js` | `Model.scala` | `Model.fs` | +| 03 | `rules.js` | `Rules.scala` | `Rules.fs` | +| 04 | `random.js`, `spawn.js` | `Random.scala`, `Spawn.scala` | `Random.fs`, `Spawn.fs` | +| 05 | `game.js` | `Game.scala` | `Game.fs` | +| 06 | `view.js` | `View.scala` | `View.fs` | +| 07 | `io.js` | `IO.scala` | `IO.fs` | +| 08 | `stream.js` | `Stream.scala` | `Stream.fs` | +| 09 | `input.js`, `app.js`, `runtime.js` (+ `browser.js`, `main.js`) | `Input.scala`, `App.scala`, `Runtime.scala` | `Input.fs`, `App.fs`, `Runtime.fs` | +| 10 | edits `game`, `input`, `app` | same | same | +| 11 | `pong/` | `pong/` | `Pong/` | + +### Running the tests + +From the repository root: + +**JavaScript** +```sh +cd js +node --test "test/m03-*.test.js" # one module +./test.sh # everything, including the purity audit +``` + +**Scala** +```sh +cd scala +./test.sh M03 # one module +./test.sh # everything +``` + +**F#** +```sh +cd fsharp +./test.sh M03 # one module +./test.sh # everything +``` + +Try it now with Module 0, which has a few sanity tests that already pass. They check that your toolchain works and that the fixtures folder can be read. If you see three green tests (Scala and F#: a short summary with no failures), you are set up. + +### What a red test means + +Run the Module 3 tests in a fresh checkout. Every one of them fails, and at the bottom Node repeats each failure with details: + +``` +✖ m03 move: move-up (0.91ms) +✖ m03 move: move-down (0.13ms) +... +✖ failing tests: + +test at test/m03-rules.test.js:9:5 +✖ m03 move: move-up (0.91ms) + Error: TODO: implement in m03 + at todo (.../js/src/todo.js:3:9) + at move (.../js/src/rules.js:4:33) +``` + +This is expected, and it is the starting point of every module. In Scala you see `scala.NotImplementedError: an implementation is missing` (that is what `???` throws), and in F# an exception with `TODO m03`. + +A red test is information, not a verdict. Read it in this order: + +1. **The test name.** Test names are sentences written for you: `dropLast leaves its input untouched`. Fixture tests are named after the function and the fixture case: `m03 advance: advance-into-vacating-tail`. +2. **The message.** `TODO: implement in m03` means you have not written that function yet. An assertion failure shows *expected* versus *actual*: compare them field by field. With game states, the difference is often in one field (`dir`, `seed`, `status`). +3. **The fixture.** For a fixture test, open `fixtures/-.json` and search for the case name. You will see the exact input. Draw it on paper; the boards in fixtures are deliberately tiny (3 x 2, 5 x 5) so that you can. + +A green test does not prove your code is right, and a red test never means you are bad at this. The tests are a conversation partner: they tell you which situation you have not thought about yet. + +### The purity rules + +> **The purity audit.** A test in every track (`purity.test.js`, `PurityTest`, `Purity`) scans your `src/` files (comments are ignored) and fails if it finds any of these: +> +> | Track | Banned outside the runtime files | +> |---|---| +> | JS | `let`, `var`, `++`, `--`, `.push(`, `.pop(`, `.shift(`, `.unshift(`, `.splice(`, `.sort(`, `.reverse(`, `Math.random`, `Date.now`, `performance.now`, `document`, `window`, `addEventListener` | +> | Scala | `var`, `mutable`, `while`, `scala.util.Random`, `System.`, `println` | +> | F# | `mutable`, `<-`, `System.Random`, `DateTime`, `printf`, `ref ` | +> +> Only the runtime files (JS `runtime.js`, `browser.js`, `main.js`; Scala `Runtime.scala`; F# `Runtime.fs`, plus the `web/` folders) may use them. +> +> **Why so strict?** Each banned word is a way to make a result depend on something other than the function's arguments, or to change something other than the function's result: +> - `let`/`var`/`++`/`--`/`<-`/`mutable`: reassignment. A name that means different things at different times. +> - `.push(`, `.sort(` and friends: they change an array in place, so everyone else holding that array sees the change. (Use `[...xs, x]`, `toSorted()`, `toReversed()` instead; they return new arrays.) +> - `Math.random`, `Date.now`, `System.Random`, `DateTime`: hidden inputs. The same call gives a different answer each time. +> - `document`, `window`, `addEventListener`, `println`, `printf`: talking to the outside world. +> +> Without `let` or `var` you cannot write a classic `for (let i = 0; ...)` loop. That is deliberate: you will use `map`, `filter`, `reduce`, recursion and small helper functions instead, which is what you know from Scala. +> +> The audit is a blunt text scan. It can be fooled, and it sometimes complains about harmless code (for instance a string containing `--`). It is a guard rail, not a proof. The real rule is the one behind it: **outside the edge, a function's result depends only on its arguments, and calling it changes nothing.** + +### Checkpoints: what to do when you are stuck + +Each module builds on the previous ones. If you get stuck, work through this list in order: + +1. Re-read the doc comment in the stub and the "Concepts" section of the module. +2. Open the failing fixture case and work it out on paper. +3. Open the module's **Hints**. Every exercise has three tiers: a *nudge*, a *stronger hint*, and a *near-solution* skeleton with holes. Open them one at a time. +4. Ask your mentor (human or AI), explaining what you tried. +5. Only then: **take the checkpoint.** Copy that module's files from `solutions/` into `src/` (for example `cp js/solutions/rules.js js/src/rules.js`) and continue with the next module. Because tests are organised per module, all earlier tests stay green. + +If you take a checkpoint, read the solution carefully and compare it with your attempt. Then close it and write the function again from memory a day later. That is where the learning happens. + +### Solo, or with an AI mentor + +The course works in two modes. + +- **Solo.** The text, the tiered hints and the tests carry you. Answer the "Check your understanding" questions at the end of each module out loud or on paper before moving on. If you cannot answer one, that is the part to re-read. +- **With an AI as a Socratic mentor.** [`MENTOR.md`](../MENTOR.md) at the repository root tells an AI assistant how to help you without doing the work for you: it asks questions before it explains, gives hints in the same order as the hint tiers, asks you to predict and explain, and writes solution code only if you explicitly give up on an exercise. Many coding assistants pick it up automatically (through `AGENTS.md` / `CLAUDE.md`); with others, start the conversation with "Read MENTOR.md and be my mentor for Module 3." + +A mentor that hands you answers feels productive and teaches very little. If your assistant starts writing your functions for you, remind it of `MENTOR.md`. + +### How each module is laid out + +Every module has the same parts, in this order: + +1. **Why this module**: the problem, usually what goes wrong in the stateful version. +2. **Learning objectives**: what you should be able to do afterwards. +3. **Concepts**: the ideas, with Scala bridges (blockquotes starting with **From Scala:**) and code in all three languages. +4. **Exercises**: the functions to write, with signatures in three languages, the tests and fixtures that check them, and pitfalls. +5. **Hints**: folded away; open them one tier at a time. +6. **Check your understanding**: questions you should be able to answer before moving on. Several ask you to predict an output or to say what would break if something changed. +7. **In production**: how real libraries and frameworks solve the same problem. +8. **Further reading**: links to the sources of the ideas. + +### Playing the game + +You can play the game once Module 9 is done. For reference, this is how: + +- **JS:** `node scripts/serve.mjs` from the repository root, then open http://localhost:8000/js/ +- **Scala:** `cd scala && ./build-web.sh` (the first run downloads the Scala.js linker), then serve as above and open http://localhost:8000/scala/ +- **F#:** `cd fsharp && ./build-web.sh`, serve, and open http://localhost:8000/fsharp/ + +## Exercises + +### Exercise 0.1 — Green lights + +Install the toolchain for your track and run the Module 0 tests. They should pass without any changes. + +- Run: JS `cd js && node --test "test/m00-*.test.js"` · Scala `cd scala && ./test.sh M00` · F# `cd fsharp && ./test.sh M00` + +### Exercise 0.2 — Read a red test + +Run the Module 3 tests (`m03` / `M03`). Pick the failing test whose name ends in `advance-into-vacating-tail`. Find that case in `fixtures/m03-advance.json` and draw the board from `args` on squared paper: mark the head, the body and the tail. Then draw the `expected` board. Write one sentence describing what happened between the two pictures. (You will meet this exact case again in Module 3.) + +### Exercise 0.3 — Break the purity audit on purpose + +In your track's Module 1 file (`js/src/util.js`, `scala/src/Util.scala`, `fsharp/src/Util.fs`) add a line that uses a banned construct, for example `let counter = 0` (JS), `var counter = 0` (Scala) or `let mutable counter = 0` (F#). Run the full test suite (`./test.sh`) and find the purity failure. Then remove the line again and check that the failure is gone. Now you know what the audit looks like when it catches you. + +## Hints + +
Exercise 0.1 — nudge + +If a command is not found, the tool is not on your `PATH`. Open a new terminal after installing, and check with `node --version`, `scala-cli version` or `dotnet --version`. + +
+ +
Exercise 0.1 — stronger hint + +The first Scala and F# runs download a lot (compiler, JDK, NuGet packages). Let them finish; if they fail with a network error, run them again. Make sure you run the command from inside the track folder (`js/`, `scala/` or `fsharp/`). + +
+ +
Exercise 0.2 — nudge + +`snake` is a list of points, head first. `y` grows downward. The fixture board is 5 x 5, so coordinates run from 0 to 4. + +
+ +## Check your understanding + +1. In the big-picture diagram, which box is allowed to change things in the world? Why is it useful that this box is small? +2. Your friend says "an `IO` value draws a rectangle". Rephrase that sentence so that it is accurate for this course. +3. Why do the three tracks share their test cases as JSON files instead of each track writing its own? +4. A test fails with `TODO: implement in m04`. What does that tell you, and what does it not tell you? +5. Why does the purity audit ban `.sort(` in JavaScript but not `.map(`? +6. The purity audit can be fooled. Write down one line of JS that mutates something but would pass the scan. What does that tell you about relying on tools versus understanding the rule? +7. When would taking a checkpoint from `solutions/` be the right decision, and what should you do right after taking one? + +## In production + +The architecture of this course is close to how several real systems are built: + +- **The Elm Architecture** ([guide.elm-lang.org/architecture](https://guide.elm-lang.org/architecture/)) structures every Elm web app as `model`, `update : Msg -> Model -> Model` and `view : Model -> Html`. The Elm runtime is the only thing that performs effects. F# programmers use the same pattern through [Elmish](https://elmish.github.io/elmish/). +- **Redux** ([redux.js.org](https://redux.js.org/)) brought the same idea to JavaScript: state changes only through pure *reducers* `(state, action) => state`. +- **cats-effect** ([typelevel.org/cats-effect](https://typelevel.org/cats-effect/)) and **ZIO** ([zio.dev](https://zio.dev/)) in Scala, with **fs2** ([fs2.io](https://fs2.io/)) for streams, make whole server applications out of `IO` values and run them with one call at the edge. Your hand-built `IO` and `Stream` (Modules 7 and 8) are small cousins of these. + +## Further reading + +- [Functional Core, Imperative Shell](https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell) — Gary Bernhardt's short screencast on splitting a program into a pure core and a thin effectful shell; this course takes the idea further. +- [Why Functional Programming Matters](https://www.cs.kent.ac.uk/people/staff/dat/miranda/whyfp90.pdf) — John Hughes' classic paper on why higher-order functions and laziness make programs modular. Short and still current. +- [Node.js test runner](https://nodejs.org/api/test.html) — documentation for `node --test`, used by the JS track. +- [munit](https://scalameta.org/munit/) — the Scala test library used by the Scala track. +- [Expecto](https://github.com/haf/expecto) — the F# test library used by the F# track. +- [Scala.js](https://www.scala-js.org/) and [Fable](https://fable.io/) — the compilers that turn the Scala and F# tracks into JavaScript for the browser. diff --git a/course/01-purity-and-immutability.md b/course/01-purity-and-immutability.md new file mode 100644 index 0000000..179b3e6 --- /dev/null +++ b/course/01-purity-and-immutability.md @@ -0,0 +1,566 @@ +# Module 1 — Purity and immutability + +> **Time:** ~3 h · **You'll edit:** JS `js/src/util.js` · Scala `scala/src/Util.scala` · F# `fsharp/src/Util.fs` +> **Tests:** JS `cd js && node --test "test/m01-*.test.js"` · Scala `cd scala && ./test.sh M01` · F# `cd fsharp && ./test.sh M01` +> **Prerequisites:** Module 0 + +## Why this module + +Look at this piece of a typical Snake game: + +```js +function step() { + const head = nextHead() + snake.body.unshift(head) // changes the array everyone shares + if (samePlace(head, apple)) { + score++ // changes a global + apple = randomFreeCell() // a different answer every call + } else { + snake.body.pop() // changes it again + } +} +``` + +`step()` takes no arguments and returns nothing. Everything it does is a change somewhere else. To know what it does you need to know the current value of `snake`, `apple`, `score`, what `randomFreeCell()` will return this time, and who else is holding a reference to `snake.body`. To test it you have to set up globals, call it, and inspect the globals afterwards. To replay a game you would have to record every random number. + +The fix is not a library. It is two habits you already practised in Scala: **write functions whose result depends only on their arguments**, and **never change data; build new data instead**. This module makes those habits precise, shows how they look in JavaScript (which does not enforce them) and in F#, and gives you a few small tools you will use in every later module. + +## Learning objectives + +After this module you can: + +- **explain** referential transparency and decide whether a given expression has it; +- **distinguish** a constant binding (`const`, `val`, `let`) from immutable data, and shallow from deep freezing; +- **update** records and arrays without mutation in all three languages (spread, `copy`, `with`); +- **implement** small list helpers (`range`, `dropLast`, `contains`) without loops or reassignment; +- **compose** functions (JS `pipe`/`compose`, Scala `andThen`/`compose`, F# `|>`/`>>`) and predict the order in which they apply. + +## Concepts + +### Referential transparency + +An expression is **referentially transparent** if you can replace it with its value anywhere in the program without changing what the program does. + +```js +const add = (a, b) => a + b +const x = add(2, 3) + add(2, 3) +// You may rewrite this as 5 + 5, or as const five = add(2, 3); five + five. +// The program means exactly the same thing. +``` + +Now compare: + +```js +let count = 0 +const next = () => { count = count + 1; return count } +const y = next() + next() // 1 + 2 = 3 +// Rewrite it as const n = next(); n + n -> 1 + 1 = 2. A different program! +``` + +`next()` is not referentially transparent: the same expression gives a different value each time, because it reads and changes hidden state. A function whose calls are always referentially transparent is called **pure**. In practice that means two things: + +1. **Same arguments, same result.** The result depends on nothing except the arguments (no globals, no clock, no randomness, no files). +2. **No observable effects.** Calling it changes nothing the rest of the program can see (no mutation of its arguments or globals, no printing, no drawing). + +Why care? Because a pure function can be understood by looking at it alone. You can test it by calling it with inputs and checking the output. You can call it twice, or zero times, or cache the result, and nothing breaks. This is the *substitution model* of evaluation you may know from your Scala course: you evaluate a program by replacing names with their definitions, like algebra. It only works when everything is referentially transparent. + +> **From Scala:** in Scala you mostly worked with `val` and immutable `List`, so most of your functions were pure without you having to think about it. The compiler does not check purity in Scala, JavaScript or F#. In all three it is a discipline. In this course the tests and the purity audit help you keep it. + +### Exercise in prose: pure or not? + +Before reading the answers, decide for each function: pure or impure? If impure, which of the two rules does it break? + +```js +const a = (xs) => xs.length // (a) +const b = (xs) => { xs.push(0); return xs } // (b) +const c = () => Date.now() // (c) +const d = (p) => ({ ...p, x: p.x + 1 }) // (d) +const e = (msg) => { console.log(msg); return msg } // (e) +const f = (xs) => xs.toSorted() // (f) +const g = (xs) => xs.sort() // (g) +const h = (n) => { throw new Error('no') } // (h) +``` + +
Answers + +- **(a) pure.** Result depends only on `xs`, nothing changes. +- **(b) impure.** It mutates its argument. The caller's array has grown, and anyone else holding it sees the change. (Same input array, second call: a different result.) +- **(c) impure.** No arguments, yet a different result every millisecond: a hidden input (the clock). +- **(d) pure.** It builds a new object; `p` is untouched. +- **(e) impure.** It returns its argument unchanged, but printing is an effect the outside world can observe. Replacing `e('hi')` with `'hi'` changes what the program does. +- **(f) pure.** `toSorted` returns a new sorted array (ES2023). +- **(g) impure.** `sort` sorts *in place* and returns the same array. This is a classic bug source in JavaScript. +- **(h) debatable.** It is deterministic and changes nothing, but it does not return a value. Many people count exceptions as an effect. In this course we avoid throwing in game logic: our rule functions are *total* (they return a value for every input), which you will see in Module 3. + +
+ +### `const` is not immutability + +In JavaScript, `const` means *this name will always refer to the same value*. It says nothing about whether the value can change: + +```js +const xs = [1, 2] +xs.push(3) // allowed! xs is still the same array, which now has 3 elements +xs = [9] // TypeError: Assignment to constant variable. +``` + +So there are two different things: + +| | Can the name point somewhere else? | Can the value change? | +|---|---|---| +| JS `let` | yes | yes (if it is an object/array) | +| JS `const` | no | yes (if it is an object/array) | +| JS `const` + frozen value | no | no | +| Scala `val` + `List`, case class | no | no | +| F# `let` + list, record | no | no | + +> **From Scala:** Scala separates these too. `val buf = ListBuffer(1)` cannot be reassigned, but `buf += 2` still mutates it. Immutability comes from the *data type* (`List`, case classes), not from `val`. F# is like Scala here, only stricter: `let` bindings cannot be reassigned at all unless declared `let mutable`, and lists and records are immutable. + +### Freezing in JavaScript + +`Object.freeze(obj)` makes an object (or array) read-only: assigning to a property or pushing an element throws in strict mode, which ES modules always are. + +```js +const p = Object.freeze({ x: 1, y: 2 }) +p.x = 5 // TypeError in a module: Cannot assign to read only property 'x' +``` + +But `freeze` is **shallow**. It freezes the object's own properties, not the objects they point to: + +```js +const game = Object.freeze({ snake: [{ x: 1, y: 1 }] }) +game.snake = [] // TypeError: good +game.snake.push({x: 2}) // works! The array inside is not frozen. +``` + +The tests in this course **deep-freeze** every input they hand to your functions. If your code accidentally mutates its input, it does not silently corrupt the test; it throws a `TypeError` at the exact line that tried. When you see `Cannot add property 3, object is not extensible` or `Cannot assign to read only property`, you have found a mutation. You will write `deepFreeze` yourself in this module. + +### Persistent data and structural sharing + +If we never change data, do we copy everything all the time? No. Since nobody can change the old value, the new value may **share** the parts that did not change. + +``` + before: snake ----> [ H0 , B1 , B2 ] (an array of three point objects) + after: snake' ---> [ H1 , H0 , B1 ] (a new array...) + | | + +----+--- ...pointing at the very same point objects +``` + +In JavaScript, `[newHead, ...rest]` creates a new array (copying the references, which is cheap for small arrays) but shares every point object. In Scala and F#, `newHead :: rest` is even cheaper: the new list is one new cell pointing to the old list, which is shared entirely. Data structures designed for this are called **persistent**: old versions stay valid after you "update" them. Sharing is only safe because nobody mutates; that is why immutability and persistence go together. + +> **From Scala:** `List`, `Vector` and `Map` are all persistent. `xs :+ x` on a `Vector` returns a new vector that shares almost all of its structure with `xs`. F#'s `list` and `Map` behave the same way. + +### Making a changed copy + +The core move of this whole course: *take a record, and produce a new record that differs in a few fields.* Each language has a built-in syntax for it. Here with a small example record (not from the game): + +**JavaScript** +```js +const ada = { name: 'Ada', hp: 10, level: 3 } +const hurt = { ...ada, hp: ada.hp - 4 } // { name: 'Ada', hp: 6, level: 3 }; ada is unchanged +``` + +**Scala** +```scala +final case class Hero(name: String, hp: Int, level: Int) +val ada = Hero("Ada", 10, 3) +val hurt = ada.copy(hp = ada.hp - 4) // Hero(Ada,6,3); ada is unchanged +``` + +**F#** +```fsharp +type Hero = { Name: string; Hp: int; Level: int } +let ada = { Name = "Ada"; Hp = 10; Level = 3 } +let hurt = { ada with Hp = ada.Hp - 4 } // { Name = "Ada"; Hp = 6; Level = 3 } +``` + +Note the naming convention: F# record fields are PascalCase (`Hp`, and later `NextDir`), where JS and Scala use camelCase (`hp`, `nextDir`). + +Watch out in JS: spread is shallow, like `freeze`. `{ ...game, snake: game.snake }` shares the same snake array. That is fine *as long as nobody mutates it*, which is exactly our rule. + +For arrays in JavaScript, learn which methods build a new array and which change the old one: + +| Builds a new value (use these) | Mutates in place (banned by the purity audit) | +|---|---| +| `map`, `filter`, `reduce`, `slice`, `concat`, `[...xs, x]`, `[x, ...xs]` | `push`, `pop`, `shift`, `unshift`, `splice` | +| `toSorted`, `toReversed`, `with(i, v)` | `sort`, `reverse`, `xs[i] = v` | +| `some`, `every`, `find`, `includes`, `Array.from` | | + +### Loops without reassignment + +A `for` loop needs a counter that changes, so it needs `let`. Without `let` you do what you did in Scala: express the loop as a transformation of a whole collection (`map`, `filter`, `some`), as a fold (`reduce`), or as recursion. + +```js +// sum of squares of the even numbers, no loop and no let +const sumEvenSquares = (xs) => xs.filter((x) => x % 2 === 0).map((x) => x * x).reduce((a, b) => a + b, 0) +``` + +> **From Scala:** `reduce(f, init)` in JavaScript is `foldLeft(init)(f)` in Scala and `List.fold f init xs` in F#; `reduceRight` is `foldRight`. JavaScript's `reduce` *without* an initial value throws on an empty array, like Scala's `reduce`. Always pass the initial value. + +`Array.from({ length: n }, (_, i) => ...)` builds an array of length `n` by calling a function with each index. `Array.from` is worth looking up now: it is the JavaScript cousin of Scala's `List.tabulate`. + +### Functions are values + +A function is a value like any other. You can store it in a `const`, pass it as an argument, and return it from another function: + +**JavaScript** +```js +const twice = (f) => (x) => f(f(x)) // takes a function, returns a function +const addThree = (n) => n + 3 +twice(addThree)(10) // 16 +``` + +**Scala** +```scala +def twice(f: Int => Int): Int => Int = x => f(f(x)) +val addThree = (n: Int) => n + 3 +twice(addThree)(10) // 16 +``` + +**F#** +```fsharp +let twice f = fun x -> f (f x) +let addThree n = n + 3 +twice addThree 10 // 16 +``` + +In F#, every function of several arguments is *curried* by default: `let add a b = a + b` has type `int -> int -> int`, and `add 3` is a function waiting for the second number. That is why F# library functions put the data last: `List.map f xs` can be partially applied as `List.map f` and used in a pipeline. + +This module's `contains(xs, p, eq)` takes a function as its third argument. Why not compare with `===`? Because in JavaScript `===` on objects compares *identity*: `{x: 1} === {x: 1}` is `false`. Passing `eq` lets the caller decide what "equal" means, for instance "same x and same y". + +### Composition + +Programs in this course are mostly small functions plugged together. Two ways of plugging: + +- **pipe** (left to right): start with a value and push it through functions in reading order. `pipe(x, f, g)` is `g(f(x))`. +- **compose** (right to left, like maths): `compose(g, f)` is the function `x => g(f(x))`, read "g after f". + +```js +const inc = (x) => x + 1 +const double = (x) => x * 2 +// pipe(3, inc, double) -> double(inc(3)) = 8 +// compose(double, inc)(3) -> double(inc(3)) = 8 +// compose(inc, double)(3) -> inc(double(3)) = 7 +``` + +JavaScript has neither built in; you write both in this module (Exercises 1.4 and 1.5). Scala and F# do have them: + +**Scala** +```scala +val inc = (x: Int) => x + 1 +val double = (x: Int) => x * 2 +(inc andThen double)(3) // 8: first inc, then double (like pipe) +(double compose inc)(3) // 8: double after inc (like compose) +3.pipe(inc).pipe(double) // 8, needs: import scala.util.chaining.* +``` + +**F#** +```fsharp +let inc x = x + 1 +let double x = x * 2 +3 |> inc |> double // 8: the pipe operator, value first +(inc >> double) 3 // 8: forward composition, like andThen +(double << inc) 3 // 8: backward composition, like compose +``` + +> **From Scala:** `f andThen g` is F#'s `f >> g` and the JS `pipe` order; `g compose f` is F#'s `g << f` and the JS `compose` order. The F# pipe `x |> f` is `f x` written backwards, which makes long chains read top to bottom, like a chain of Scala method calls. + +This is why the Scala and F# tracks have a smaller Module 1: `pipe`, `compose` and immutable data are built in. Their tests cover `range`, `dropLast` and `contains`; the composition exercise is a short practice session in the REPL instead. + +## Exercises + +All functions here must be pure: no mutation (inputs are deep-frozen in the JS tests, so mutation throws), no reassignment. + +### Exercise 1.1 — `range` + +`range(n)` returns the numbers `0, 1, ..., n-1` in order; `range(0)` is empty. You will use it in Module 4 to list every cell of the board. + +**JavaScript** +```js +export const range = (n) => ... // range(3) -> [0, 1, 2] +``` +**Scala** +```scala +def range(n: Int): List[Int] // range(3) == List(0, 1, 2) +``` +**F#** +```fsharp +let range (n: int) : int list // range 3 = [0; 1; 2] +``` + +Tests: `range(0) is empty`, `range(3) counts from zero up to n-1`. + +Watch out: no `for (let i ...)` in JS. Think "build an array of length `n` from its indices". + +### Exercise 1.2 — `dropLast` + +`dropLast(xs)` returns `xs` without its last element. The empty list stays empty. It must never modify `xs`. You will use it in Module 3: a moving snake loses its last segment. + +**JavaScript** +```js +export const dropLast = (xs) => ... // dropLast([1, 2, 3]) -> [1, 2]; dropLast([]) -> [] +``` +**Scala** +```scala +def dropLast[A](xs: List[A]): List[A] +``` +**F#** +```fsharp +let dropLast (xs: 'a list) : 'a list +``` + +Tests: `dropLast of an empty list is empty`, `dropLast removes only the last element`, `dropLast leaves its input untouched` (the input is frozen in JS). + +Watch out: the empty case. In Scala, `List.empty.init` *throws*. In F#, `List.init` is something else entirely (it builds a list from a length and a function). A function that throws on some inputs is called *partial*; we want *total* functions. + +### Exercise 1.3 — `contains` + +`contains(xs, p, eq)` is true if some element `x` of `xs` satisfies `eq(x, p)`. In Module 3 you will ask "does the snake's body contain this point?", where points are compared by their coordinates. + +**JavaScript** +```js +export const contains = (xs, p, eq) => ... // contains([1, 2, 3], 5, sameParity) -> true +``` +**Scala** +```scala +def contains[A](xs: List[A], p: A, eq: (A, A) => Boolean): Boolean +``` +**F#** +```fsharp +let contains (xs: 'a list) (p: 'a) (eq: 'a -> 'a -> bool) : bool +``` + +Tests: `contains finds an element using the supplied equality` (with `sameParity`, 5 "equals" 1 and 3), `contains is false when no element is equal`, `contains is false for an empty list`, `contains compares structurally when given a structural equality`. + +Watch out: the argument order of `eq`. The doc comment says `eq(x, p)`: the list element first. + +### Exercise 1.4 — `pipe` (JS only) + +`pipe(x, ...fns)` threads `x` through the functions from left to right. With no functions it returns `x`. + +**JavaScript** +```js +export const pipe = (x, ...fns) => ... // pipe(2, x => x + 1, x => x * 10) -> 30; pipe(5) -> 5 +``` + +Tests: `pipe threads a value through functions left to right`, `pipe with no functions returns the value`. + +**Scala / F# practice (no tests).** Open a REPL (`scala-cli repl` or `dotnet fsi`). Define `inc`, `double` and `square`, and predict, *before evaluating*, the results of: Scala `(inc andThen double andThen square)(1)` and `(inc compose double compose square)(1)`; F# `1 |> inc |> double |> square` and `(inc << double << square) 1`. Check your predictions. + +### Exercise 1.5 — `compose` (JS only) + +`compose(...fns)` returns a function that applies the functions **right to left**. `compose()` with no functions is the identity function. + +**JavaScript** +```js +export const compose = (...fns) => ... // compose(x => x * 10, x => x + 1)(2) -> 30; compose()(5) -> 5 +``` + +Tests: `compose applies functions right to left`, `compose with no functions is the identity`. + +### Exercise 1.6 — `deepFreeze` (JS only) + +`deepFreeze(obj)` freezes `obj` and everything reachable from it, then returns the same object. Two situations make it interesting: + +- **An already-frozen parent with unfrozen children.** You must still go into it. (Test: `deepFreeze reaches unfrozen children of an already-frozen parent`.) +- **Cycles.** `o.child.parent === o`. A naive recursion never ends. (Test: `deepFreeze handles cyclic objects`.) + +**JavaScript** +```js +export const deepFreeze = (obj) => ... // returns obj itself, frozen all the way down +``` + +Other tests: `deepFreeze freezes nested objects and arrays`, `deepFreeze returns the same object it was given`. + +Watch out: `typeof null === 'object'`. And you may not keep a mutable "already seen" set in a `let`. How else could a recursive call know where it has already been? + +One honest remark: `Object.freeze` itself changes the object (it flips it to read-only). That is the one deliberate "mutation" in this module, and it only ever *removes* the ability to change things. + +### Run + +- JS: `cd js && node --test "test/m01-*.test.js"` +- Scala: `cd scala && ./test.sh M01` +- F#: `cd fsharp && ./test.sh M01` + +When green, also run the full suite once (`./test.sh`) to check the purity audit is happy with `util`. + +## Hints + +
Exercise 1.1 (range) — nudge + +You know a function in each language that builds a collection of a given length from the index. In Scala and F# there is even a ready-made range syntax. + +
+ +
Exercise 1.1 (range) — stronger hint + +JS: `Array.from` takes an "array-like" object (anything with a `length`) and a mapping function `(element, index) => ...`. Scala: `0 until n` is a `Range`; it has `.toList`. F#: `[ a .. b ]` is a list literal for a range (what is `b` when you want `n` numbers starting at 0?), and `List.init` builds from a length and a function. + +
+ +
Exercise 1.1 (range) — near-solution (try the others first) + +```js +export const range = (n) => Array.from({ length: ___ }, (___, i) => ___) +``` +```scala +def range(n: Int): List[Int] = (0 until ___).___ +``` +```fsharp +let range (n: int) : int list = List.init ___ (fun i -> ___) +``` + +
+ +
Exercise 1.2 (dropLast) — nudge + +Which non-mutating method gives you "a piece of the array from here to there"? Can its "to" index count from the end? + +
+ +
Exercise 1.2 (dropLast) — stronger hint + +JS: `slice(start, end)` returns a new array and accepts negative indices, counted from the end. Try `[].slice(0, -1)` in a Node REPL. Scala: `dropRight` exists and is total; or pattern-match on the list. F#: pattern matching on the list is the clearest. Which three shapes can a list have that matter here? (empty, exactly one element, more than one) + +
+ +
Exercise 1.2 (dropLast) — near-solution (try the others first) + +```js +export const dropLast = (xs) => xs.slice(___, ___) +``` +```scala +def dropLast[A](xs: List[A]): List[A] = xs.dropRight(___) +``` +```fsharp +let rec dropLast (xs: 'a list) : 'a list = + match xs with + | [] | [ _ ] -> ___ + | x :: rest -> ___ :: ___ +``` + +Note the `rec` in F#: recursive functions must say so. + +
+ +
Exercise 1.3 (contains) — nudge + +The question "is there some element such that ...?" has a name in every collection library. + +
+ +
Exercise 1.3 (contains) — stronger hint + +JS `Array.prototype.some`, Scala `exists`, F# `List.exists`. Each takes a predicate (a function element → boolean). Build that predicate from `eq` and `p`. + +
+ +
Exercise 1.3 (contains) — near-solution (try the others first) + +```js +export const contains = (xs, p, eq) => xs.some((x) => ___) +``` +```scala +def contains[A](xs: List[A], p: A, eq: (A, A) => Boolean): Boolean = xs.exists(x => ___) +``` +```fsharp +let contains (xs: 'a list) (p: 'a) (eq: 'a -> 'a -> bool) : bool = xs |> List.exists (fun x -> ___) +``` + +
+ +
Exercise 1.4 (pipe) — nudge + +You have a starting value and a list of functions. You want to go through the list, carrying a "current value" along. Which list operation carries an accumulator through a list? + +
+ +
Exercise 1.4 (pipe) — stronger hint + +It is a fold: `fns.reduce(step, x)`. The accumulator is the current value; each step applies the next function to it. With no functions, `reduce` returns the initial value, which is exactly the required behaviour. + +
+ +
Exercise 1.4 (pipe) — near-solution (try the others first) + +```js +export const pipe = (x, ...fns) => fns.reduce((acc, f) => ___, ___) +``` + +
+ +
Exercise 1.5 (compose) — nudge + +`compose` returns a function. Inside that function you have the argument and the list of functions. How is that like `pipe`, and in which direction must you walk the list? + +
+ +
Exercise 1.5 (compose) — stronger hint + +`compose(f, g, h)(x)` is `f(g(h(x)))`: apply the *last* function first. `reduceRight` walks an array from the end. (Alternatively: `toReversed()` and reuse your `pipe`.) + +
+ +
Exercise 1.5 (compose) — near-solution (try the others first) + +```js +export const compose = (...fns) => (x) => fns.reduceRight((acc, f) => ___, ___) +``` + +
+ +
Exercise 1.6 (deepFreeze) — nudge + +Write the simple version first: freeze the object, then call `deepFreeze` on each of its values (`Object.values` works for arrays too). Then think about the two stopping conditions: values that are not objects, and objects you are already inside of. + +
+ +
Exercise 1.6 (deepFreeze) — stronger hint + +Do not use `Object.isFrozen` to decide whether to stop: that is exactly what breaks for an already-frozen parent with unfrozen children. To detect a cycle without mutable state, give the function a second parameter with a default value: the list of **ancestors** (the objects on the path from the root down to here). A cycle means the current object is one of its own ancestors. Each recursive call passes a *new* list with the current object added. + +
+ +
Exercise 1.6 (deepFreeze) — near-solution (try the others first) + +```js +export const deepFreeze = (obj, ancestors = []) => { + if (obj === null || typeof obj !== ___ || ancestors.includes(___)) return obj + Object.freeze(obj) + Object.values(obj).forEach((v) => deepFreeze(v, [___, ___])) + return ___ +} +``` + +
+ +## Check your understanding + +1. Is `const now = () => Date.now()` referentially transparent? Is the expression `now` (without calling it)? What is the difference? +2. Predict: `const a = Object.freeze({ xs: [1] }); a.xs.push(2); a.xs.length`. What does it evaluate to, and why? What would happen with `deepFreeze` instead? +3. `{ ...state, score: state.score + 1 }` shares the `snake` array between the old and the new state. Why is that safe here, and what single line elsewhere in the program would make it unsafe? +4. Predict `compose(pipe)(3)` and `pipe(3, compose())`. Explain using the definitions, not by running them. +5. In F#, why is it `List.map f xs` (data last) while in JS our functions take the data first? What does the F# order make easy? +6. Your `dropLast` passes the tests. Would it still be correct if the tests did not freeze the input? How could you be sure it never mutates, other than through tests? +7. The purity audit bans `.sort(` but you may use `.toSorted()`. Explain in one sentence what the difference is, in terms of referential transparency. +8. Why does `contains` take an `eq` parameter in JavaScript, but in Scala you can pass `_ == _` and get structural equality for case classes? + +## In production + +- **Immer** ([immerjs.github.io/immer](https://immerjs.github.io/immer/)) lets JavaScript developers write code that *looks* like mutation inside a callback, and turns it into a new immutable value with structural sharing. Redux Toolkit uses it under the hood. +- **Immutable.js** ([immutable-js.com](https://immutable-js.com/)) provides persistent `List` and `Map` types for JavaScript, like Scala's and F#'s collections. +- **Scala and F# collections** are persistent by default: [Scala's immutable collections](https://docs.scala-lang.org/overviews/collections-2.13/concrete-immutable-collection-classes.html), F#'s `list`, `Map` and `Set`. +- **Clojure** made persistent data structures the default for a whole language; Rich Hickey's talk below explains why. + +## Further reading + +- [Referential transparency (Wikipedia)](https://en.wikipedia.org/wiki/Referential_transparency) — the definition and its history, with examples. +- [Mostly Adequate Guide to Functional Programming](https://mostly-adequate.gitbook.io/mostly-adequate-guide/) — a free book on FP in JavaScript; chapters 3 (pure functions) and 5 (composition) match this module. +- [Why Functional Programming Matters](https://www.cs.kent.ac.uk/people/staff/dat/miranda/whyfp90.pdf) — John Hughes on why gluing small functions together makes programs modular. +- [Object.freeze (MDN)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) — exact semantics, including why it is shallow. +- [Spread syntax (MDN)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) — copying arrays and objects with `...`. +- [Array.prototype.toSorted (MDN)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toSorted) — the non-mutating siblings of `sort`, `reverse` and `splice`. +- [F# functions: composition and pipelining](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/functions/#function-composition-and-pipelining) — official docs for `|>`, `>>` and `<<`. +- [Scala 3 book: functions](https://docs.scala-lang.org/scala3/book/taste-functions.html) — a refresher on function values in Scala. +- [Persistent data structure (Wikipedia)](https://en.wikipedia.org/wiki/Persistent_data_structure) and [Chris Okasaki, *Purely Functional Data Structures* (thesis)](https://www.cs.cmu.edu/~rwh/students/okasaki.pdf) — how immutable structures can be efficient. +- [Rich Hickey, "Are We There Yet?"](https://www.infoq.com/presentations/Are-We-There-Yet-Rich-Hickey/) — a talk on values, identity and time; why mutable objects mix up the three. diff --git a/course/02-the-game-as-data.md b/course/02-the-game-as-data.md new file mode 100644 index 0000000..04ca366 --- /dev/null +++ b/course/02-the-game-as-data.md @@ -0,0 +1,468 @@ +# Module 2 — The game as data + +> **Time:** ~3 h · **You'll edit:** JS `js/src/model.js` · Scala `scala/src/Model.scala` · F# `fsharp/src/Model.fs` +> **Tests:** JS `cd js && node --test "test/m02-*.test.js"` · Scala `cd scala && ./test.sh M02` · F# `cd fsharp && ./test.sh M02` +> **Prerequisites:** Module 1 + +## Why this module + +In the usual object-oriented Snake, the game is spread over objects that each hold a bit of state and change it: a `Snake` with a `body` array and a `direction` field, an `Apple` with a `pos`, a `Game` with `score`, `isPaused`, `isOver`, a timer handle, and a random generator hidden somewhere. To save or replay a game, you would have to find all of those pieces and copy them at the right moment. + +We are going to do the opposite: **the whole game is one value.** One immutable record that contains everything needed to draw the current frame and compute the next one. If you have that value, you have the game. You can print it, compare it with `==`, store it in a test fixture, or keep a list of them as a history. + +Choosing that record is the most important design decision of the course. Every later module reads it, and the shared test fixtures are written in its shape. So before you are shown the course's model, you will design your own. + +## Learning objectives + +After this module you can: + +- **design** a minimal immutable state for a game and justify each field; +- **explain** algebraic data types (product types and sum types) and how each language spells them; +- **apply** "make illegal states unrepresentable" to choose between representations; +- **implement** the model's small functions (`delta`, `opposite`, `initialState`, and in JS `point`, `pointEquals`, `Action`); +- **compare** your design with the canonical one and name the trade-offs. + +## Concepts + +### Design it yourself first + +Take a sheet of paper and fifteen minutes. **Do not read past the "Stop here" line until you have written something down.** It does not matter if your answer differs from the course's; what matters is that you have thought about the questions, so that the canonical model answers questions you have actually asked. + +Your task: write down the type of "the state of a game of Snake", in the language of your track or in pseudo-code. Use these questions to guide you: + +1. **What is the minimum information** you need to (a) draw the current screen and (b) compute the next state when time passes or a key is pressed? Is there anything you were tempted to include that can be *computed* from the rest? (Candidates: the snake's length, the set of occupied cells, pixel coordinates.) +2. **How do you represent the snake's body?** A list of cells? In which order? Moving the snake means "add a new cell at the front, remove the last". Which end of a list is cheap to add to in Scala's `List` and F#'s `list`? +3. **How many directions do you need to store?** Imagine the player presses two keys very quickly, both before the snake moves again. What do you need to remember to handle that well? (Hint: there is "the way the snake is going" and "the way the player has asked it to go".) +4. **Is there always an apple?** Think of the moment right after the snake eats it, and of a board completely filled by the snake. How do you represent "no apple"? +5. **Where does randomness come from?** The apple appears on a random free cell. If the generator's state lives outside the game state, is a function `nextState(state)` still pure? Could you replay a game? +6. **How do you represent "playing", "paused", "game over" and "you won"?** Would two booleans `isPaused` and `isOver` do? How many combinations do two booleans have, and how many of them make sense? +7. **What about the size of the board?** Hard-code it, or store it? Think about writing a test where the snake hits a wall: would you rather draw a 20 x 20 board or a 5 x 5 one? +8. **What happens in the game that is not a change of time?** List all the things that can *happen* (a tick of the clock, a key press, ...). Could those be data too? + +--- + +**Stop here.** Write your design down before reading on. + +--- + +### The canonical model + +Here is the model the course uses. All three tracks use the same fields with the same meaning, which is what lets one JSON fixture test all three. + +**JavaScript** (JS has no types, so the shape is documented with JSDoc in `model.js`) +```js +/** @typedef {{x: number, y: number}} Point */ +/** @typedef {'Up'|'Down'|'Left'|'Right'} Direction */ +/** @typedef {'Playing'|'Paused'|'GameOver'|'Won'} Status */ +/** @typedef {{width: number, height: number, snake: Point[], dir: Direction, nextDir: Direction, + * apple: Point|null, seed: number, score: number, status: Status, frame: number}} GameState */ +// Action: {tag:'Tick'} | {tag:'Turn', dir} | {tag:'TogglePause'} | {tag:'Restart'} | {tag:'Frame'} +``` + +**Scala** (already in `Model.scala`) +```scala +final case class Point(x: Int, y: Int) +enum Direction: + case Up, Down, Left, Right +enum Status: + case Playing, Paused, GameOver, Won +enum Action: + case Tick + case Turn(dir: Direction) + case TogglePause + case Restart + case Frame +final case class GameState( + width: Int, height: Int, + snake: List[Point], // head first + dir: Direction, nextDir: Direction, + apple: Option[Point], + seed: Long, // unsigned 32-bit, 0 until 2^32 + score: Int, status: Status, frame: Int) +``` + +**F#** +```fsharp +type Point = { X: int; Y: int } +type Direction = Up | Down | Left | Right +type Status = Playing | Paused | GameOver | Won +type Action = + | Tick + | Turn of Direction + | TogglePause + | Restart + | Frame +type GameState = + { Width: int; Height: int + Snake: Point list // head first + Dir: Direction; NextDir: Direction + Apple: Point option + Seed: uint32 + Score: int; Status: Status; Frame: int } +``` + +A fresh game is 20 x 20, the snake is `(10,10), (9,10), (8,10)` heading `Right` (both `dir` and `nextDir`), there is no apple yet, the seed is whatever you were given, the score is 0, the status `Playing`, and `frame` is 0. The `y` axis grows **downward**, as on a screen: `Up` is `{x: 0, y: -1}`. + +Now compare with your design, question by question. + +**1. Minimal state.** Everything in `GameState` is needed to compute the future. Nothing is stored that can be derived: the length is `snake.length`, the occupied cells are the snake's points, pixel positions are the view's business (Module 6). Storing derived data creates a second source of truth that can disagree with the first. (A trade-off you might make in a bigger game: cache a set of occupied cells for speed, and accept that you must keep it consistent.) + +**2. Head first.** The snake is a list of points with the **head at index 0**. Every move adds a cell at the head and (usually) removes the tail. For Scala's `List` and F#'s `list`, adding at the front (`h :: rest`) is O(1) and shares the whole old list. Reading the head is O(1) too. In JS it hardly matters for speed, but the same order keeps the tracks identical. + +**3. Two directions.** `dir` is the direction the snake **last moved**. `nextDir` is the direction it **will move** on the next step, the most recent valid key press. A key press only changes `nextDir`; a step of the game moves the head in `nextDir` and then records it as `dir`. Why both? You will discover in Module 3 that if you only keep one, a fast player can make the snake reverse into itself. Keep that question in mind. + +**4. Optional apple.** There is not always an apple: at the very start (before the first one is placed), and when the snake has filled the whole board. JS uses `null` for "no apple"; Scala and F# use `Option[Point]` / `Point option`, so the compiler forces every reader to handle the "none" case. The board-full situation becomes the status `Won` in Module 4. + +**5. The seed is in the state.** The apple's position comes from a pseudo-random generator, and a generator is a little machine with an internal state (a 32-bit number, the *seed*). If that state lived in a global variable, every function that places an apple would read and change a global, so it would not be pure, and two runs could differ. By putting the seed **inside** the game state, "place an apple" becomes a pure function `state → state` that takes the seed out, uses it, and stores the new seed. Same starting seed, same game, every time. Module 4 is all about this. + +**6. Status as one value.** Two booleans have four combinations; "paused and game over" is meaningless, and every function would need to decide what to do with it. A `Status` with four named cases has exactly the four states that make sense. This idea has a name; see "Making illegal states unrepresentable" below. + +**7. Board size in the state.** Storing `width` and `height` costs two fields and pays back enormously in testing: the fixtures use boards of 5 x 5 and 3 x 2, small enough to draw by hand. A function that reads a global `WIDTH` constant could not be tested on a tiny board. + +**8. Events as data.** Everything that can happen to the game is a value of type `Action`: `Tick` (time passes one step), `Turn(dir)` (the player asks to turn), `TogglePause`, `Restart`, and `Frame` (a finer-grained clock you will use in Module 10 for speed-ups). Module 5 shows why this is powerful: a whole game becomes "a start state plus a list of actions". + +There is one more field, `frame`. It counts small clock frames so that the snake can speed up later without changing the clock (Module 10). Until then it stays 0. + +Did your design differ? Common alternatives and their costs: + +| Alternative | What goes wrong | +|---|---| +| A 2D grid of cells (`'empty' \| 'snake' \| 'apple'`) | Does not record the snake's *order* (which cell is the head? where is the tail?). Allows two apples, or snake cells that are not connected. | +| Direction stored as a vector `{dx, dy}` | `{dx: 2, dy: 3}` is representable but means nothing. You also lose the names in pattern matches. | +| `isPaused`, `isOver`, `hasWon` booleans | Eight combinations, four meaningful. | +| Score derived from the length | Works until a feature changes length without scoring (or the reverse). A judgement call; we store it. | +| The random generator as an object in the state | Objects with hidden mutable state are exactly what we are removing. A number is a value. | + +### Algebraic data types + +The model is built from two kinds of types. Together they are called **algebraic data types (ADTs)**. + +A **product type** holds several values *at the same time*: a `Point` has an `x` *and* a `y`. Its number of possible values is the product of its fields' (hence the name). Records, case classes, tuples and plain JS objects are product types. + +A **sum type** is *one of* several alternatives: a `Direction` is `Up` *or* `Down` *or* `Left` *or* `Right`. An `Action` is `Tick` *or* `Turn` (carrying a direction) *or* ... The number of possible values is the sum of the alternatives'. Each alternative is called a *case* (or *variant*, or *constructor*). + +> **From Scala:** you know both. `case class` is a product, `enum` (or a `sealed trait` with case classes) is a sum, and `match` takes a sum apart. F# spells the sum as a *discriminated union* (DU): `type Shape = Circle of float | Square of float`. JavaScript has no sum types, so we encode them by convention, as *tagged unions*. + +Here is the same small sum type, unrelated to the game, in all three languages: a traffic light, and a function that gives the next light. + +**JavaScript** +```js +// A nullary sum type: plain strings are enough. +/** @typedef {'Red'|'Green'|'Yellow'} Light */ +const nextLight = (l) => ({ Red: 'Green', Green: 'Yellow', Yellow: 'Red' })[l] + +// A sum type whose cases carry data: objects with a `tag` field. +/** @typedef {{tag: 'Circle', r: number} | {tag: 'Square', side: number}} Shape */ +const area = (s) => { + switch (s.tag) { + case 'Circle': return Math.PI * s.r * s.r + case 'Square': return s.side * s.side + } +} +``` + +**Scala** +```scala +enum Light: + case Red, Green, Yellow +def nextLight(l: Light): Light = l match + case Light.Red => Light.Green + case Light.Green => Light.Yellow + case Light.Yellow => Light.Red + +enum Shape: + case Circle(r: Double) + case Square(side: Double) +def area(s: Shape): Double = s match + case Shape.Circle(r) => math.Pi * r * r + case Shape.Square(side) => side * side +``` + +**F#** +```fsharp +type Light = Red | Green | Yellow +let nextLight l = + match l with + | Red -> Green + | Green -> Yellow + | Yellow -> Red + +type Shape = + | Circle of r: float + | Square of side: float +let area s = + match s with + | Circle r -> System.Math.PI * r * r + | Square side -> side * side +``` + +Note the JS lookup-table trick in `nextLight`: an object literal used as a function from a fixed set of strings to values. It is often the clearest way to write a function over a nullary sum type. + +The important difference between the languages is **exhaustiveness checking**. If you add `case Triangle` to Scala's `Shape` and forget to handle it in `area`, the compiler warns you. F# does the same. JavaScript will silently return `undefined`. In JS you must be your own exhaustiveness checker; the fixtures, which cover every case, help. + +### Making illegal states unrepresentable + +Yaron Minsky's advice from *Effective ML* is: **make illegal states unrepresentable.** Choose types so that a value that makes no sense *cannot be built at all*. Then no function ever has to check for it, and no bug can produce it. + +You have already seen it at work: `Status` instead of booleans, `Direction` instead of a vector, `Option[Point]` instead of "a point plus a flag `hasApple`". Each choice removes nonsense values from the type. + +Be honest about what our model still allows. A `GameState` can still be built with: + +- an empty snake, or a snake whose segments are not next to each other; +- an apple on top of the snake; +- `dir` and `nextDir` pointing in opposite directions; +- in Scala, a seed that is negative or ≥ 2^32 (a `Long` is wider than 32 bits). + +Types could rule out some of these (a `NonEmptyList` for the snake, an opaque `Seed` type), at the price of more ceremony. This course keeps the model simple and maintains the rest as **invariants**: properties that every function promises to preserve. `initialState` creates a valid state, and every rule in Module 3 turns a valid state into a valid state. Choosing how much to push into types is a real engineering trade-off, and you should be able to argue both sides. + +### Equality: identity or structure? + +When are two points equal? In Scala, case classes compare **structurally**: `Point(1, 2) == Point(1, 2)` is `true`. F# records do the same. In JavaScript, `===` on objects compares **identity**: + +```js +const a = { x: 1, y: 2 } +a === { x: 1, y: 2 } // false: two different objects +a === a // true +``` + +That is why the JS track needs a `pointEquals(a, b)` function, and why Module 1's `contains` takes an `eq` argument. Scala and F# need neither (the Scala test for `pointEquals` checks `==` on the same four fixture cases). + +### Smart constructors + +A **smart constructor** is a function that builds a value, possibly checking or normalising it, instead of building it directly. `point(x, y)` in JS is the simplest possible one: it builds `{x, y}`, so every point in the program has exactly that shape (no extra fields, no typos like `{X: 1}`). `initialState(seed)` is another: it is the only place that knows what a fresh game looks like. In a stricter design, a smart constructor would also *reject* invalid input, for example `makeSeed(n)` returning `None` for negative numbers. + +### Actions in JavaScript + +In Scala and F#, `Action` is a type with five cases, already declared for you. In JavaScript you build the values yourself, in `model.js`: + +```js +export const Action = { + Tick: ..., // the value { tag: 'Tick' } + Turn: (dir) => ..., // a function: Action.Turn('Left') is { tag: 'Turn', dir: 'Left' } + TogglePause: ..., + Restart: ..., + Frame: ..., +} +``` + +Cases with no data are constants; a case with data (`Turn`) is a function that builds a tagged object. (The stub uses getters only so that the file loads before you have written it; replace them with plain values.) + +## Exercises + +### Exercise 2.0 — Your own design (paper) + +Complete the design questions at the top of "Concepts" before reading the canonical model. Afterwards, write three sentences: one thing your design did better or differently, one thing the canonical model does that you had not thought of, and one trade-off you would make differently in a bigger game. Keep the sheet; Module 11 asks you to do this again for Pong. + +### Exercise 2.1 — `point` and `pointEquals` (JS) + +**JavaScript** +```js +export const point = (x, y) => ... // point(3, 4) -> { x: 3, y: 4 } +export const pointEquals = (a, b) => ... // true iff same x and same y +``` +**Scala** — nothing to write: `Point(3, 4)` and `==` already do this. Check in the REPL that `Point(1, 2) == Point(2, 1)` is `false`. +**F#** — nothing to write: `{ X = 3; Y = 4 }` and `=` already do this. + +Fixture: `fixtures/m02-point-equals.json` (cases `same-point`, `different-x`, `different-y`, `swapped-coordinates`). Test: `point builds a {x, y} record`. + +### Exercise 2.2 — `delta` + +The unit step for each direction: Up `(0,-1)`, Down `(0,1)`, Left `(-1,0)`, Right `(1,0)`. + +**JavaScript** +```js +export const delta = (dir) => ... // delta('Up') -> { x: 0, y: -1 } +``` +**Scala** +```scala +def delta(dir: Direction): Point +``` +**F#** +```fsharp +let delta (dir: Direction) : Point +``` + +Fixture: `fixtures/m02-delta.json` (cases `up`, `down`, `left`, `right`). Watch out: `y` grows **downward**, so `Up` is negative. + +### Exercise 2.3 — `opposite` + +**JavaScript** +```js +export const opposite = (dir) => ... // opposite('Left') -> 'Right' +``` +**Scala** +```scala +def opposite(dir: Direction): Direction +``` +**F#** +```fsharp +let opposite (dir: Direction) : Direction +``` + +Fixture: `fixtures/m02-opposite.json`. Test: `opposite is its own inverse` (for every `d`, `opposite(opposite(d)) == d`). You will use `opposite` in Module 3 to reject 180-degree turns. + +### Exercise 2.4 — `Action` (JS) + +Fill in the `Action` object as described in "Actions in JavaScript" above. Scala and F#: nothing to write; read the `Action` declaration and make sure you can build `Turn(Left)` and match on it. + +Tests: `Action constants are tagged records`, `Action.Turn carries the direction`. + +### Exercise 2.5 — `initialState` + +**JavaScript** +```js +export const initialState = (seed) => ... +``` +**Scala** +```scala +def initialState(seed: Long): GameState +``` +**F#** +```fsharp +let initialState (seed: uint32) : GameState +``` + +A 20 x 20 board; snake `(10,10), (9,10), (8,10)`; `dir` and `nextDir` both `Right`; no apple; the given seed, stored as given; score 0; status `Playing`; frame 0. + +Fixture: `fixtures/m02-initial-state.json` (cases `seed-7` and `seed-is-stored-as-given`, which uses the largest 32-bit seed, 4294967295). Watch out: the order of the snake's points matters (head first), and in JS use your `point` function so that every point has the same shape. + +### Run + +- JS: `cd js && node --test "test/m02-*.test.js"` +- Scala: `cd scala && ./test.sh M02` +- F#: `cd fsharp && ./test.sh M02` + +## Hints + +
Exercise 2.1 (pointEquals) — nudge + +Two points are equal when both coordinates are equal. Numbers compare by value with `===`, so compare the fields, not the objects. + +
+ +
Exercise 2.1 (pointEquals) — stronger hint + +`pointEquals` is a boolean expression with one `&&`. `point` is an arrow function returning an object literal; remember that `(x, y) => { x, y }` is parsed as a function *body*, not an object. Wrap the object in parentheses. + +
+ +
Exercise 2.1 (pointEquals) — near-solution (try the others first) + +```js +export const point = (x, y) => ({ ___ }) +export const pointEquals = (a, b) => a.x === ___ && ___ +``` + +
+ +
Exercises 2.2 and 2.3 (delta, opposite) — nudge + +Both are functions from a four-case sum type to something. Look at the traffic-light example again. + +
+ +
Exercises 2.2 and 2.3 (delta, opposite) — stronger hint + +JS: an object literal indexed by the direction string (`{ Up: ..., Down: ..., ... }[dir]`), or a `switch`. Scala and F#: a `match` with one line per case. In Scala the cases are `Direction.Up` etc. (or `import Direction.*` to write `Up`). You can build `delta`'s result with `point(...)` (JS), `Point(...)` (Scala) or a record expression `{ X = ...; Y = ... }` (F#). + +
+ +
Exercises 2.2 and 2.3 (delta, opposite) — near-solution (try the others first) + +```js +export const opposite = (dir) => ({ Up: ___, Down: ___, Left: ___, Right: ___ })[dir] +``` +```scala +def opposite(dir: Direction): Direction = dir match + case Direction.Up => ___ + case Direction.Down => ___ + case Direction.Left => ___ + case Direction.Right => ___ +``` +```fsharp +let opposite (dir: Direction) : Direction = + match dir with + | Up -> ___ + | Down -> ___ + | Left -> ___ + | Right -> ___ +``` +`delta` has the same shape, returning points. + +
+ +
Exercise 2.4 (Action) — nudge + +Each constant is an object with a single `tag` field. `Turn` is the only one with data. + +
+ +
Exercise 2.4 (Action) — stronger hint + +Replace `get Tick() { return todo('m02') }` by `Tick: { tag: 'Tick' }`, and so on. `Turn` is an arrow function returning `{ tag: 'Turn', dir }` (wrapped in parentheses). + +
+ +
Exercise 2.5 (initialState) — nudge + +This is a single record literal. Read the fixture `m02-initial-state.json`: the `expected` value is exactly the record you must build (with the seed from `args`). + +
+ +
Exercise 2.5 (initialState) — stronger hint + +Ten fields: `width`, `height`, `snake`, `dir`, `nextDir`, `apple`, `seed`, `score`, `status`, `frame`. In Scala/F#, "no apple" is `None`; in JS it is `null`. In Scala the enum values are `Direction.Right` and `Status.Playing`. + +
+ +
Exercise 2.5 (initialState) — near-solution (try the others first) + +```js +export const initialState = (seed) => ({ + width: 20, height: 20, + snake: [point(10, 10), ___, ___], + dir: ___, nextDir: ___, + apple: ___, seed, score: ___, status: ___, frame: ___, +}) +``` +```scala +def initialState(seed: Long): GameState = + GameState(20, 20, List(Point(10, 10), ___, ___), Direction.Right, ___, ___, seed, ___, ___, ___) +``` +```fsharp +let initialState (seed: uint32) : GameState = + { Width = 20; Height = 20 + Snake = [ { X = 10; Y = 10 }; ___; ___ ] + Dir = Right; NextDir = ___ + Apple = ___; Seed = seed; Score = ___; Status = ___; Frame = ___ } +``` + +
+ +## Check your understanding + +1. Why is the snake stored head first? Name one operation that would be more expensive (in Scala or F#) if it were tail first. +2. Explain in your own words why `seed` is part of `GameState`. What would you lose if it were a global variable that the apple-placing function updates? +3. How many values does the type `{ isPaused: boolean, isOver: boolean }` have? How many does `Status` have? Which illegal combinations disappear? +4. Name two illegal states that our `GameState` can still represent. For one of them, sketch a type that would rule it out, and say what it would cost. +5. In JS, what does `[point(1, 2)].includes(point(1, 2))` return, and why? How does Scala's `List(Point(1, 2)).contains(Point(1, 2))` differ? +6. Predict: if you add a sixth case `Grow` to `Action` in Scala and forget to handle it in a `match`, what happens at compile time? What happens in JS if you forget a `case` in a `switch`? +7. Why store `width` and `height` in the state rather than as global constants? Give a concrete testing scenario. +8. `dir` and `nextDir`: describe a sequence of key presses in which they differ. (Module 3 will ask what can go wrong if we only had one.) + +## In production + +- **Elm** models every application as a single immutable `Model` record plus a `Msg` custom type for everything that can happen, exactly like `GameState` and `Action`. See [custom types in the Elm guide](https://guide.elm-lang.org/types/custom_types.html). +- **TypeScript** checks JavaScript tagged unions at compile time: a `switch` on `tag` can be made exhaustive. See [discriminated unions](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#discriminated-unions). (We use plain JS in this course, so you are the type checker.) +- **Redux** actions are tagged objects (`{ type: 'todos/added', payload }`), the same idea as our `Action`. +- Domain modelling with types is a whole discipline in the F# and Scala communities; see Scott Wlaschin's work below. + +## Further reading + +- [Yaron Minsky, "Effective ML Revisited"](https://blog.janestreet.com/effective-ml-revisited/) — the origin of "make illegal states unrepresentable", with a worked example of redesigning a type. +- [Scott Wlaschin, "Designing with types: Making illegal states unrepresentable"](https://fsharpforfunandprofit.com/posts/designing-with-types-making-illegal-states-unrepresentable/) — the same idea, step by step, in F#. +- [Alexis King, "Parse, don't validate"](https://lexi-lambda.github.io/blog/2019/11/05/parse-don-t-validate/) — why smart constructors that *return a more precise type* beat checks scattered through the code. +- [Algebraic data type (Wikipedia)](https://en.wikipedia.org/wiki/Algebraic_data_type) and [Tagged union (Wikipedia)](https://en.wikipedia.org/wiki/Tagged_union) — definitions, and how many languages encode them. +- [Scala 3 reference: algebraic data types with enums](https://docs.scala-lang.org/scala3/reference/enums/adts.html) — the `enum` syntax used in `Model.scala`. +- [F# discriminated unions](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/discriminated-unions) and [F# records](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/records) — official docs for the F# side. +- [Tony Hoare, "Null References: The Billion Dollar Mistake"](https://www.infoq.com/presentations/Null-References-The-Billion-Dollar-Mistake-Tony-Hoare/) — why `Option` is better than `null`, from the man who invented `null`. diff --git a/course/03-pure-game-rules.md b/course/03-pure-game-rules.md new file mode 100644 index 0000000..4fdf36c --- /dev/null +++ b/course/03-pure-game-rules.md @@ -0,0 +1,446 @@ +# Module 3 — Pure game rules + +> **Time:** ~3–4 h · **You'll edit:** JS `js/src/rules.js` · Scala `scala/src/Rules.scala` · F# `fsharp/src/Rules.fs` +> **Tests:** JS `cd js && node --test "test/m03-*.test.js"` · Scala `cd scala && ./test.sh M03` · F# `cd fsharp && ./test.sh M03` +> **Prerequisites:** Modules 1 and 2 + +## Why this module + +In a stateful Snake, the rules live in methods like `snake.move()`, `snake.changeDirection(d)` and `game.checkCollision()`. They are called in some order from a game loop, and each one reads and writes shared fields. The *order* of those calls is part of the rules, but it is written nowhere. Is the collision checked before or after the tail moves? Before or after the snake grows? You find out by reading the loop, or by watching bugs. + +In this module, every rule is a **pure function from a state to a new state**. `turn(state, dir)` returns the state after the player asks to turn. `advance(state)` returns the state one step later. Each function says, in one place, everything that happens in that step and in which order. You test it by handing it a state and comparing the result with the expected state; no timers, no setup, no mocks. + +Two of the rules hide real subtleties that many Snake implementations get wrong. You will meet them as puzzles, and reason them through before you read the explanations. + +## Learning objectives + +After this module you can: + +- **implement** game rules as total functions `State → State` using copy-with-changes; +- **explain** why a rule returns the state unchanged instead of throwing when it does not apply; +- **reason** about the order of events inside one step (move, grow, collide) and predict the result for a given board; +- **explain** why `turn` must compare against `dir` (last moved) and not `nextDir`; +- **read** a fixture case, draw it, and use it to debug. + +## Concepts + +### Rules as functions `State → State` + +Each rule has the same shape: it takes the whole game state (plus maybe some input) and returns a whole new game state. + +``` +turn : (GameState, Direction) -> GameState +advance : GameState -> GameState +``` + +Because the result is a full state, rules can be chained: `advance(turn(s, 'Up'))` is the state after turning up and moving. Because they are pure, a rule called with a frozen state from a fixture gives exactly the fixture's expected state, every time. + +Inside a rule you use the "copy with changes" move from Module 1: `{ ...state, nextDir: dir }` in JS, `state.copy(nextDir = dir)` in Scala, `{ state with NextDir = dir }` in F#. You never assign to a field. + +### Total functions: "unchanged" is a valid answer + +A function is **total** if it returns a result for every possible input. It never throws, never loops forever, never returns `undefined` by accident. + +What should `turn` do when the game is paused? Throw "cannot turn while paused"? Then every caller must catch, and a key press during pause would crash the game. Instead, our rules follow one simple convention: **when a rule does not apply, it returns the state unchanged.** `turn` while paused returns the same state. `advance` when the game is over returns the same state. This is what the fixtures `turn-ignored-when-paused`, `advance-game-over-unchanged`, `advance-won-unchanged` check. + +> **From Scala:** this is the same reason you prefer `headOption` to `head`, and `Option` to exceptions: totality moves "what if it does not apply?" from a runtime surprise to an ordinary value. In a game, "nothing happens" is the right value surprisingly often. + +A typical rule therefore starts with a **guard**: + +**JavaScript** +```js +// (illustration on a different record) +const heal = (hero, amount) => { + if (hero.hp <= 0) return hero // dead heroes cannot be healed: unchanged + return { ...hero, hp: Math.min(hero.maxHp, hero.hp + amount) } +} +``` + +**Scala** +```scala +def heal(hero: Hero, amount: Int): Hero = + if hero.hp <= 0 then hero + else hero.copy(hp = math.min(hero.maxHp, hero.hp + amount)) +``` + +**F#** +```fsharp +let heal (hero: Hero) (amount: int) : Hero = + if hero.Hp <= 0 then hero + else { hero with Hp = min hero.MaxHp (hero.Hp + amount) } +``` + +### Compute, decide, build + +Longer rules read best in three phases: + +1. **Compute** the facts you need, each as a named `const` / `val` / `let`: where the head goes, whether it is eating, what the body looks like afterwards. +2. **Decide** which outcome applies (a collision? a normal move?). +3. **Build** the result with one copy-with-changes per outcome. + +Named intermediate values cost nothing (they are not variables; they never change) and make the rule read like its specification. Compare the doc comment of `advance` in your stub with the code you write: they should look alike. + +### The four rules + +- **`move(p, dir)`**: the cell one step from `p` in direction `dir`. It is `p + delta(dir)`. It does **not** wrap around the edges: moving `Right` from `x = 4` on a 5-wide board gives `x = 5`, off the board. Deciding what that means is another rule's job (fixture `move-does-not-wrap-at-the-edge`). +- **`inBounds(state, p)`**: is `p` on the board? `0 <= x < width` and `0 <= y < height`. Note the strict `<`: on a 5-wide board, `x = 5` is outside (fixtures `x-equals-width`, `y-equals-height`). +- **`turn(state, dir)`**: the player asks to go in `dir`. Only while `Playing`, and never straight back the way the snake came. If allowed, it sets `nextDir`; it does not move anything. +- **`advance(state)`**: one step of time. The head moves one cell in `nextDir` (which then becomes `dir`). Then, depending on where it lands: it eats, or it moves, or it crashes. + +`move` and `inBounds` are warm-ups. The interesting parts are in `turn` and `advance`. Now the puzzles. + +### Puzzle 1: can the snake bite its own tail? + +Here is a 5 x 5 board. The snake has four segments, in a tight square. Head `H` at (1,1), then (1,2), (2,2), and the tail `T` at (2,1). It is about to move `Right`, onto the tail's cell. There is no apple. + +``` + x=0 x=1 x=2 x=3 x=4 + y=0 . . . . . + y=1 . H T . . snake (head first): (1,1) (1,2) (2,2) (2,1) + y=2 . b b . . nextDir: Right + y=3 . . . . . + y=4 . . . . . +``` + +**Before reading on, decide:** is this move a collision (game over), or legal? Draw what the board looks like after the move in each case. Then consider a second board: identical, except that there **is an apple** on (2,1), the cell where the tail is. Same question. + +
Think about it first, then open the explanation + +All segments of the snake move **at the same time**. In one step, the head enters a new cell and, if the snake is not eating, the tail leaves its cell. So in the first board, by the time the head arrives at (2,1), the tail has moved on. The move is **legal**, and the snake chases its own tail around the square: + +``` + before: (1,1) (1,2) (2,2) (2,1) after: (2,1) (1,1) (1,2) (2,2) +``` + +In the second board, the head lands on the apple, so the snake **grows**: the tail stays where it is. Now the head and the tail would occupy the same cell, which is a **collision**: game over. + +So the right check is not "is the new head on the current snake?" but "is the new head on the **body that remains after this step**?" And the remaining body depends on whether the snake is eating: + +``` +eating? yes -> body = the whole old snake (tail stays, the snake grows) + no -> body = the old snake minus its last cell (tail leaves) +collision if the new head is off the board, or is contained in body +``` + +This is why `advance` must first decide whether it eats, then compute the body, and only then check for collisions. The order is the rule. The fixtures `advance-into-vacating-tail` and `advance-into-tail-while-eating` are exactly these two boards. Many implementations check the collision against the old snake and wrongly kill the player in the first case. + +
+ +### Puzzle 2: the quick double turn + +The snake moves one cell per tick (150 ms). The player can press several keys between two ticks. Suppose the snake is moving `Right`: + +``` + x=0 x=1 x=2 x=3 + y=1 . . . . + y=2 b b H . dir = Right, nextDir = Right +``` + +Within one tick, the player quickly presses **Up** and then **Left** (perhaps aiming for a U-turn). The rule says "you may not turn straight back". Here are two ways to write that check: + +``` +version A: reject dir if dir == opposite(state.nextDir) +version B: reject dir if dir == opposite(state.dir) +``` + +**Before reading on, trace both versions.** Write down `dir` and `nextDir` after each key press, and then where the head goes on the next tick. Which version lets the player kill the snake with two key presses? + +
Think about it first, then open the explanation + +**Version A** (check against `nextDir`): + +1. Press Up. Opposite of `nextDir` (Right) is Left; Up is fine. Now `nextDir = Up`. The snake has not moved: `dir` is still Right. +2. Press Left. Opposite of `nextDir` (Up) is Down; Left is fine! Now `nextDir = Left`. +3. Tick. The head moves **Left**, from (2,2) to (1,2), straight into its own neck. Game over, from two perfectly reasonable key presses. + +The check compared against a direction the snake *had not moved in yet*. The snake's body still points the old way. + +**Version B** (check against `dir`, the direction the snake **last moved**): + +1. Press Up. Opposite of `dir` (Right) is Left; Up is fine. `nextDir = Up`. +2. Press Left. Opposite of `dir` (still Right, nothing has moved) is Left: **rejected**. The state is unchanged; `nextDir` stays Up. +3. Tick. The head moves Up. Safe. + +That is why the model has two directions (Module 2, question 3): `dir` is a fact about the body (where the neck is), `nextDir` is a wish. Turns are validated against the fact. The fixture `turn-against-last-moved-dir` is exactly this situation: `dir = Right`, `nextDir = Up`, turn `Left`, expected result: unchanged. + +Notice the consequences of the rule: several allowed turns within one tick overwrite each other, and "the last allowed one wins" (fixture `turn-replaces-pending-turn`: with `dir = Right` and `nextDir = Up`, pressing `Down` gives `nextDir = Down`, which is allowed because Down is not the opposite of Right). And the player's U-turn intention (Up, then Left) is partly lost: only Up happens. Some games keep a small queue of pending turns to honour both; see the questions at the end. + +
+ +### What a crash looks like + +When the head would leave the board or hit the remaining body, `advance` returns the state with: + +- `status: 'GameOver'`, +- `dir` set to the direction it tried to move in, +- everything else, **including the snake, unchanged**. + +The snake is not moved into the wall: the last picture the player sees is the snake just before the crash. Recording `dir` keeps a simple invariant: after any `advance` of a playing game, `dir === nextDir`. The fixture `advance-into-tail-while-eating` checks this: before the step `dir` is `Up` and `nextDir` is `Right`; the snake crashes, and the result has `dir: 'Right'` with the snake exactly as it was. + +### Reading a fixture + +A fixture case is `{ name, args, expected }`. For `advance`, `args` is a one-element list (the state) and `expected` is the resulting state. Here is the shape of `advance-into-wall-is-game-over`, trimmed: + +```json +{ "name": "advance-into-wall-is-game-over", + "args": [ { "width": 5, "height": 5, "snake": [ {"x": 4, "y": 2}, {"x": 3, "y": 2}, {"x": 2, "y": 2} ], + "dir": "Right", "nextDir": "Right", "apple": null, "status": "Playing", ... } ], + "expected": { "width": 5, "height": 5, "snake": [ {"x": 4, "y": 2}, {"x": 3, "y": 2}, {"x": 2, "y": 2} ], + "dir": "Right", "nextDir": "Right", "apple": null, "status": "GameOver", ... } } +``` + +When a fixture test fails, find the case by name, draw the board, and compute by hand what each phase (compute, decide, build) should give. Then compare with what your function returns. Usually one field differs, and the field tells you which phase is wrong. + +> **From Scala:** in Scala, the enum values are qualified (`Status.Playing`, `Direction.Up`); add `import Direction.*` and `import Status.*` at the top of `Rules.scala` if you prefer the short names. Comparing a `Status` uses `==` (or `!=`). In F#, union cases are unqualified (`Playing`, `Up`), and record fields are PascalCase (`state.NextDir`). + +### Using what you built + +`rules` builds on Modules 1 and 2: + +- `delta` and `opposite` from `model`; +- `dropLast` and `contains` from `util`; +- `pointEquals` (JS) to compare points; `==` / `=` in Scala and F#. + +In JS, import them at the top of `rules.js`, for example `import { delta, opposite, pointEquals } from './model.js'` and `import { dropLast, contains } from './util.js'`. In Scala, all files are in package `snake`, so write `Model.delta`, `Util.dropLast` (or `import Model.*`). In F#, refer to `Model.delta` and `Util.dropLast`, or `open` those modules. + +## Exercises + +### Exercise 3.1 — `move` + +**JavaScript** +```js +export const move = (p, dir) => ... // move({x: 2, y: 2}, 'Up') -> {x: 2, y: 1} +``` +**Scala** +```scala +def move(p: Point, dir: Direction): Point +``` +**F#** +```fsharp +let move (p: Point) (dir: Direction) : Point +``` + +Fixture: `fixtures/m03-move.json` (`move-up`, `move-down`, `move-left`, `move-right`, `move-does-not-wrap-at-the-edge`). + +### Exercise 3.2 — `inBounds` + +**JavaScript** +```js +export const inBounds = (state, p) => ... // true iff 0 <= p.x < width and 0 <= p.y < height +``` +**Scala** +```scala +def inBounds(state: GameState, p: Point): Boolean +``` +**F#** +```fsharp +let inBounds (state: GameState) (p: Point) : bool +``` + +Fixture: `fixtures/m03-in-bounds.json` (corners, negatives, `x-equals-width`, `y-equals-height`, `middle`). Watch out: four conditions, and two of them are strict. + +### Exercise 3.3 — `turn` + +Returns `state` unchanged unless the status is `Playing` **and** `dir` is not the opposite of `state.dir`. Otherwise sets `nextDir = dir` (and nothing else). + +**JavaScript** +```js +export const turn = (state, dir) => ... +``` +**Scala** +```scala +def turn(state: GameState, dir: Direction): GameState +``` +**F#** +```fsharp +let turn (state: GameState) (dir: Direction) : GameState +``` + +Fixture: `fixtures/m03-turn.json`: `turn-normal-perpendicular`, `turn-same-direction-is-fine`, `turn-reverse-rejected`, `turn-replaces-pending-turn`, `turn-against-last-moved-dir` (Puzzle 2), `turn-ignored-when-paused`, `turn-ignored-when-game-over`, `turn-ignored-when-won`. + +Watch out: compare against `dir`, not `nextDir`. And "unchanged" means returning the very same state, not a copy with the same values; both pass the tests, but returning `state` makes the intent obvious. + +### Exercise 3.4 — `advance` + +One step of the game. Unchanged unless `Playing`. Otherwise, with `d = nextDir` and `h = move(head, d)`: + +- `eating` is true when there is an apple and `h` is on it; +- the remaining `body` is the whole snake when eating, otherwise the snake without its last cell; +- if `h` is out of bounds or `body` contains `h`: status `GameOver`, `dir = d`, snake unchanged; +- otherwise: the snake becomes `h` followed by `body`, `dir = d`, the apple is removed if eaten, and the score goes up by one if eaten. + +**JavaScript** +```js +export const advance = (state) => ... +``` +**Scala** +```scala +def advance(state: GameState): GameState +``` +**F#** +```fsharp +let advance (state: GameState) : GameState +``` + +Fixture: `fixtures/m03-advance.json`, twelve cases including `advance-plain-move`, `advance-moves-in-nextDir-and-records-it-as-dir`, `advance-eat-grows-scores-clears-apple`, `advance-into-wall-is-game-over`, `advance-into-own-body-is-game-over`, `advance-into-vacating-tail` and `advance-into-tail-while-eating` (Puzzle 1), and the three "unchanged" cases for `Paused`, `GameOver`, `Won`. + +Watch out: +- In JS the apple may be `null`; check before comparing. In Scala/F# compare `apple` with `Some(h)` / `Some h`, or pattern-match. +- Eating does **not** place a new apple. `advance` sets it to none; Module 4 adds the spawning. +- The score only changes when eating; `seed` and `frame` never change here. + +### Run + +- JS: `cd js && node --test "test/m03-*.test.js"` +- Scala: `cd scala && ./test.sh M03` +- F#: `cd fsharp && ./test.sh M03` + +## Hints + +
Exercise 3.1 (move) — nudge + +`delta(dir)` gives you a point to add. + +
+ +
Exercise 3.1 (move) — stronger hint + +Build a new point whose `x` is `p.x + d.x` and whose `y` is `p.y + d.y`, where `d = delta(dir)`. + +
+ +
Exercise 3.1 (move) — near-solution (try the others first) + +```js +export const move = (p, dir) => { const d = delta(dir); return point(___, ___) } +``` +```scala +def move(p: Point, dir: Direction): Point = { val d = Model.delta(dir); Point(___, ___) } +``` +```fsharp +let move (p: Point) (dir: Direction) : Point = let d = Model.delta dir in { X = ___; Y = ___ } +``` + +
+ +
Exercise 3.2 (inBounds) — nudge + +It is one boolean expression with four comparisons joined by "and". + +
+ +
Exercise 3.2 (inBounds) — stronger hint + +Valid `x` values on a board of width 5 are 0, 1, 2, 3, 4. So `x >= 0` and `x < width`. Same for `y` and `height`. + +
+ +
Exercise 3.3 (turn) — nudge + +Write the guard first: in which two situations does the function return `state` as it is? Then the one-field copy. + +
+ +
Exercise 3.3 (turn) — stronger hint + +The guard is "status is not Playing, **or** `dir` equals `opposite(state.dir)`". If the guard holds, return `state`; otherwise return a copy with `nextDir` replaced. Re-read Puzzle 2 if `turn-against-last-moved-dir` fails. + +
+ +
Exercise 3.3 (turn) — near-solution (try the others first) + +```js +export const turn = (state, dir) => + state.status !== ___ || dir === opposite(___) ? state : { ...state, ___ } +``` +```scala +def turn(state: GameState, dir: Direction): GameState = + if state.status != ___ || dir == Model.opposite(___) then state + else state.copy(___) +``` +```fsharp +let turn (state: GameState) (dir: Direction) : GameState = + if state.Status <> ___ || dir = Model.opposite ___ then state + else { state with ___ } +``` + +
+ +
Exercise 3.4 (advance) — nudge + +Follow "compute, decide, build". Compute four named values in this order: the direction `d`, the new head `h`, `eating`, `body`. Then decide between two outcomes. + +
+ +
Exercise 3.4 (advance) — stronger hint + +- The head is the first element of the snake (`state.snake[0]`, `state.snake.head`, `List.head state.Snake`). +- `body` uses `dropLast` when not eating. +- The collision test uses `inBounds` and `contains(body, h, pointEquals)` (Scala/F#: `body.contains(h)` / `List.contains h body` work too, thanks to structural equality). +- The new snake is `h` in front of `body`: `[h, ...body]`, `h :: body`. +- If `advance-into-vacating-tail` fails, you are probably checking against the whole snake instead of `body`. If `advance-into-tail-while-eating` fails, you are probably dropping the tail even when eating. + +
+ +
Exercise 3.4 (advance) — near-solution (try the others first) + +```js +export const advance = (state) => { + if (state.status !== 'Playing') return state + const d = ___ + const h = move(___, d) + const eating = state.apple !== null && ___ + const body = eating ? ___ : ___ + if (!inBounds(state, h) || ___) return { ...state, dir: d, status: ___ } + return { ...state, snake: ___, dir: d, apple: ___, score: ___ } +} +``` +```scala +def advance(state: GameState): GameState = + if state.status != Status.Playing then state + else + val d = ___ + val h = move(___, d) + val eating = state.apple.contains(___) + val body = if eating then ___ else ___ + if !inBounds(state, h) || ___ then state.copy(dir = d, status = ___) + else state.copy(snake = ___, dir = d, apple = ___, score = ___) +``` +```fsharp +let advance (state: GameState) : GameState = + if state.Status <> Playing then state + else + let d = ___ + let h = move ___ d + let eating = (state.Apple = Some h) + let body = if eating then ___ else ___ + if not (inBounds state h) || ___ then { state with Dir = d; Status = ___ } + else { state with Snake = ___; Dir = d; Apple = ___; Score = ___ } +``` + +
+ +## Check your understanding + +1. Why does `turn` return the state unchanged when paused, instead of throwing? Who would have to deal with the exception, and how would that change the rest of the program? +2. Without looking: on a 5 x 5 board, the snake is `(1,1) (1,2) (2,2) (2,1)` heading Up with `nextDir = Right` and an apple at (2,1). What does `advance` return? Now the same with no apple. +3. Explain to a friend, with a drawing, why `turn` must compare against `dir` and not `nextDir`. What sequence of key presses kills the snake in the wrong version? +4. With the correct `turn`, the player presses Up then Left within one tick, intending a quick U-turn, and only Up happens. Sketch a different model that would honour both presses. What would you add to `GameState`, and how would `advance` change? +5. After `advance` crashes into a wall, the snake is unchanged but `dir` is updated. What invariant does that keep? Would anything visible break if `dir` were left unchanged? +6. `advance` does not place a new apple after eating. Why is it a good idea to keep that out of `advance`? (Think about what `advance` would need as input.) +7. Predict: `advance(advance(s))` where `s` is a `GameOver` state. And `turn(advance(s), 'Up')` for the same `s`. +8. Why are the fixture boards 5 x 5 rather than the real 20 x 20? What does that tell you about the decision to store `width` and `height` in the state? + +## In production + +- **Redux reducers** are rules of exactly this shape, `(state, action) => newState`, and the Redux docs insist they must be pure and must not mutate: see [Reducers](https://redux.js.org/tutorials/fundamentals/part-3-state-actions-reducers). +- **Elm's** `update` function plays the same role; the [Elm Architecture](https://guide.elm-lang.org/architecture/) guide shows it for a counter, then for larger apps. +- Games that need **exact replays** or **networked lockstep** (many real-time strategy games) are built around deterministic step functions like `advance`: all players run the same rules on the same inputs and get the same state. See Glenn Fiedler's [Deterministic Lockstep](https://gafferongames.com/post/deterministic_lockstep/). + +## Further reading + +- [Total functional programming (Wikipedia)](https://en.wikipedia.org/wiki/Total_functional_programming) — the idea of insisting every function is total, taken to its logical end. +- [Snake (video game genre) (Wikipedia)](https://en.wikipedia.org/wiki/Snake_%28video_game_genre%29) — the history of the game, and the many rule variants (wrapping edges, tail biting). +- [Mostly Adequate Guide, chapter 3: Pure Happiness with Pure Functions](https://mostly-adequate.gitbook.io/mostly-adequate-guide/) — the case for pure functions in JavaScript, with testing in mind. +- [Scott Wlaschin, "Designing with types"](https://fsharpforfunandprofit.com/posts/designing-with-types-making-illegal-states-unrepresentable/) — useful again here: which checks in `advance` could types remove? +- [Why Functional Programming Matters](https://www.cs.kent.ac.uk/people/staff/dat/miranda/whyfp90.pdf) — Hughes' argument that small pure pieces glue together into larger ones, which is what `turn` and `advance` are about to do in Module 5. diff --git a/course/04-randomness-without-side-effects.md b/course/04-randomness-without-side-effects.md new file mode 100644 index 0000000..2ae25ac --- /dev/null +++ b/course/04-randomness-without-side-effects.md @@ -0,0 +1,646 @@ +# Module 4 — Randomness without side effects + +> **Time:** ~4 h · **You'll edit:** JS `js/src/random.js`, `js/src/spawn.js` · Scala `scala/src/Random.scala`, `scala/src/Spawn.scala` · F# `fsharp/src/Random.fs`, `fsharp/src/Spawn.fs` +> **Tests:** JS `cd js && node --test "test/m04-*.test.js"` · Scala `cd scala && ./test.sh M04` · F# `cd fsharp && ./test.sh M04` +> **Prerequisites:** Modules 1–3 + +## Why this module + +After the snake eats, a new apple must appear on a random free cell. The usual code is one line: + +```js +const i = Math.floor(Math.random() * cells.length) +``` + +That line quietly destroys everything you have built so far: + +- **It is not pure.** `Math.random()` takes no arguments and returns a different number each time. A function that calls it is not referentially transparent, so `spawnApple(state)` would no longer mean one thing. +- **It cannot be tested exactly.** What should the test for "eating spawns a new apple" expect? You can only check vague properties ("the apple is somewhere free"). +- **It cannot be replayed.** A player reports a bug after 300 moves. You have their key presses, but not the random numbers, so you cannot reproduce the game. + +The fix is an old idea: a pseudo-random generator is not random at all. It is a **deterministic function** from a state (the seed) to a number and a new state. If we make that function explicit and *carry the seed in the game state* (that is why `seed` is in `GameState`, Module 2), randomness becomes pure. Same seed, same game. + +Carrying the seed by hand is tedious and error-prone, though. Fixing *that* leads you to discover `map` and `flatMap` for random generators, and to a pattern with a famous name: the **State monad**. + +## Learning objectives + +After this module you can: + +- **explain** why `Math.random` breaks purity and testing, and how a seed fixes it; +- **describe** at a high level how mulberry32 turns a seed into a number and a new seed, and why 32-bit arithmetic must be handled carefully in each language; +- **implement** `Random
= seed → (A, seed)` with `pure`, `map` and `flatMap`, and recognise it as the State monad; +- **use** Scala's for-comprehension and F#'s computation expressions as syntax for the same chaining; +- **implement** apple spawning, including the full-board edge case, and explain why "same seed ⇒ same game". + +## Concepts + +### Pseudo-random numbers are a pure function in disguise + +A pseudo-random number generator (PRNG) keeps a small internal state. Each time you ask for a number, it computes the number from the state and updates the state. `Math.random()` does exactly this, but the state is hidden inside the JavaScript engine and seeded from something unpredictable when the program starts. + +Take the hidden state out, and what remains is a plain function: + +``` +next : seed -> (value, nextSeed) +``` + +Call it with the same seed, you get the same value and the same next seed, every time. To get a *sequence* of numbers, you feed each returned seed into the next call: + +``` +seed0 --next--> (v1, seed1) --next--> (v2, seed2) --next--> (v3, seed3) ... +``` + +That is the whole trick. The numbers look random (they pass statistical tests), but they are completely determined by the first seed. Where does the *first* seed come from? From the outside world, once, at the very edge of the program (in Module 9, the runtime asks the browser's `crypto` for one). Everything after that is pure. + +> **From Scala:** `scala.util.Random` and `java.util.Random` are objects with a mutable seed inside: `rng.nextInt()` changes `rng`. Even `new Random(42)` only makes the *start* reproducible; the object is still mutated by each call, so `rng.nextInt() + rng.nextInt()` is not referentially transparent. We want the seed in our hands, as a value. + +### mulberry32, in plain words + +The course uses a small, fast generator called **mulberry32**. You do not need to understand why its constants work (that is research), but you should understand its structure, because it is surprisingly simple: + +1. **Advance a counter.** The new seed is the old seed plus a fixed odd constant, `0x6D2B79F5`, wrapped around modulo 2^32. Because the constant is odd, repeatedly adding it visits all 2^32 possible seeds before repeating. That is the *period* of the generator: about 4.3 billion numbers. +2. **Scramble the counter.** The output value is a "mix" of the new seed: a few rounds of XOR-with-a-shifted-copy and 32-bit multiplication. These operations spread every bit of the input over every bit of the output, so consecutive seeds (which differ by a constant) give outputs that look unrelated. + +In other words: the seed is only a counter, and the "randomness" is a hash of the counter. In pseudo-code, with all arithmetic on unsigned 32-bit integers: + +``` +s = (seed + 0x6D2B79F5) mod 2^32 -- the next seed +t = imul(s xor (s >>> 15), s or 1) +t = t xor (t + imul(t xor (t >>> 7), t or 61)) +value = t xor (t >>> 14) -- as an unsigned 32-bit number +return (value, s) +``` + +(`imul` is 32-bit multiplication, keeping only the low 32 bits; `>>>` is an unsigned right shift.) mulberry32 is fine for games and simulations, and **not** suitable for anything security-related: given one output, the next ones can be predicted. + +For example, from seed `0` the first draw gives the value `1144304738` and the new seed `1831565813` (which is `0x6D2B79F5`: zero plus the constant). The next draw, from seed `1831565813`, gives `1416247` and seed `3663131626`. These are the first two cases of `fixtures/m04-random.json`. + +### The same 32 bits in three languages + +The three tracks must produce **bit-identical** numbers, otherwise the shared fixtures (and apple positions) would differ between languages. That is harder than it sounds, because the languages treat integers differently: + +| | JavaScript | Scala | F# | +|---|---|---|---| +| Seed type | `number` (a 64-bit float) | `Long` (holds 0 until 2^32) | `uint32` | +| 32-bit multiply | `Math.imul(a, b)` | `Int` `*` wraps around | `uint32` `*` wraps around | +| Unsigned shift | `>>>` | `>>>` on `Int` | `>>>` on `uint32` (unsigned type, so logical) | +| XOR / OR | `^` / `\|` | `^` / `\|` | `^^^` / `\|\|\|` | +| Back to unsigned | `x >>> 0` | `x.toLong & 0xFFFFFFFFL` | already unsigned | + +The usual bugs: in JS, forgetting that `+` and `*` on numbers do not wrap at 2^32 (so you get huge or fractional values), or that `^` and `|` produce *signed* 32-bit results (so you get negative numbers). In Scala, doing the arithmetic in `Long` (which does not wrap at 32 bits) or forgetting to convert the final `Int` back to unsigned. The fixture `m04-random.json` checks ten consecutive draws from seeds `0`, `1` and `4294967295` (the largest seed), which catches every one of these mistakes. + +### Threading seeds by hand + +Suppose you have `nextUint32 : seed → [value, seed]` and you want a die roll, then the sum of two dice. Written directly (JavaScript, as an illustration): + +```js +const d6 = (seed) => { + const [v, s] = nextUint32(seed) + return [1 + (v % 6), s] +} + +const twoDice = (seed) => { + const [a, s1] = d6(seed) + const [b, s2] = d6(s1) + return [a + b, s2] +} +``` + +It works, and it is pure. But look at how much of it is plumbing: every line unpacks a pair, passes the seed on, and repacks a pair. And the plumbing is easy to get wrong: + +```js +const twoDiceBuggy = (seed) => { + const [a, s1] = d6(seed) + const [b, s2] = d6(s1) + const [c, s3] = d6(s1) // oops: s1 again. c always equals b. + return [a + b + c, s2] // and we return the wrong seed +} +``` + +Nothing stops you from reusing a seed or returning the wrong one, and the result is still "random-looking", so the bug is hard to spot. When code has the same plumbing everywhere, the functional move is to **name the pattern and write the plumbing once**. + +### A type for "something that needs a seed" + +Give the shape a name: + +``` +Random = seed -> [A, seed] +``` + +A `Random` is a *description* of how to produce an `A` from a seed: a recipe, not a number. `nextUint32` is a `Random`. `d6` is a `Random`. `twoDice` is a `Random`. Nothing happens until you **run** it with a seed: `run(r, seed)`. + +**JavaScript** +```js +// A Random is a plain function: seed => [value, nextSeed] +export const run = (r, seed) => ... +``` +**Scala** +```scala +// A case class wrapping the function; running it is r.run(seed) +final case class Random[A](run: Long => (A, Long)) +``` +**F#** +```fsharp +// A single-case union wrapping the function; running it is Random.run seed r +type Random<'a> = Random of (uint32 -> 'a * uint32) +``` + +Scala and F# wrap the function in a small type of its own (a case class, a single-case union) so that the compiler can tell a `Random` apart from any other function with the same shape, and so that `map` and `flatMap` can be attached to it. F#'s `run` takes the seed first and the `Random` last (data last), so you can write `r |> Random.run 42u`. + +Now look at the plumbing in `d6` and `twoDice` again. There are only two patterns in it. + +**Pattern 1: change the value, keep the seed.** `d6` runs `nextUint32`, transforms the value (`1 + v % 6`) and passes the seed through untouched. Call this **`map`**: + +``` +map(r, f) : Random where r : Random, f : A -> B + + seed --r--> [a, s1] gives [f(a), s1] +``` + +**Pattern 2: run one, then choose the next based on its value, and run that with the leftover seed.** `twoDice` runs `d6`, and then runs "another `d6`, and add `a` to its result", which depends on `a`. Call this **`flatMap`**: + +``` +flatMap(r, f) : Random where r : Random, f : A -> Random + + seed --r--> [a, s1] + a --f--> r2 + s1 --r2--> [b, s2] gives [b, s2] +``` + +And one trivial pattern: **a value that needs no randomness**, which leaves the seed alone. Call it **`pure`**: + +``` +pure(a) : Random seed gives [a, seed] +``` + +With these, the dice need no plumbing at all, and the seed-reuse bug cannot even be written: + +**JavaScript** +```js +const d6 = map(nextUint32, (v) => 1 + (v % 6)) +const twoDice = flatMap(d6, (a) => map(d6, (b) => a + b)) +run(twoDice, 42) // [sum, nextSeed], the same every time for seed 42 +``` + +**Scala** +```scala +val d6: Random[Int] = Random.nextUint32.map(v => 1 + (v % 6).toInt) +val twoDice: Random[Int] = d6.flatMap(a => d6.map(b => a + b)) +twoDice.run(42L) +``` + +**F#** +```fsharp +let d6 : Random = Random.nextUint32 |> Random.map (fun v -> 1 + int (v % 6u)) +let twoDice : Random = d6 |> Random.bind (fun a -> d6 |> Random.map (fun b -> a + b)) +Random.run 42u twoDice +``` + +Naming differences to note (the full table is in `course/reference.md`): + +- JS functions take the data first: `map(r, f)`. Scala uses methods: `r.map(f)`, and `Random.pure(a)` lives in the companion object. F# functions take the data last so they fit pipelines: `r |> Random.map f`. +- F# calls `flatMap` **`bind`**, which is the traditional ML/Haskell name. + +You are going to implement `pure`, `map`, `flatMap` and `run` yourself in Exercise 4.2. The diagrams above are the specification. + +### You have met this before: the State monad + +Look at the type again: `seed → [A, seed]`. Replace "seed" by any state type `S`: + +``` +State = S -> (A, S) +``` + +This is the **State monad**: a computation that reads a state, produces a value, and hands on a new state, with `pure` and `flatMap` to sequence such computations without writing the plumbing. Our `Random` is `State`. It is a pure, honest version of "a function that uses and updates a variable": the "variable" is threaded through explicitly, by `flatMap`, so nothing is hidden. + +> **From Scala:** you have used `map` and `flatMap` on `List` and `Option`. Any type with a sensible `pure` and `flatMap` is a **monad**, and `Random` is one. "Sensible" means three laws hold: +> - **left identity:** `flatMap(pure(a), f)` behaves like `f(a)`; +> - **right identity:** `flatMap(r, pure)` behaves like `r`; +> - **associativity:** `flatMap(flatMap(r, f), g)` behaves like `flatMap(r, a => flatMap(f(a), g))`. +> +> "Behaves like" means "gives the same result when run with any seed". The tests check the two identity laws (`flatMap with pure on the left or right changes nothing`). + +Because `Random` has `map` and `flatMap`, Scala's **for-comprehension** works on it directly. A for-comprehension is only syntax: the compiler rewrites it into `flatMap` and `map` calls. + +**Scala** +```scala +val twoDice: Random[Int] = + for + a <- d6 + b <- d6 + yield a + b +// the compiler turns this into: d6.flatMap(a => d6.map(b => a + b)) +``` + +F# has the same idea, called a **computation expression**. You define a small "builder" object that tells the compiler which function is `bind` and which is `return`, and then you can write: + +**F#** +```fsharp +type RandomBuilder() = + member _.Bind(r, f) = Random.bind f r + member _.Return(a) = Random.pure a +let random = RandomBuilder() + +let twoDice : Random = + random { + let! a = d6 + let! b = d6 + return a + b + } +``` + +`let!` means "run this `Random` and bind its value", which is `bind`. F#'s `async { }`, `task { }` and `seq { }` are built the same way. (The builder is optional; the course code does not need it.) + +JavaScript has no such syntax for user-defined types, so you write the nested `flatMap`/`map` calls. Once you see them as "and then, with the result...", they read naturally. + +### Drawing a number below `n` + +To pick one of `n` free cells, you need a number in `[0, n)`. `intBelow(n)` is `nextUint32` with its value taken modulo `n`. That is a `map`. + +(Strictly, `v % n` is slightly biased when 2^32 is not a multiple of `n`: small results are a tiny bit more likely. For `n` up to 400 the bias is below one in ten million, far below anything a player could notice. Libraries that need exact uniformity reject and redraw; we keep it simple and identical across tracks.) + +### Spawning an apple + +Now apply it to the game. `spawnApple(state)`: + +1. If there already is an apple, or the game is not `Playing`, return `state` unchanged. +2. List the **free cells**: every cell of the board that is not part of the snake, in **row-major order** (row `y = 0` from left to right, then row `y = 1`, ...). +3. Draw an index `i` below the number of free cells, from `state.seed`. +4. Return the state with `apple` set to the `i`-th free cell and `seed` set to the seed the draw left behind. + +Why does the order of free cells matter? Because the index `i` must mean the same cell in every track. "Row-major" is part of the contract; the fixture `free-cells-row-major-order` checks it. + +A worked example from the tests. A 3 x 2 board, a one-cell snake at (0,0) about to eat the apple at (1,0), seed 0: + +``` + before tick after advance after spawnApple + x: 0 1 2 x: 0 1 2 x: 0 1 2 +y=0 H A . y=0 b H . y=0 b H . +y=1 . . . y=1 . . . y=1 . A . +``` + +After `advance`, the snake is `(1,0) (0,0)` and there is no apple. Free cells, row-major: `(2,0), (0,1), (1,1), (2,1)`, four of them. From seed 0, `nextUint32` gives `1144304738`, and `1144304738 % 4 = 2`, so the apple goes to the cell at index 2: `(1,1)`. The new seed is `1831565813`. That is the test `tick: eating the apple spawns a new one in the same step`. + +### Puzzle: the board is full + +What happens when the snake fills the entire board? There are no free cells. Before reading on, work out what *your* `spawnApple` would do when the list of free cells is empty, in your language. What is `v % 0`? + +
Think about it first, then open the explanation + +- **JavaScript:** `v % 0` is `NaN`, `cells[NaN]` is `undefined`, so `apple` becomes `undefined`. No crash, only a corrupt state that will cause confusing errors later. +- **Scala:** `v % 0` on integers throws `ArithmeticException: / by zero`. The game crashes at its moment of triumph. +- **F#:** `v % 0u` throws `DivideByZeroException`. + +None of these is a rule of the game. A full board means the player has won. So `spawnApple` handles it explicitly: **if there are no free cells, the status becomes `Won`** (no apple, seed unchanged). This is the fixture `spawn-on-full-board`: a 3 x 2 board completely covered by a six-cell snake. + +This is a pattern worth remembering: when an operation has no valid answer for some input (here: "pick one of zero cells"), find out what that input *means in the domain*, and make it an explicit, total case. + +
+ +### Putting the pieces together: `tick` and `newGame` + +Two small functions finish the module: + +- **`tick(state)`** is one complete game step: `advance`, then `spawnApple`. If the snake ate, the apple is gone after `advance`, and `spawnApple` places a new one. If it did not eat, there still is an apple, so `spawnApple` leaves the state (and the seed!) unchanged. Test: `tick leaves the seed alone when no apple needs spawning`. +- **`newGame(seed)`** is `initialState(seed)` with its first apple already placed. + +Both are compositions of functions you already have. You wrote `compose` in Module 1; in Scala, `andThen`; in F#, `>>`. + +And now the payoff: `newGame(42)` is the same game every time you call it. Any sequence of `tick`s and `turn`s from it gives the same states, apples included. Module 5 turns that into replays. + +## Exercises + +### Exercise 4.1 — `nextUint32` + +Implement mulberry32 as described above (the formula is also in the doc comment of your stub). + +**JavaScript** +```js +export const nextUint32 = (seed) => ... // nextUint32(0) -> [1144304738, 1831565813] +``` +**Scala** +```scala +lazy val nextUint32: Random[Long] // Random.nextUint32.run(0L) == (1144304738L, 1831565813L) +``` +**F#** +```fsharp +let nextUint32 : Random // Random.run 0u Random.nextUint32 = (1144304738u, 1831565813u) +``` + +Fixture: `fixtures/m04-random.json`, cases `seed-0-step-1` ... `seed-4294967295-step-10`. Each step starts from the previous step's new seed. + +Watch out: see the 32-bit table above. Both the value **and** the new seed must be unsigned and below 2^32. In Scala, do the arithmetic in `Int` (where `*` wraps) and convert at the end. + +### Exercise 4.2 — `pure`, `map`, `flatMap`, `run` + +**JavaScript** +```js +export const pure = (a) => ... // Random
+export const map = (r, f) => ... // Random, (A -> B) -> Random +export const flatMap = (r, f) => ... // Random, (A -> Random) -> Random +export const run = (r, seed) => ... // [A, seed] +``` +**Scala** +```scala +final case class Random[A](run: Long => (A, Long)): + def map[B](f: A => B): Random[B] + def flatMap[B](f: A => Random[B]): Random[B] +object Random: + def pure[A](a: A): Random[A] +``` +**F#** (in `module Snake.Random`) +```fsharp +let run (seed: uint32) (r: Random<'a>) : 'a * uint32 +let pure (a: 'a) : Random<'a> +let map (f: 'a -> 'b) (r: Random<'a>) : Random<'b> +let bind (f: 'a -> Random<'b>) (r: Random<'a>) : Random<'b> +``` + +(In Scala, `run` is the case class's field, so there is nothing to write for it. In F#, `pure` is a word the compiler reserves for future use; the project file silences that warning, so the name is fine.) + +Tests (in `m04-random.test.js` / `M04Random` / `M04`): `pure returns its value and leaves the seed alone`, `map applies f to the value and keeps the seed from the underlying step`, `map with the identity function changes nothing`, `flatMap threads the seed through both steps`, `flatMap with pure on the left or right changes nothing`, `running a Random does not mutate anything: same seed, same answer`. + +Watch out: each of these returns a **function** (JS) or a `Random(...)` wrapping one (Scala, F#). Nothing is run when you call `map` or `flatMap`; only `run` runs. + +### Exercise 4.3 — `intBelow` + +**JavaScript** +```js +export const intBelow = (n) => ... // Random in [0, n) +``` +**Scala** +```scala +def intBelow(n: Int): Random[Int] +``` +**F#** +```fsharp +let intBelow (n: int) : Random +``` + +Fixture: `fixtures/m04-int-below.json` (e.g. `below-6-from-seed-0`, `below-400-from-seed-42`). Test: `intBelow always lands in [0, n)`. Write it with `map`, not by unpacking pairs. + +### Exercise 4.4 — `freeCells` + +Every cell not covered by the snake, row-major (`y` outer, `x` inner). The apple does **not** count as occupied. + +**JavaScript** +```js +export const freeCells = (state) => ... // Point[] +``` +**Scala** +```scala +def freeCells(state: GameState): List[Point] +``` +**F#** +```fsharp +let freeCells (state: GameState) : Point list +``` + +Fixture: `fixtures/m04-free-cells.json` (`free-cells-row-major-order`, `free-cells-skip-snake`, `free-cells-snake-in-second-row`, `free-cells-apple-cell-is-still-free`, `free-cells-full-board-is-empty`). Your `range` and `contains` from Module 1 are useful here. + +### Exercise 4.5 — `spawnApple` + +**JavaScript** +```js +export const spawnApple = (state) => ... +``` +**Scala** +```scala +def spawnApple(state: GameState): GameState +``` +**F#** +```fsharp +let spawnApple (state: GameState) : GameState +``` + +Unchanged if there is an apple or the status is not `Playing`; `Won` if no cell is free; otherwise the apple goes to `freeCells[i]` where `[i, s'] = run(intBelow(count), state.seed)`, and `seed` becomes `s'`. + +Fixture: `fixtures/m04-spawn.json` (`spawn-picks-a-free-cell-and-advances-seed`, `spawn-other-seed-picks-other-cell`, `spawn-with-one-free-cell`, `spawn-on-full-board`, and four "unchanged" cases). + +Watch out: check the full board **before** drawing (see the puzzle). In Scala/F#, the apple is `Some(cell)` / `Some cell`. + +### Exercise 4.6 — `tick` and `newGame` + +**JavaScript** +```js +export const tick = (state) => ... // advance, then spawnApple +export const newGame = (seed) => ... // initialState, then spawnApple +``` +**Scala** +```scala +def tick(state: GameState): GameState +def newGame(seed: Long): GameState +``` +**F#** +```fsharp +let tick (state: GameState) : GameState +let newGame (seed: uint32) : GameState +``` + +Fixtures: `fixtures/m04-tick.json` (`eat-then-respawn`, `plain-move-no-respawn`, `wall-hit-is-game-over-no-spawn`, `game-over-state-unchanged`) and `fixtures/m04-new-game.json` (`seed-0`, `seed-42`). Tests: `tick is spawnApple after advance`, `newGame is reproducible: same seed, same game`. + +### Run + +- JS: `cd js && node --test "test/m04-*.test.js"` (runs both `m04-random` and `m04-spawn`) +- Scala: `cd scala && ./test.sh M04` +- F#: `cd fsharp && ./test.sh M04` + +## Hints + +
Exercise 4.1 (nextUint32) — nudge + +Translate the pseudo-code line by line, then fix the integer behaviour. Start with seed 0: the new seed must be `1831565813`, which is `0 + 0x6D2B79F5`. Get that right first, then the value. + +
+ +
Exercise 4.1 (nextUint32) — stronger hint + +- **JS:** `(seed + 0x6D2B79F5) >>> 0` wraps the sum into an unsigned 32-bit number. `Math.imul(a, b)` multiplies like a 32-bit machine. `^`, `|`, `>>>` work on 32-bit integers; `^` and `|` give *signed* results, so finish the value with `>>> 0`. Use `const` for each intermediate (you cannot reassign `t`; call them `t1`, `t2`). +- **Scala:** `val s = seed.toInt + 0x6D2B79F5` (Int addition wraps). Then the `Int` formula with `*`, `^`, `|`, `>>>`. Return `(value.toLong & 0xFFFFFFFFL, s.toLong & 0xFFFFFFFFL)`. Remember to wrap the function: `Random(seed => ...)`. +- **F#:** `let s = seed + 0x6D2B79F5u` (uint32 addition wraps). Then `^^^`, `|||`, `>>>`, `*` on `uint32`; literals need the `u` suffix (`1u`, `61u`). No conversion needed at the end. Wrap the function: `Random(fun seed -> ...)`. + +If seed 0 passes but seed 4294967295 fails, your addition is not wrapping. + +
+ +
Exercise 4.1 (nextUint32) — near-solution (try the others first) + +```js +export const nextUint32 = (seed) => { + const s = (seed + 0x6D2B79F5) >>> 0 + const t1 = Math.imul(s ^ (s >>> 15), ___) + const t2 = t1 ^ (t1 + Math.imul(___, ___)) + return [(t2 ^ (t2 >>> 14)) >>> 0, ___] +} +``` +```scala +lazy val nextUint32: Random[Long] = Random { seed => + val s = seed.toInt + 0x6D2B79F5 + val t1 = (s ^ (s >>> 15)) * ___ + val t2 = t1 ^ (t1 + ___ * ___) + val v = t2 ^ (t2 >>> 14) + (___, ___) +} +``` +```fsharp +let nextUint32 : Random = + Random(fun seed -> + let s = seed + 0x6D2B79F5u + let t1 = (s ^^^ (s >>> 15)) * ___ + let t2 = t1 ^^^ (t1 + ___ * ___) + (t2 ^^^ (t2 >>> 14), ___)) +``` + +
+ +
Exercise 4.2 (pure, map, flatMap, run) — nudge + +Follow the types. `map(r, f)` must return a `Random`, which is a function from a seed. So it starts with `(seed) => ...` (JS), `Random(seed => ...)` (Scala) or `fun seed -> ...` (F#). Inside, you have a seed and `r`. What can you do with them? + +
+ +
Exercise 4.2 (pure, map, flatMap, run) — stronger hint + +Draw the diagrams from the Concepts section and turn each arrow into a line of code. For `flatMap`: run `r` with the incoming seed to get `[a, s1]`; call `f(a)` to get the next `Random`; run *that* with `s1`; return its result as is. `run(r, seed)` in JS applies the function. In Scala, `r.run(seed)` is the field. In F#, write `run` first, by taking the function out of the union with a pattern (`let run seed (Random f) = ...` is allowed: a single-case union can be matched right in the parameter list); then use `run` inside `map` and `bind`, and wrap each result with `Random(fun seed -> ...)`. + +
+ +
Exercise 4.2 (pure, map, flatMap, run) — near-solution (try the others first) + +```js +export const pure = (a) => (seed) => [___, ___] +export const map = (r, f) => (seed) => { const [a, s1] = r(seed); return [___, ___] } +export const flatMap = (r, f) => (seed) => { const [a, s1] = r(seed); return ___(___) } +export const run = (r, seed) => ___ +``` +```scala +def map[B](f: A => B): Random[B] = Random { seed => val (a, s1) = run(seed); (___, ___) } +def flatMap[B](f: A => Random[B]): Random[B] = Random { seed => val (a, s1) = run(seed); ___.run(___) } +def pure[A](a: A): Random[A] = Random(seed => (___, ___)) +``` +```fsharp +let run (seed: uint32) (Random f: Random<'a>) : 'a * uint32 = ___ +let pure (a: 'a) : Random<'a> = Random(fun seed -> (___, ___)) +let map (f: 'a -> 'b) (r: Random<'a>) : Random<'b> = + Random(fun seed -> let (a, s1) = run seed r in (___, ___)) +let bind (f: 'a -> Random<'b>) (r: Random<'a>) : Random<'b> = + Random(fun seed -> let (a, s1) = run seed r in run ___ ___) +``` + +
+ +
Exercise 4.3 (intBelow) — nudge + +It is "the value of `nextUint32`, transformed". Which of your new functions transforms a value? + +
+ +
Exercise 4.3 (intBelow) — stronger hint + +`map` over `nextUint32` with a function that takes the remainder modulo `n`. In Scala the value is a `Long` and the result an `Int`: `(v % n).toInt`. In F#, `int (v % uint32 n)`. + +
+ +
Exercise 4.4 (freeCells) — nudge + +First build *all* cells in row-major order, then keep those not in the snake. + +
+ +
Exercise 4.4 (freeCells) — stronger hint + +All cells: for each `y` in `range(height)`, for each `x` in `range(width)`, the point `(x, y)`. In JS that is `flatMap` over the rows and `map` over the columns. In Scala, a for-comprehension over two ranges (with `yield`). In F#, a list comprehension `[ for y in ... do for x in ... -> ... ]`. Then `filter` with "not `contains` in the snake". + +
+ +
Exercise 4.4 (freeCells) — near-solution (try the others first) + +```js +export const freeCells = (state) => + range(state.height) + .flatMap((y) => range(___).map((x) => point(___, ___))) + .filter((p) => !contains(___, p, pointEquals)) +``` +```scala +def freeCells(state: GameState): List[Point] = + (for y <- Util.range(state.height); x <- Util.range(___) yield Point(___, ___)) + .filterNot(p => ___) +``` +```fsharp +let freeCells (state: GameState) : Point list = + [ for y in 0 .. state.Height - 1 do + for x in 0 .. ___ do + { X = ___; Y = ___ } ] + |> List.filter (fun p -> not (___)) +``` + +
+ +
Exercise 4.5 (spawnApple) — nudge + +Three outcomes: unchanged, `Won`, or a new apple with a new seed. Write the guards first, in that order, then the draw. + +
+ +
Exercise 4.5 (spawnApple) — stronger hint + +Compute `cells = freeCells(state)`. If it is empty, return the state with status `Won`. Otherwise run `intBelow(cells.length)` with `state.seed` and destructure the pair into the index and the new seed. Build the result with both `apple` and `seed` changed. + +
+ +
Exercise 4.5 (spawnApple) — near-solution (try the others first) + +```js +export const spawnApple = (state) => { + if (state.apple !== null || state.status !== 'Playing') return state + const cells = freeCells(state) + if (cells.length === 0) return { ...state, status: ___ } + const [i, seed] = run(intBelow(___), ___) + return { ...state, apple: ___, seed } +} +``` +The Scala and F# versions have the same shape: `if ... then state else` / `match cells with [] -> ... | _ -> ...`, then `val (i, s) = Random.intBelow(cells.length).run(state.seed)` / `let (i, s) = Random.run state.Seed (Random.intBelow (List.length cells))`, and `List.item i cells` picks the cell. + +
+ +
Exercise 4.6 (tick, newGame) — nudge + +Each is one line: a function applied to the result of another. + +
+ +
Exercise 4.6 (tick, newGame) — stronger hint + +`tick(state) = spawnApple(advance(state))`. You can write that call directly, or with your `compose` / `pipe` (JS), `andThen` (Scala) or `>>` (F#). Import `advance` from `rules` and `initialState` from `model`. + +
+ +## Check your understanding + +1. Why is `const roll = () => Math.floor(Math.random() * 6)` impure, while `d6 = map(nextUint32, v => 1 + v % 6)` is pure? Where did the "randomness" go? +2. Predict: `run(pure(7), 123)`. Then `run(map(pure(7), x => x * 2), 123)`. Then `run(flatMap(nextUint32, _ => pure('x')), 0)`. (For the last one, you know from the text what `nextUint32` does to seed 0.) +3. In the buggy `twoDiceBuggy`, which line would be impossible to write using only `map`/`flatMap`? Why? +4. `Random
` and `State`: what is `S` for our generator? Name another piece of the game state that could be threaded with the same pattern. +5. Write (on paper) the Scala for-comprehension for "draw three dice and return them as a tuple", then desugar it by hand into `flatMap`/`map`. +6. What would go wrong, across tracks, if `freeCells` listed cells column-major in JS but row-major in Scala? +7. What does `spawnApple` do on a full board, and why is "status becomes `Won`" better than letting `intBelow(0)` decide? +8. Why does `tick` leave the seed unchanged when the snake did not eat? What would break in replays if every tick consumed a random number anyway? (Hint: would it break anything? Think about determinism versus "which apple appears".) + +## In production + +- **Haskell's** [`random`](https://hackage.haskell.org/package/random) package uses pure generators: `uniformR :: g -> (a, g)`, exactly our shape. The **State monad** pattern is described on the [Haskell wiki](https://wiki.haskell.org/State_Monad). +- **cats** (Scala) has a general [`State`](https://typelevel.org/cats/datatypes/state.html) data type: `State[S, A]` with `map`, `flatMap`, `get`, `set`. *Functional Programming in Scala* builds a `RNG`/`State` very much like this module in its chapter on purely functional state. +- **Property-based testing** libraries such as [ScalaCheck](https://scalacheck.org/) and [fast-check](https://fast-check.dev/) generate random test inputs from a seed, so that a failing case can be reproduced by re-running with the same seed. Their generators are monads like `Random`. +- **Java's** [`SplittableRandom`](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/SplittableRandom.html) is a mutable object, but its algorithm (a counter plus a mixing function) has the same structure as mulberry32. +- **Games** that support replays or lockstep multiplayer use seeded PRNGs for exactly the reason in this module: same seed and same inputs give the same game. + +## Further reading + +- [mulberry32 and friends (Tommy Ettinger's gist)](https://gist.github.com/tommyettinger/46a874533244883189143505d203312c) — the generator's source, with notes on its quality. +- [bryc's collection of JavaScript PRNGs](https://github.com/bryc/code/blob/master/jshash/PRNGs.md) — a reference implementation of mulberry32 in JS, which the course's fixtures agree with. +- [Pseudorandom number generator (Wikipedia)](https://en.wikipedia.org/wiki/Pseudorandom_number_generator) — the general idea, periods, and why PRNGs are not for cryptography. +- [Xorshift (Wikipedia)](https://en.wikipedia.org/wiki/Xorshift) and [PCG](https://www.pcg-random.org/) — the family of shift-and-xor generators, and a well-documented modern alternative. +- [Monad (functional programming) (Wikipedia)](https://en.wikipedia.org/wiki/Monad_%28functional_programming%29) — the general definition and the laws, with the State monad as an example. +- [Functional Programming in Scala, 2nd edition](https://www.manning.com/books/functional-programming-in-scala-second-edition) — chapter 6, "Purely functional state", is this module in book form. +- [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 official docs for the syntax you saw. +- [Mostly Adequate Guide](https://mostly-adequate.gitbook.io/mostly-adequate-guide/) — chapters 8 and 9 build `map` and `chain` (flatMap) for containers in JavaScript. diff --git a/course/05-folding-time.md b/course/05-folding-time.md new file mode 100644 index 0000000..35e4c40 --- /dev/null +++ b/course/05-folding-time.md @@ -0,0 +1,359 @@ +# Module 5 — Folding time + +> **Time:** ~2–3 h · **You'll edit:** JS `js/src/game.js` · Scala `scala/src/Game.scala` · F# `fsharp/src/Game.fs` +> **Tests:** JS `cd js && node --test "test/m05-*.test.js"` · Scala `cd scala && ./test.sh M05` · F# `cd fsharp && ./test.sh M05` +> **Prerequisites:** Modules 1–4 + +## Why this module + +Every interactive program you have seen has a loop at its heart: + +```js +while (true) { + handleInput() // reads the keyboard, changes some fields + update() // changes more fields + render() // reads the fields, draws + sleep(150) +} +``` + +Two things are invisible in that loop: **time** and **input**. They are not values anywhere. They happen *to* the program, and the program's state is whatever is left behind in its variables after they have happened. That is why such programs are hard to test (you have to simulate time passing), hard to debug (by the time you see the bug, the state that caused it has been overwritten) and impossible to replay. + +You already have everything you need to do better. In Module 2 you made everything that can happen into data: `Action`. In Modules 3 and 4 you wrote pure functions that compute the next state. In this module you put the two together into one function, `update(state, action)`, and discover that **a game is a fold over its actions**. That one sentence gives you replays, a full history, and trivial tests, for free. + +## Learning objectives + +After this module you can: + +- **implement** `update : (State, Action) → State` by pattern matching on the action; +- **explain** why a whole game is `fold(update, start, actions)` and what that buys (replay, history, determinism); +- **implement** `replay` with a fold and `history` with a scan, and state the relationship between them; +- **predict** the final state of a short game from its start state and action list; +- **describe** how time will become "another action" in the rest of the course. + +## Concepts + +### One function for everything that happens + +So far you have separate functions: `tick(state)` for time passing, `turn(state, dir)` for a key press. A program that uses them must know which one to call. Let us make that decision itself a pure function: + +``` +update : (GameState, Action) -> GameState +``` + +`update` looks at the action and dispatches: a `Tick` means "run a game step" (`tick`), a `Turn(dir)` means "ask to turn" (`turn`). In this module every other action (`TogglePause`, `Restart`, `Frame`) leaves the state unchanged; you will give them meaning in Module 10. + +This is the most important function signature in the course. It has a name in several communities: Redux calls it a **reducer**, Elm calls it `update`, and in automata theory it is the **transition function** of a state machine. Everything the game can ever do is described by this one function and the start state. + +> **From Scala:** dispatching on a sum type is a `match`. In Scala, `case Action.Turn(dir) => ...` extracts the direction; in F#, `| Turn dir -> ...`; in JS, `switch (action.tag)` and then read `action.dir`. For now a final catch-all case (`case _`, `| _ ->`, `default:`) handles the actions that do nothing. In Module 10 you will replace it by explicit cases, so that the Scala and F# compilers warn you if you ever forget one. + +### A game is a fold + +Suppose you have a start state `s0` and the list of everything that happened, in order: `[a1, a2, a3]`. The final state is + +``` +update(update(update(s0, a1), a2), a3) +``` + +You have seen that shape before. It is exactly what a **left fold** computes: + +``` +foldLeft over [a1, a2, a3] starting from s0 with update + + s0 --a1--> s1 --a2--> s2 --a3--> s3 +``` + +`update` has precisely the type a fold wants for its step function: `(accumulator, element) → accumulator`. The accumulator is the game state, and the elements are the actions. Here is the same idea on a different example, a bank account folding over transactions: + +**JavaScript** +```js +const apply = (balance, tx) => (tx.tag === 'Deposit' ? balance + tx.amount : balance - tx.amount) +const txs = [{ tag: 'Deposit', amount: 50 }, { tag: 'Withdraw', amount: 20 }, { tag: 'Deposit', amount: 5 }] +txs.reduce(apply, 100) // 135 +``` + +**Scala** +```scala +enum Tx: + case Deposit(amount: Int) + case Withdraw(amount: Int) +def apply(balance: Int, tx: Tx): Int = tx match + case Tx.Deposit(a) => balance + a + case Tx.Withdraw(a) => balance - a +val txs = List(Tx.Deposit(50), Tx.Withdraw(20), Tx.Deposit(5)) +txs.foldLeft(100)(apply) // 135 +``` + +**F#** +```fsharp +type Tx = + | Deposit of int + | Withdraw of int +let apply balance tx = + match tx with + | Deposit a -> balance + a + | Withdraw a -> balance - a +let txs = [ Deposit 50; Withdraw 20; Deposit 5 ] +List.fold apply 100 txs // 135 +``` + +Note the argument orders, which trip people up: JS `reduce(step, initial)` is called *on* the array; Scala `foldLeft(initial)(step)` is too, with the initial value first; F# `List.fold step initial list` takes the list last (data last, again). In all three, the step function takes the **accumulator first and the element second**, which is why `update` takes the state first. + +A game of Snake is, literally, `actions.reduce(update, start)`. Time and input have become a list. + +### History is a scan + +A fold gives you only the final state. Sometimes you want every state along the way: to draw a replay frame by frame, to step backwards through a game in a debugger, or to find the first state where something went wrong. The operation that keeps every intermediate accumulator is called a **scan**: + +``` +scanLeft over [a1, a2, a3] starting from s0 with update = [s0, s1, s2, s3] +``` + +Notice two facts, which the tests check: the result has **one more element than there are actions** (it includes the start), and its **last element is the fold's result**. On the bank account: + +**JavaScript** +```js +// JS arrays have no scan. You will build one for game states in Exercise 5.3. +// The expected result for the bank example would be: [100, 150, 130, 135] +``` + +**Scala** +```scala +txs.scanLeft(100)(apply) // List(100, 150, 130, 135) +``` + +**F#** +```fsharp +List.scan apply 100 txs // [100; 150; 130; 135] +``` + +> **From Scala:** `scanLeft` is in the standard library of Scala and F#. In JavaScript you can build a scan out of `reduce`, by making the accumulator the *list of states so far* instead of the latest state. Think about what the step function then has to do. (Building a new array on every step with `[...acc, next]` costs O(n) per step and O(n²) overall. For games of a few thousand steps that does not matter; persistent lists, as in Scala and F#, avoid it.) + +### What the fold gives you for free + +Because a game is `fold(update, start, actions)` and `update` is pure: + +- **Replays.** To store a whole game you do not need a video. You need the start state, which is fully determined by the seed (`newGame(seed)`), and the list of actions. Replaying them through `update` reproduces every state exactly, apples included, because the random seed is in the state (Module 4). +- **Deterministic tests.** A test is "this start, these actions, this expected end". No timers, no mocks, no sleeping. The fixture `twelve-actions-eat-one-apple` is a whole little game in one test case: + + ``` + start: the standard 20x20 snake (10,10) (9,10) (8,10), heading Right, apple at (1,9) + actions: Turn Up, Tick, Turn Left, then 9 x Tick + ``` + + Work it out: after `Turn Up` and one `Tick`, the head is at (10,9). After `Turn Left`, nine ticks move it along row 9 from x = 10 to x = 1, where the apple is. On the last tick the snake eats: it grows to four segments `(1,9) (2,9) (3,9) (4,9)`, the score becomes 1, and a new apple is drawn from the seed and lands at (6,5). Every number in the expected state follows from the start state and the list. + +- **Bug reports you can reproduce.** If players' games were logged as seed plus actions, any crash could be replayed exactly on a developer's machine. +- **Splitting and resuming.** Since a fold over a concatenated list is a fold over the first part followed by a fold over the second, `replay(s, a ++ b) == replay(replay(s, a), b)`. You can save a game anywhere (only the state) and continue later. + +Notice what made this possible: no step of the game reads anything except its arguments. A single `Math.random()` or `Date.now()` hidden in `advance` would break every bullet point above. + +### Time is only another action + +Look at the actions again: `Tick` is not "time". It is a *value that says "one step of time has passed"*. The game does not know about milliseconds, timers, or clocks. Something outside the game decides **when** to put a `Tick` into the list. + +That is the plan for the rest of the course: + +``` + clock --every 150 ms--> Tick \ + merge --> actions --> scan(update, newGame(seed)) --> states + keyboard --ArrowUp, w, ...-----> Turn(dir) / +``` + +In Module 8 you will build **streams**: values that describe "a Tick every 150 ms" and "a Turn for every arrow key", that can be merged into one stream of actions, and then *scanned* with `update`, exactly the scan you write today, except that the list of actions arrives over time and never ends. In Module 9 the runtime feeds real clock ticks and key presses in. In Module 10 you will even make the snake speed up without touching the clock: a faster `Frame` action plus a counter in the state (that is what the `frame` field is for). + +The game loop from the top of this module becomes: **the states of the game are a scan of `update` over the stream of actions.** Nothing in the game itself will need to change. + +## Exercises + +### Exercise 5.1 — `update` + +`Tick` runs a game step (`tick` from `spawn`), `Turn(dir)` asks to turn (`turn` from `rules`), every other action returns the state unchanged. + +**JavaScript** +```js +export const update = (state, action) => ... +``` +**Scala** +```scala +def update(state: GameState, action: Action): GameState +``` +**F#** +```fsharp +let update (state: GameState) (action: Action) : GameState +``` + +Fixture: `fixtures/m05-update.json`: `tick-moves-the-snake`, `tick-eating-respawns-apple`, `tick-into-wall-is-game-over`, `tick-while-paused-does-nothing`, `turn-sets-next-dir`, `turn-against-direction-is-ignored`, `turn-while-paused-is-ignored`. + +Watch out: do not re-implement any rule here. `update` only *dispatches*; all the behaviour is already in `tick` and `turn`. Notice that several fixture cases (paused, against direction) pass "for free" because the rules you wrote are total. + +### Exercise 5.2 — `replay` + +Apply all actions in order, starting from `state`, and return the final state. + +**JavaScript** +```js +export const replay = (state, actions) => ... +``` +**Scala** +```scala +def replay(state: GameState, actions: List[Action]): GameState +``` +**F#** +```fsharp +let replay (state: GameState) (actions: Action list) : GameState +``` + +Fixture: `fixtures/m05-replay.json`: `twelve-actions-eat-one-apple`, `no-actions-returns-the-start`. + +### Exercise 5.3 — `history` + +Like `replay`, but return every state: `[state, afterFirst, afterSecond, ...]`. Its length is `actions.length + 1`. + +**JavaScript** +```js +export const history = (state, actions) => ... +``` +**Scala** +```scala +def history(state: GameState, actions: List[Action]): List[GameState] +``` +**F#** +```fsharp +let history (state: GameState) (actions: Action list) : GameState list +``` + +Fixture: `fixtures/m05-history.json`: `no-actions-gives-just-the-start`, `every-intermediate-state-is-kept`. Tests: `history has one more state than there are actions`, `history starts at the start state and ends where replay ends`. + +Watch out (JS): no `.push(`. Each step must produce a new array. The previous state is the last element of the accumulator; `acc.at(-1)` reads it. + +### Run + +- JS: `cd js && node --test "test/m05-*.test.js"` +- Scala: `cd scala && ./test.sh M05` +- F#: `cd fsharp && ./test.sh M05` + +Then run your track's **whole** suite (`./test.sh`): Modules 1–5 and the purity audit should all be green. You now have a complete, pure, deterministic Snake game. It cannot be seen or played yet; that is Modules 6 to 9. + +## Hints + +
Exercise 5.1 (update) — nudge + +Three branches: `Tick`, `Turn`, everything else. Which function handles each? + +
+ +
Exercise 5.1 (update) — stronger hint + +JS: `switch (action.tag)` with `case 'Tick'`, `case 'Turn'` (read `action.dir`) and `default`. Import `tick` from `./spawn.js` and `turn` from `./rules.js`. Scala: `action match` with `case Action.Tick`, `case Action.Turn(dir)`, `case _`, calling `Spawn.tick` and `Rules.turn`. F#: `match action with | Tick -> ... | Turn dir -> ... | _ -> ...`, calling `Spawn.tick` and `Rules.turn`. + +
+ +
Exercise 5.1 (update) — near-solution (try the others first) + +```js +export const update = (state, action) => { + switch (action.tag) { + case 'Tick': return ___ + case 'Turn': return ___ + default: return ___ + } +} +``` +```scala +def update(state: GameState, action: Action): GameState = action match + case Action.Tick => ___ + case Action.Turn(dir) => ___ + case _ => ___ +``` +```fsharp +let update (state: GameState) (action: Action) : GameState = + match action with + | Tick -> ___ + | Turn dir -> ___ + | _ -> ___ +``` + +
+ +
Exercise 5.2 (replay) — nudge + +Re-read "A game is a fold". The bank account example is the same shape. + +
+ +
Exercise 5.2 (replay) — stronger hint + +The step function is `update`, the initial value is `state`, the list is `actions`. JS `reduce`, Scala `foldLeft`, F# `List.fold`. Mind each language's argument order. + +
+ +
Exercise 5.2 (replay) — near-solution (try the others first) + +```js +export const replay = (state, actions) => actions.reduce(___, ___) +``` +```scala +def replay(state: GameState, actions: List[Action]): GameState = actions.foldLeft(___)(___) +``` +```fsharp +let replay (state: GameState) (actions: Action list) : GameState = List.fold ___ ___ ___ +``` + +
+ +
Exercise 5.3 (history) — nudge + +Scala and F#: the standard library already has the operation; see "History is a scan". JS: fold, but with a list of states as the accumulator. + +
+ +
Exercise 5.3 (history) — stronger hint + +Scala `scanLeft`, F# `List.scan`. In JS, start the `reduce` with `[state]`. At each action, compute the next state from the *last* state in the accumulator, and return a new array with that next state appended. + +
+ +
Exercise 5.3 (history) — near-solution (try the others first) + +```js +export const history = (state, actions) => + actions.reduce((states, action) => [...states, update(___, action)], ___) +``` +```scala +def history(state: GameState, actions: List[Action]): List[GameState] = actions.scanLeft(___)(___) +``` +```fsharp +let history (state: GameState) (actions: Action list) : GameState list = List.scan ___ ___ ___ +``` + +
+ +## Check your understanding + +1. Write down the type of the step function that `reduce`/`foldLeft`/`List.fold` expects. Why does that force `update` to take the state as its *first* argument? +2. Predict: `replay(s, [])`, and `history(s, [])`. Now `history(s, [Tick]).length`. +3. Show, using the definition of a fold, that `replay(s, a.concat(b))` equals `replay(replay(s, a), b)`. What practical feature does this law give you? +4. To store a replay of a 10-minute game, what exactly do you need to save? Why is a single number enough to reproduce every apple? +5. Suppose someone "optimises" `spawnApple` by using `Math.random()` when the board is large. Which of the "free" benefits in this module break, and how would you notice in the tests? +6. `update` currently ignores `TogglePause`. Sketch (in words) what it should do in Module 10. Will `replay` or `history` need to change? Why not? +7. Who decides *when* a `Tick` happens? Is that decision part of the game? Where in the big-picture diagram of Module 0 will it live? +8. A teammate suggests making `update` also *draw* the new state, "since it's called every frame anyway". What would you lose? (Think about `history` of a 1000-action game.) + +## In production + +- **Redux** is built on exactly this idea: the application state is `actions.reduce(reducer, initialState)`, conceptually. The [Redux DevTools](https://github.com/reduxjs/redux-devtools) use it for *time-travel debugging*: they keep the action log and let you jump to any past state. +- **Elm** applications are a fold of `update` over messages; the Elm debugger lets you export a session and replay it on another machine, see ["The Perfect Bug Report"](https://elm-lang.org/news/the-perfect-bug-report). +- **Event sourcing** stores a system's history as a log of events and computes the current state by folding over it; see Martin Fowler's [Event Sourcing](https://martinfowler.com/eaaDev/EventSourcing.html). Banks, ledgers and many large systems work this way. +- **Deterministic lockstep** networking in strategy games sends only the players' actions over the network; every machine folds the same actions over the same start state. See Glenn Fiedler's [Deterministic Lockstep](https://gafferongames.com/post/deterministic_lockstep/). +- **Elmish** brings the same model-update architecture to F# ([elmish.github.io](https://elmish.github.io/elmish/)), and **fs2** in Scala has `scan` on streams, which is what you will build by hand in Module 8 ([fs2.io](https://fs2.io/)). + +## Further reading + +- [Graham Hutton, "A tutorial on the universality and expressiveness of fold"](https://www.cs.nott.ac.uk/~pszgmh/fold.pdf) — why so many functions are folds, and how to prove things about them (like the replay law above). +- [Fold (higher-order function) (Wikipedia)](https://en.wikipedia.org/wiki/Fold_%28higher-order_function%29) — left and right folds, and their relation to scans. +- [Array.prototype.reduce (MDN)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/reduce) — JavaScript's fold, including the empty-array pitfall. +- [F# List module](https://fsharp.github.io/fsharp-core-docs/reference/fsharp-collections-listmodule.html) and [Scala List API](https://www.scala-lang.org/api/current/scala/collection/immutable/List.html) — `fold`/`scan` and `foldLeft`/`scanLeft`. +- [The Elm Architecture](https://guide.elm-lang.org/architecture/) — model, update, view: the structure this course is building, from Elm's own guide. +- [Mealy machine (Wikipedia)](https://en.wikipedia.org/wiki/Mealy_machine) — a state machine whose step produces outputs from the current state and input; a preview of how Module 8 will model streams. +- [Conal Elliott and Paul Hudak, "Functional Reactive Animation"](http://conal.net/papers/icfp97/) — the paper that started treating time-varying values and events as first-class values, the idea behind Modules 8 and 9.