docs(course): replace Scala bridges with general programming in 00–05, README, mentor, spec

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas Winther
2026-10-29 01:57:10 +01:00
co-authored by Claude Opus 5.5
parent 9891bb596d
commit d8828e1b6a
11 changed files with 109 additions and 54 deletions
+14 -9
View File
@@ -52,7 +52,7 @@ 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.
> **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
@@ -205,7 +205,7 @@ Naming differences to note (the full table is in `course/reference.md`):
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
### A pattern with a name: the State monad
Look at the type again: `seed → [A, seed]`. Replace "seed" by any state type `S`:
@@ -215,14 +215,17 @@ 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`).
> **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]`.
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.
Any type with a sensible `pure` and `flatMap` is called a **monad**, and `Random` is one. (`map` then comes for free: `map(r, f)` behaves like `flatMap(r, (a) => pure(f(a)))`.) "Sensible" means three laws hold:
- **left identity:** `flatMap(pure(a), f)` behaves like `f(a)`;
- **right identity:** `flatMap(r, pure)` behaves like `r`;
- **associativity:** `flatMap(flatMap(r, f), g)` behaves like `flatMap(r, a => flatMap(f(a), g))`.
"Behaves like" means "gives the same result when run with any seed". The tests check the two identity laws (`flatMap with pure on the left or right changes nothing`).
Nested `flatMap` and `map` calls get hard to read as they grow, so Scala and F# offer syntax that makes them look like a sequence of steps. In the Scala track this is the **for-comprehension**, and because `Random` has `map` and `flatMap`, it works on `Random` directly. Each line `a <- r` means "run `r` and call its value `a`", and `yield` gives the final value. It is only syntax: the compiler rewrites it into `flatMap` and `map` calls.
**Scala**
```scala
@@ -255,6 +258,8 @@ let twoDice : Random<int> =
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`.