Audit pass: - Frame rule counts frames since the last move, so every step lasts exactly movesEvery(score) frames, also right after a speed-up - clampTime + MAX_GAP_MS: a hidden tab no longer replays the whole gap - Pong Restart keeps held keys - Fact corrections (mulberry32 output coverage, Haskell IO, uniform) - New fixture cases from a mutation analysis Extensions (all three tracks, shared fixtures, course text): - Turn queue (Exercise 10.6), persisted high score (10.7), replay viewer (10.8), play() end-to-end helper (9.5) - Optional State (4.7) and optional Module 7b, stack-safe IO - Module 12, property-based testing with generators, shrinking, invariants and a model-based oracle - Module 13, Conway's Life with a Click event - JS type checking via jsconfig, JSDoc and types.d.ts - Maintainer tools: scripts/mutate.mjs, scripts/typecheck-js.sh, Playwright end-to-end suite (e2e/, scripts/e2e.sh) Docs: docs/contract.md is the canonical contract; specs and build log under docs/superpowers. Verified: JS 728, Scala 689, F# 691 unit tests; stubs and progression M01-M13 and M07b; 176 end-to-end tests on Chromium and WebKit. Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
810 lines
50 KiB
Markdown
810 lines
50 KiB
Markdown
# Module 4 — Randomness without side effects
|
||
|
||
> **Time:** ~4 h (+1 h for the optional Exercise 4.7) · **You'll edit:** JS `js/src/random.js`, `js/src/spawn.js` (optional: `js/src/state.js`) · Scala `scala/src/Random.scala`, `scala/src/Spawn.scala` (optional: `scala/src/State.scala`) · F# `fsharp/src/Random.fs`, `fsharp/src/Spawn.fs` (optional: `fsharp/src/State.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<A> = 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";
|
||
- *(optional)* **generalise** `Random` to `State<S, A>` with `get`, `put` and `modify`, and use it for state other than a seed.
|
||
|
||
## 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 everyday programming:** the random generators you have used, such as Java's `java.util.Random`, C#'s `System.Random`, Python's `random` module or `Math.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 **not** suitable for anything security-related: given one output, the next ones can be predicted.
|
||
|
||
One more honest detail. The counter visits every 32-bit value once, but the scramble is not one-to-one: different counters can give the same output. Run the generator through its whole period and count (it takes a minute in any of the three languages): only 1 893 145 848 of the 4 294 967 296 possible values ever come out, about 44 %, and those that do come out are not equally frequent. For placing apples this is irrelevant. For a simulation whose results you intend to publish, pick a generator whose output step is a bijection, such as SplitMix or PCG (see "Further reading").
|
||
|
||
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 on .NET, but not in the browser (see below) |
|
||
| 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.
|
||
|
||
> **F# in the browser.** The F# tests run on .NET, where `uint32` `+` and `*` wrap around at 2^32 exactly as the table says. The browser page, however, is compiled to JavaScript by Fable (Module 9), and Fable turns `uint32` `+` and `*` into plain JavaScript `+` and `*`. Those do not wrap, and a product of two large 32-bit numbers is not even exact in a JavaScript `number`. So a `nextUint32` written with plain `*` and `+` passes every test on .NET and then places the apples differently (or on cells that do not exist) in the browser. The tests cannot see this, because they never run the Fable output. The fix is to do each `*` and `+` through two tiny helpers that go up to `uint64` and back: `let mul (a: uint32) (b: uint32) = uint32 (uint64 a * uint64 b)`, and `add` the same way. The product of two `uint32` values always fits in a `uint64`, and converting back to `uint32` keeps the low 32 bits, so the helpers are exact on .NET and in the browser. (`^^^`, `|||` and `>>>` behave the same on both and need no helper.) The doc comment in `fsharp/src/Random.fs` says the same.
|
||
|
||
### 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<A> = seed -> [A, seed]
|
||
```
|
||
|
||
A `Random<A>` is a *description* of how to produce an `A` from a seed: a recipe, not a number. `nextUint32` is a `Random<uint32>`. `d6` is a `Random<number>`. `twoDice` is a `Random<number>`. Nothing happens until you **run** it with a seed: `run(r, seed)`.
|
||
|
||
**JavaScript**
|
||
```js
|
||
// A Random<A> 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<B> where r : Random<A>, 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<B> where r : Random<A>, f : A -> Random<B>
|
||
|
||
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<A> 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<int> = Random.nextUint32 |> Random.map (fun v -> 1 + int (v % 6u))
|
||
let twoDice : Random<int> = 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.
|
||
|
||
### A pattern with a name: the State monad
|
||
|
||
Look at the type again: `seed → [A, seed]`. Replace "seed" by any state type `S`:
|
||
|
||
```
|
||
State<S, A> = 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<A>` is `State<Seed, A>`. 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 everyday programming:** if you have chained JavaScript promises, you have used this pattern. In `fetch(url).then((res) => res.json()).then((data) => show(data))`, each `then` takes a callback that receives the previous step's result and may return the *next* asynchronous step; the promise does the plumbing (waiting, passing the result on) for you. `then` with such a callback is `flatMap` for promises. Arrays have a `flatMap` too, where each element produces an array and the results are joined: `[1, 2].flatMap((x) => [x, x * 10])` is `[1, 10, 2, 20]`.
|
||
|
||
Any type with a sensible `pure` and `flatMap` is called a **monad**, and `Random` is one. (`map` then comes for free: `map(r, f)` behaves like `flatMap(r, (a) => pure(f(a)))`.) "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`).
|
||
|
||
Nested `flatMap` and `map` calls get hard to read as they grow, so Scala and F# offer syntax that makes them look like a sequence of steps. In the Scala track this is the **for-comprehension**, and because `Random` has `map` and `flatMap`, it works on `Random` directly. Each line `a <- r` means "run `r` and call its value `a`", and `yield` gives the final value. It 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<int> =
|
||
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.
|
||
|
||
> **From everyday programming:** `async`/`await` in JavaScript, C# or Python is the same idea for promises and tasks. `const res = await fetch(url)` reads like an ordinary assignment, but behind the scenes it is a `then` whose callback is the rest of the function. A for-comprehension or a computation expression gives `Random` (or any monad) that same "looks like sequential code" syntax.
|
||
|
||
### Going further: `State` for any kind of state (optional)
|
||
|
||
This section and Exercise 4.7 are optional: nothing in the game depends on them. If you want to get to the apples, skip to "Drawing a number below `n`" and come back after Exercise 4.6.
|
||
|
||
The previous section claimed that `Random<A>` is `State<Seed, A>`. A claim like that is worth checking, and the way to check it is to write `State` itself and then rebuild the dice with it.
|
||
|
||
**Why bother, if `Random` already works?** Because the seed is not the only thing a program threads through by hand. A counter that hands out fresh labels (`L0`, `L1`, `L2`, ...) has the same problem as the seed: each step needs the number the previous step left behind, and reusing an old number is a bug that nothing catches. So does a parser that reads a string piece by piece, where each step returns a value and "the rest of the input". Every one of these is `S -> (A, S)` for some `S`. Write the plumbing once, for every `S`, and each of them gets `map` and `flatMap` for free.
|
||
|
||
The code of `pure`, `map`, `flatMap` and `run` does not change at all: look at your `random.js` and rename "seed" to "s". What is new are three small building blocks that talk about the state itself. `Random` never needed them, because only `nextUint32` ever touched the seed directly; for a general state, you need a way to read it and to change it:
|
||
|
||
```
|
||
get : State<S, S> s gives [s, s] the value is the state; the state is unchanged
|
||
put(s2) : State<S, unit> s gives [nothing, s2] throw the old state away, store s2
|
||
modify(f) : State<S, unit> s gives [nothing, f(s)] store f of the old state
|
||
```
|
||
|
||
("unit" is the type with a single value that carries no information: the value a step gives when only its effect on the state matters. JavaScript uses `undefined` for it, Scala `()` of type `Unit`, F# `()` of type `unit`.)
|
||
|
||
With these, the label counter is one line, and three labels in a row are a chain of `flatMap`s with no counter in sight:
|
||
|
||
```js
|
||
const fresh = flatMap(get, (n) => map(put(n + 1), () => `L${n}`)) // read n, store n + 1, give "Ln"
|
||
const three = flatMap(fresh, (a) => flatMap(fresh, (b) => map(fresh, (c) => [a, b, c])))
|
||
run(three, 0) // [['L0', 'L1', 'L2'], 3]
|
||
```
|
||
|
||
And a single draw of the generator can be written with nothing but `get` and `put`: read the seed, compute the value and the next seed, store the next seed. Build the dice of the previous section on top of that draw, and you get exactly the values and seeds that `Random` gives. That is the precise meaning of "`Random` is `State` with the seed as `S`", and it is what the last test of Exercise 4.7 checks.
|
||
|
||
> **From everyday programming:** the usual way to hand out fresh ids is a field and an increment: `this.nextId++` in a class, or a module-level counter. It works until two parts of the program share the counter by accident, or a test needs it to start at 0 again. `State` makes the counter an explicit input and output of every step, so it can never be shared by accident and every run starts where you say. Redux reducers are the same idea in a different shape: `(state, action) => newState` returns the next state instead of changing the current one.
|
||
|
||
### 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`: even for a perfectly uniform `v`, small results would be a tiny bit more likely. For `n` up to 400 that effect is below one in ten million, much smaller than the unevenness of mulberry32 itself, and both are far below anything a player could notice. Libraries that need exact uniformity reject and redraw, see question 9; 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`?
|
||
|
||
<details><summary>Think about it first, then open the explanation</summary>
|
||
|
||
- **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.
|
||
|
||
</details>
|
||
|
||
### 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
|
||
|
||
> **About test names.** The test titles quoted under "Test"/"Tests" are the JavaScript ones; the JS runner prints them with the module in front (`m04 intBelow always lands in [0, n)`). The Scala and F# suites test the same things one-to-one with the same titles, except that F# says `bind` where JS and Scala say `flatMap` (`bind threads the seed through both steps`). Failing tests are reported by name when you run the module's tests; to read all titles, open `scala/test/M04Random.test.scala`, `M04Spawn.test.scala` and `M04State.test.scala`, or `fsharp/tests/M04Random.fs`, `M04Spawn.fs` and `M04State.fs`. Fixture-based tests have the same case names in every track (JS `m04 nextUint32: seed-0-step-1`, Scala/F# `nextUint32: seed-0-step-1`).
|
||
|
||
### 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<uint32> // 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. In F#, route every `*` and `+` through the `uint64` helpers (see "F# in the browser"), or the game will pass the tests and still misbehave in the browser.
|
||
|
||
### Exercise 4.2 — `pure`, `map`, `flatMap`, `run`
|
||
|
||
**JavaScript**
|
||
```js
|
||
export const pure = (a) => ... // Random<A>
|
||
export const map = (r, f) => ... // Random<A>, (A -> B) -> Random<B>
|
||
export const flatMap = (r, f) => ... // Random<A>, (A -> Random<B>) -> Random<B>
|
||
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<number> in [0, n)
|
||
```
|
||
**Scala**
|
||
```scala
|
||
def intBelow(n: Int): Random[Int]
|
||
```
|
||
**F#**
|
||
```fsharp
|
||
let intBelow (n: int) : Random<int>
|
||
```
|
||
|
||
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`.
|
||
|
||
### Exercise 4.7 (optional) — `State`
|
||
|
||
Read "Going further: `State` for any kind of state" first. The file is new: JS `js/src/state.js`, Scala `scala/src/State.scala`, F# `fsharp/src/State.fs`. Nothing else in the course imports it.
|
||
|
||
**JavaScript**
|
||
```js
|
||
export const pure = (a) => ... // State<S, A>: s => [a, s]
|
||
export const map = (st, f) => ... // State<S, A>, (A -> B) -> State<S, B>
|
||
export const flatMap = (st, f) => ... // State<S, A>, (A -> State<S, B>) -> State<S, B>
|
||
export const get = ... // State<S, S> (a value, like nextUint32)
|
||
export const put = (s) => ... // State<S, undefined>
|
||
export const modify = (f) => ... // State<S, undefined>, f : S -> S
|
||
export const run = (st, s) => ... // [A, S]
|
||
```
|
||
**Scala**
|
||
```scala
|
||
final case class State[S, A](run: S => (A, S)):
|
||
def map[B](f: A => B): State[S, B]
|
||
def flatMap[B](f: A => State[S, B]): State[S, B]
|
||
object State:
|
||
def pure[S, A](a: A): State[S, A]
|
||
def get[S]: State[S, S]
|
||
def put[S](s: S): State[S, Unit]
|
||
def modify[S](f: S => S): State[S, Unit]
|
||
```
|
||
**F#** (in `module Snake.State`, which also defines the type)
|
||
```fsharp
|
||
type State<'s, 'a> = State of ('s -> 'a * 's)
|
||
let run (s: 's) (State f: State<'s, 'a>) : 'a * 's
|
||
let pure (a: 'a) : State<'s, 'a>
|
||
let map (f: 'a -> 'b) (st: State<'s, 'a>) : State<'s, 'b>
|
||
let bind (f: 'a -> State<'s, 'b>) (st: State<'s, 'a>) : State<'s, 'b>
|
||
let get () : State<'s, 's> // a function of unit, so it can be generic
|
||
let put (s: 's) : State<'s, unit>
|
||
let modify (f: 's -> 's) : State<'s, unit>
|
||
```
|
||
|
||
Scala's `get[S]` needs the state type at the call site when nothing else fixes it: `State.get[Int]`. In the tests, the Random comparison turns a `Random` step into a `State` by hand: Scala `State[Long, Long](seed => Random.nextUint32.run(seed))`, F# `State(fun seed -> Random.run seed Random.nextUint32)`. In JS there is nothing to turn: a `Random` already *is* a function `seed => [value, seed]`.
|
||
|
||
Tests (in `m04-state.test.js` / `M04State`): `State pure gives its value and leaves the state alone`, `State map applies f to the value and keeps the state from the underlying step`, `State flatMap threads the state through both steps`, `State get gives the state as the value and leaves it alone`, `State put replaces the state and gives undefined` (Scala/F#: "gives unit"), `State modify applies f to the state and gives undefined` (Scala/F#: "gives unit"), the three laws `State law: left identity, ...`, `State law: right identity, ...`, `State law: associativity, ...`, `State a counter hands out fresh labels in order`, and `State Random is State with the seed as S: the dice give the same values and seeds`.
|
||
|
||
Watch out: like `Random`, every function here returns a State (a function, or a value wrapping one) and runs nothing. `put(s)` ignores the incoming state, but it must still *be* a function of it.
|
||
|
||
### Run
|
||
|
||
- JS: `cd js && node --test "test/m04-*.test.js"` (runs `m04-random`, `m04-spawn` and the optional `m04-state`; until you do Exercise 4.7, the `m04 State ...` tests fail; if you decide to skip it, copy the finished file instead, `cp js/solutions/state.js js/src/state.js`, and the same for `State.scala` / `State.fs`, so that your suite stays green)
|
||
- Scala: `cd scala && ./test.sh M04`
|
||
- F#: `cd fsharp && ./test.sh M04`
|
||
|
||
## Hints
|
||
|
||
<details><summary>Exercise 4.1 (nextUint32) — nudge</summary>
|
||
|
||
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.
|
||
|
||
</details>
|
||
|
||
<details><summary>Exercise 4.1 (nextUint32) — stronger hint</summary>
|
||
|
||
- **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#:** first write the two helpers `mul` and `add` from "F# in the browser" above, and use them for every `*` and `+` (on .NET plain `uint32` arithmetic would wrap too, but the browser build would break). Then `let s = add seed 0x6D2B79F5u`, and `^^^`, `|||`, `>>>` 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.
|
||
|
||
</details>
|
||
|
||
<details><summary>Exercise 4.1 (nextUint32) — near-solution (try the others first)</summary>
|
||
|
||
```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
|
||
// wrap-around * and + that also work in the browser (Fable): via uint64 and back
|
||
let private mul (a: uint32) (b: uint32) : uint32 = uint32 (uint64 a * uint64 b)
|
||
let private add (a: uint32) (b: uint32) : uint32 = ___
|
||
|
||
let nextUint32 : Random<uint32> =
|
||
Random(fun seed ->
|
||
let s = add seed 0x6D2B79F5u
|
||
let t1 = mul (s ^^^ (s >>> 15)) ___
|
||
let t2 = t1 ^^^ add t1 (mul ___ ___)
|
||
(t2 ^^^ (t2 >>> 14), ___))
|
||
```
|
||
|
||
</details>
|
||
|
||
<details><summary>Exercise 4.2 (pure, map, flatMap, run) — nudge</summary>
|
||
|
||
Follow the types. `map(r, f)` must return a `Random<B>`, 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?
|
||
|
||
</details>
|
||
|
||
<details><summary>Exercise 4.2 (pure, map, flatMap, run) — stronger hint</summary>
|
||
|
||
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 -> ...)`.
|
||
|
||
</details>
|
||
|
||
<details><summary>Exercise 4.2 (pure, map, flatMap, run) — near-solution (try the others first)</summary>
|
||
|
||
```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 ___ ___)
|
||
```
|
||
|
||
</details>
|
||
|
||
<details><summary>Exercise 4.3 (intBelow) — nudge</summary>
|
||
|
||
It is "the value of `nextUint32`, transformed". Which of your new functions transforms a value?
|
||
|
||
</details>
|
||
|
||
<details><summary>Exercise 4.3 (intBelow) — stronger hint</summary>
|
||
|
||
`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)`.
|
||
|
||
</details>
|
||
|
||
<details><summary>Exercise 4.3 (intBelow) — near-solution (try the others first)</summary>
|
||
|
||
```js
|
||
export const intBelow = (n) => map(___, (v) => ___)
|
||
```
|
||
```scala
|
||
def intBelow(n: Int): Random[Int] = nextUint32.map(v => ___)
|
||
```
|
||
```fsharp
|
||
let intBelow (n: int) : Random<int> = nextUint32 |> map (fun v -> ___)
|
||
```
|
||
|
||
</details>
|
||
|
||
<details><summary>Exercise 4.4 (freeCells) — nudge</summary>
|
||
|
||
First build *all* cells in row-major order, then keep those not in the snake.
|
||
|
||
</details>
|
||
|
||
<details><summary>Exercise 4.4 (freeCells) — stronger hint</summary>
|
||
|
||
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".
|
||
|
||
</details>
|
||
|
||
<details><summary>Exercise 4.4 (freeCells) — near-solution (try the others first)</summary>
|
||
|
||
```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 (___))
|
||
```
|
||
|
||
</details>
|
||
|
||
<details><summary>Exercise 4.5 (spawnApple) — nudge</summary>
|
||
|
||
Three outcomes: unchanged, `Won`, or a new apple with a new seed. Write the guards first, in that order, then the draw.
|
||
|
||
</details>
|
||
|
||
<details><summary>Exercise 4.5 (spawnApple) — stronger hint</summary>
|
||
|
||
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.
|
||
|
||
</details>
|
||
|
||
<details><summary>Exercise 4.5 (spawnApple) — near-solution (try the others first)</summary>
|
||
|
||
```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.
|
||
|
||
</details>
|
||
|
||
<details><summary>Exercise 4.6 (tick, newGame) — nudge</summary>
|
||
|
||
Each is one line: a function applied to the result of another.
|
||
|
||
</details>
|
||
|
||
<details><summary>Exercise 4.6 (tick, newGame) — stronger hint</summary>
|
||
|
||
`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`.
|
||
|
||
</details>
|
||
|
||
<details><summary>Exercise 4.6 (tick, newGame) — near-solution (try the others first)</summary>
|
||
|
||
```js
|
||
export const tick = (state) => spawnApple(___(state))
|
||
export const newGame = (seed) => ___(initialState(___))
|
||
```
|
||
```scala
|
||
def tick(state: GameState): GameState = spawnApple(___)
|
||
def newGame(seed: Long): GameState = ___(Model.initialState(___))
|
||
```
|
||
```fsharp
|
||
let tick (state: GameState) : GameState = state |> Rules.advance |> ___
|
||
let newGame (seed: uint32) : GameState = seed |> ___ |> ___
|
||
```
|
||
|
||
</details>
|
||
|
||
<details><summary>Exercise 4.7 (State, optional) — nudge</summary>
|
||
|
||
Four of the seven functions are already written: they are in your `random.js` (`Random.scala`, `Random.fs`) under the same names, with "seed" where you now write "s". For the other three, write down what each must return when run with a state `s`, as a pair, and the code follows from the pair.
|
||
|
||
</details>
|
||
|
||
<details><summary>Exercise 4.7 (State, optional) — stronger hint</summary>
|
||
|
||
- `get` run with `s` gives `[s, s]`. In JS, `get` is itself that function (not a function that returns one), just as `nextUint32` is a `Random`.
|
||
- `put(s2)` run with any `s` gives `[undefined, s2]`: it ignores `s`, but it is still a function of it.
|
||
- `modify(f)` run with `s` gives `[undefined, f(s)]`. (You can also write it with `flatMap`, `get` and `put`: read, then store `f` of what you read.)
|
||
- Scala: `get`, `put` and `modify` live in the companion object and wrap their function with `State(...)`; in `put` and `modify` the value is `()`. F#: same shape with `State(fun s -> ...)`; `get` takes `()` so that it can stay generic.
|
||
|
||
</details>
|
||
|
||
<details><summary>Exercise 4.7 (State, optional) — near-solution (try the others first)</summary>
|
||
|
||
```js
|
||
export const get = (s) => [___, ___]
|
||
export const put = (s) => () => [undefined, ___]
|
||
export const modify = (f) => (s) => [___, ___]
|
||
```
|
||
```scala
|
||
def get[S]: State[S, S] = State(s => (___, ___))
|
||
def put[S](s: S): State[S, Unit] = State(_ => ((), ___))
|
||
def modify[S](f: S => S): State[S, Unit] = State(s => (___, ___))
|
||
```
|
||
```fsharp
|
||
let get () : State<'s, 's> = State(fun s -> (___, ___))
|
||
let put (s: 's) : State<'s, unit> = State(fun _ -> ((), ___))
|
||
let modify (f: 's -> 's) : State<'s, unit> = State(fun s -> (___, ___))
|
||
```
|
||
`pure`, `map`, `flatMap`/`bind` and `run` are the near-solution of Exercise 4.2 with `Random` renamed to `State` and the seed renamed to `s`.
|
||
|
||
</details>
|
||
|
||
## 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<A>` and `State<S, A>`: 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".)
|
||
9. **Challenge: a draw without bias.** `intBelow(n)` is very slightly biased (see "Drawing a number below `n`"). The standard cure is *rejection*: let `limit` be the largest multiple of `n` that is at most 2^32; draw a value; if it is below `limit`, use `v % n`; otherwise draw again. Write `intBelowUniform(n)` on paper with the functions of this module. Why do you need `flatMap` here, where `intBelow` only needed `map`? What does that tell you about the difference between the two? (Do not put it in the course code: the fixtures expect the simple version.)
|
||
10. Two generators that start from seeds `s` and `s + 0x6D2B79F5` produce the same sequence, shifted by one draw. Explain why from the structure of mulberry32. Why does that make "seed the second player's generator with `seed + 1`" a reasonable idea and "seed it with the first player's *next seed*" a bad one?
|
||
11. *(Exercise 4.7)* **A label counter with `State`.** A game log numbers its lines: `1: ate an apple`, `2: turned left`, ... Write `label(text)` as a `State<number, string>` (Scala `State[Int, String]`, F# `State<int, string>`) using `get` and `put` (or `modify`), so that running the chain of `label('ate an apple')` and then `label('turned left')` from `1` gives both lines and leaves the counter at `3`. What is `S` here, and what bug can no longer happen that `this.line++` would allow? (When you reach Module 11, try the same with the Pong serve: write the choice of `dy` as a `State` over the seed, with `get` and `put`, and compare it with the `Random` version.)
|
||
12. *(Exercise 4.7)* In JavaScript, `Random` and `State` are the same plain functions, so `random.js` could import `pure`, `map` and `flatMap` from `state.js`. In Scala and F#, `Random[A]` and `State[Long, A]` are different types, and you had to convert one into the other by hand in the tests. What does the separate `Random` type buy you there, and what does it cost? Which of `get`, `put`, `modify` would you *not* want to offer on `Random`, and why? (Think about what `put(0)` would do to a game's apples.)
|
||
|
||
## In production
|
||
|
||
- **Haskell's** [`random`](https://hackage.haskell.org/package/random) package uses pure generators: `uniform :: g -> (a, g)` (leaving out the type-class constraints), 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.
|
||
- [Philip Wadler, "Monads for functional programming"](https://homepages.inf.ed.ac.uk/wadler/papers/marktoberdorf/baastad.pdf) — the classic tutorial paper; its section 2 threads a state through an evaluator by hand and then removes the plumbing, exactly the path of this module.
|
||
- [Guy Steele, Doug Lea and Christine Flood, "Fast Splittable Pseudorandom Number Generators"](https://gee.cs.oswego.edu/dl/papers/oopsla14.pdf) — the design behind "a counter plus a mixing function" (SplitMix), the family mulberry32 belongs to.
|
||
- [Daniel Lemire, "Fast Random Integer Generation in an Interval"](https://arxiv.org/abs/1805.10941) — how libraries draw a number below `n` without the modulo bias, and without a division.
|