Files
functional-programming-snake/course/04-randomness-without-side-effects.md
T

38 KiB
Raw Blame History

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:

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

Drawing a number below n

To pick one of n free cells, you need a number in [0, n). intBelow(n) is nextUint32 with its value taken modulo n. That is a map.

(Strictly, v % n is slightly biased when 2^32 is not a multiple of n: small results are a tiny bit more likely. For n up to 400 the bias is below one in ten million, far below anything a player could notice. Libraries that need exact uniformity reject and redraw; we keep it simple and identical across tracks.)

Spawning an apple

Now apply it to the game. spawnApple(state):

  1. If there already is an apple, or the game is not Playing, return state unchanged.
  2. List the free cells: every cell of the board that is not part of the snake, in row-major order (row y = 0 from left to right, then row y = 1, ...).
  3. Draw an index i below the number of free cells, from state.seed.
  4. Return the state with apple set to the i-th free cell and seed set to the seed the draw left behind.

Why does the order of free cells matter? Because the index i must mean the same cell in every track. "Row-major" is part of the contract; the fixture free-cells-row-major-order checks it.

A worked example from the tests. A 3 x 2 board, a one-cell snake at (0,0) about to eat the apple at (1,0), seed 0:

   before tick           after advance           after spawnApple
   x: 0 1 2              x: 0 1 2                x: 0 1 2
y=0   H A .           y=0   b H .             y=0   b H .
y=1   . . .           y=1   . . .             y=1   . A .

After advance, the snake is (1,0) (0,0) and there is no apple. Free cells, row-major: (2,0), (0,1), (1,1), (2,1), four of them. From seed 0, nextUint32 gives 1144304738, and 1144304738 % 4 = 2, so the apple goes to the cell at index 2: (1,1). The new seed is 1831565813. That is the test tick: eating the apple spawns a new one in the same step.

Puzzle: the board is full

What happens when the snake fills the entire board? There are no free cells. Before reading on, work out what your spawnApple would do when the list of free cells is empty, in your language. What is v % 0?

Think about it first, then open the explanation
  • JavaScript: v % 0 is NaN, cells[NaN] is undefined, so apple becomes undefined. No crash, only a corrupt state that will cause confusing errors later.
  • Scala: v % 0 on integers throws ArithmeticException: / by zero. The game crashes at its moment of triumph.
  • F#: v % 0u throws DivideByZeroException.

None of these is a rule of the game. A full board means the player has won. So spawnApple handles it explicitly: if there are no free cells, the status becomes Won (no apple, seed unchanged). This is the fixture spawn-on-full-board: a 3 x 2 board completely covered by a six-cell snake.

This is a pattern worth remembering: when an operation has no valid answer for some input (here: "pick one of zero cells"), find out what that input means in the domain, and make it an explicit, total case.

Putting the pieces together: tick and newGame

Two small functions finish the module:

  • tick(state) is one complete game step: advance, then spawnApple. If the snake ate, the apple is gone after advance, and spawnApple places a new one. If it did not eat, there still is an apple, so spawnApple leaves the state (and the seed!) unchanged. Test: tick leaves the seed alone when no apple needs spawning.
  • newGame(seed) is initialState(seed) with its first apple already placed.

Both are compositions of functions you already have. You wrote compose in Module 1; in Scala, andThen; in F#, >>.

And now the payoff: newGame(42) is the same game every time you call it. Any sequence of 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 and M04Spawn.test.scala, or fsharp/tests/M04Random.fs and M04Spawn.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.

Run

  • JS: cd js && node --test "test/m04-*.test.js" (runs both m04-random and m04-spawn)
  • Scala: cd scala && ./test.sh M04
  • F#: cd fsharp && ./test.sh M04

Hints

Exercise 4.1 (nextUint32) — nudge

Translate the pseudo-code line by line, then fix the integer behaviour. Start with seed 0: the new seed must be 1831565813, which is 0 + 0x6D2B79F5. Get that right first, then the value.

Exercise 4.1 (nextUint32) — stronger hint
  • JS: (seed + 0x6D2B79F5) >>> 0 wraps the sum into an unsigned 32-bit number. Math.imul(a, b) multiplies like a 32-bit machine. ^, |, >>> work on 32-bit integers; ^ and | give signed results, so finish the value with >>> 0. Use const for each intermediate (you cannot reassign t; call them t1, t2).
  • Scala: val s = seed.toInt + 0x6D2B79F5 (Int addition wraps). Then the Int formula with *, ^, |, >>>. Return (value.toLong & 0xFFFFFFFFL, s.toLong & 0xFFFFFFFFL). Remember to wrap the function: Random(seed => ...).
  • F#: 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 |> ___ |> ___

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 package uses pure generators: uniformR :: g -> (a, g), 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