Files
functional-programming-snake/course/04-randomness-without-side-effects.md
T
Lucas WintherandClaude Fable 5.2 edd57f0148 feat(course): audit fixes and twelve extensions (Modules 7b, 12, 13)
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>
2026-11-11 12:05:29 +01:00

50 KiB
Raw Blame History

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:

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):

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:

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

// A Random<A> is a plain function: seed => [value, nextSeed]
export const run = (r, seed) => ...

Scala

// A case class wrapping the function; running it is r.run(seed)
final case class Random[A](run: Long => (A, Long))

F#

// 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

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

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#

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

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#

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 flatMaps with no counter in sight:

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?

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 ticks and turns 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

export const nextUint32 = (seed) => ...        // nextUint32(0) -> [1144304738, 1831565813]

Scala

lazy val nextUint32: Random[Long]              // Random.nextUint32.run(0L) == (1144304738L, 1831565813L)

F#

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

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

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)

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

export const intBelow = (n) => ...             // Random<number> in [0, n)

Scala

def intBelow(n: Int): Random[Int]

F#

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

export const freeCells = (state) => ...        // Point[]

Scala

def freeCells(state: GameState): List[Point]

F#

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

export const spawnApple = (state) => ...

Scala

def spawnApple(state: GameState): GameState

F#

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

export const tick = (state) => ...             // advance, then spawnApple
export const newGame = (seed) => ...           // initialState, then spawnApple

Scala

def tick(state: GameState): GameState
def newGame(seed: Long): GameState

F#

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

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

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)

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

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#: 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.

Exercise 4.1 (nextUint32) — near-solution (try the others first)
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, ___]
}
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)
  (___, ___)
}
// 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), ___))
Exercise 4.2 (pure, map, flatMap, run) — nudge

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?

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)
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) => ___
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 => (___, ___))
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.3 (intBelow) — near-solution (try the others first)
export const intBelow = (n) => map(___, (v) => ___)
def intBelow(n: Int): Random[Int] = nextUint32.map(v => ___)
let intBelow (n: int) : Random<int> = nextUint32 |> map (fun v -> ___)
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)
export const freeCells = (state) =>
  range(state.height)
    .flatMap((y) => range(___).map((x) => point(___, ___)))
    .filter((p) => !contains(___, p, pointEquals))
def freeCells(state: GameState): List[Point] =
  (for y <- Util.range(state.height); x <- Util.range(___) yield Point(___, ___))
    .filterNot(p => ___)
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)
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.

Exercise 4.6 (tick, newGame) — near-solution (try the others first)
export const tick = (state) => spawnApple(___(state))
export const newGame = (seed) => ___(initialState(___))
def tick(state: GameState): GameState = spawnApple(___)
def newGame(seed: Long): GameState = ___(Model.initialState(___))
let tick (state: GameState) : GameState = state |> Rules.advance |> ___
let newGame (seed: uint32) : GameState = seed |> ___ |> ___
Exercise 4.7 (State, optional) — nudge

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.

Exercise 4.7 (State, optional) — stronger hint
  • 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.
Exercise 4.7 (State, optional) — near-solution (try the others first)
export const get = (s) => [___, ___]
export const put = (s) => () => [undefined, ___]
export const modify = (f) => (s) => [___, ___]
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 => (___, ___))
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.

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 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.
  • cats (Scala) has a general State 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 and fast-check 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 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