647 lines
33 KiB
Markdown
647 lines
33 KiB
Markdown
# 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<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".
|
||
|
||
## 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<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.
|
||
|
||
### 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 -> (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 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<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.
|
||
|
||
### 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`?
|
||
|
||
<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
|
||
|
||
### 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.
|
||
|
||
### 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`.
|
||
|
||
### 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
|
||
|
||
<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#:** `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.
|
||
|
||
</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
|
||
let nextUint32 : Random<uint32> =
|
||
Random(fun seed ->
|
||
let s = seed + 0x6D2B79F5u
|
||
let t1 = (s ^^^ (s >>> 15)) * ___
|
||
let t2 = t1 ^^^ (t1 + ___ * ___)
|
||
(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.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>
|
||
|
||
## 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".)
|
||
|
||
## 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.
|