Files
functional-programming-snake/course/07b-stack-safe-io.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

473 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Module 7b — Stack-safe IO (optional)
> **Time:** ~3 h · **You'll edit:** JS `js/src/safeio.js` · Scala `scala/src/SafeIO.scala` · F# `fsharp/src/SafeIO.fs`
> **Tests:** JS `cd js && node --test "test/m07b-*.test.js"` · Scala `cd scala && ./test.sh M07b` · F# `cd fsharp && ./test.sh M07b`
> **Prerequisites:** all of Module 7, especially sections 8 (`sequence`), 10 (the monad laws) and 12 (stack safety). No later module needs this one: you can do it straight after Module 7, after the whole course, or never.
## Why this module
Module 7 ended with a rule: **build `sequence` with a right fold.** Fold from the left and every Module 7 test still passes, but the 100 000-draw test of Module 9 dies with a stack overflow. Section 12 explained why in two sentences and moved on.
A rule like that is fragile. Nothing in the type says "nest me to the right", and left nesting is what you get naturally whenever you build a program step by step, adding to "the program so far":
```js
// A perfectly reasonable way to build a program: start from nothing, add one step per item.
const drawAll = (shapes) => shapes.reduce((soFar, s) => flatMap(soFar, () => draw(s)), pure(undefined))
```
Here is what that costs with Module 7's `IO`, measured with Node on the machine this module was written on, with a `while` loop as the interpreter (the loop of Module 9's `runIO`):
| Draws, left-nested | Time | Draws, right-nested | Time |
|---|---|---|---|
| 1 000 | 11 ms | 100 000 | 38 ms |
| 5 000 | 223 ms | | |
| 10 000 | 1 082 ms | | |
| 100 000 | stack overflow | | |
Two different problems hide in that table. Going from 5 000 to 10 000 draws multiplies the time by almost five: the work grows with the *square* of the length. And at 100 000 the stack runs out, even though the interpreter is a loop. On top of that, Module 7's `trace` is recursive, one level per operation, so it overflows at around 10 000 operations whichever way the program is nested.
This module removes both problems by changing the representation, not the rule. You will add a third constructor, `FlatMap`, so that "and then" becomes a piece of data; write the one rewrite rule that makes left nesting harmless; and build an interpreter that never grows the stack, out of tiny steps. At the end, 100 000 left-nested `flatMap`s run in a fraction of a second, and the programs behave exactly as they did under Module 7's `IO`, which the tests check.
## Learning objectives
By the end of this module you can:
- **explain** why a left-nested chain of `flatMap`s is both deep (stack) and quadratic (time) with the `IO` of Module 7;
- **implement** an `IO` whose `flatMap` only builds a `FlatMap` node;
- **state** the re-association rule, draw it as a picture, and **justify** it with the associativity law;
- **implement** `step`, a single rewrite with a constant amount of work, and **explain** how an interpreter built from such steps (a *trampoline*) keeps the stack flat;
- **explain** how a loop can be written without a loop in a language that does not eliminate tail calls, and why the ops must not be collected with `[...ops, op]`;
- **describe** what production effect libraries add on top of this.
## Concepts
### 1. Where the stack goes
Take Module 7's `flatMap` again:
```js
export const flatMap = (io, f) =>
io.tag === 'Pure'
? f(io.value)
: { tag: 'Effect', op: io.op, k: (result) => flatMap(io.k(result), f) }
```
Build a left-nested chain of three steps, `flatMap(flatMap(flatMap(e, f1), f2), f3)`, where `e` is one `Effect(op1, k)`. Work from the inside out:
```
flatMap(e, f1) = Effect(op1, k1) where k1(r) = flatMap(k(r), f1)
flatMap(that, f2) = Effect(op1, k2) where k2(r) = flatMap(k1(r), f2)
flatMap(that, f3) = Effect(op1, k3) where k3(r) = flatMap(k2(r), f3)
```
The outermost program still starts with `op1`, but its continuation is a **tower**: `k3` calls `k2`, which calls `k1`, which calls `k`. When the interpreter answers `op1` and calls `k3(r)`, all of those calls are on the stack at the same time before anything comes back. With `n` steps, the tower is `n` calls high. A `while` loop in the interpreter does not help, because the depth is *inside one call of a continuation*, not between the loop's iterations.
With right nesting, `flatMap(e, (r) => flatMap(e2, (r2) => …))`, the inner `flatMap` sits inside a function that has not been called yet. Each continuation does one small thing and returns the next `Effect`. Nothing piles up.
### 2. Why it is also quadratic
Follow the tower one level further. When `k3(r)` finally returns, what does it return? `k(r)` gave the rest of `e` (say `Pure(r)`); `flatMap(Pure(r), f1)` gave `f1(r)`, which is the program's second step, say `Effect(op2, …)`; then `flatMap(…, f2)` wrapped that in a new continuation, and `flatMap(…, f3)` wrapped it again. So the second operation comes out with a tower of `n − 1` levels, the third with `n − 2`, and so on. The total number of calls is about `n + (n − 1) + … + 1`, which is `n²/2`. That is the answer to the challenge question 9 of Module 7, and the reason the table above grows by a factor of four to five each time `n` doubles.
> **From everyday programming:** this is the same trap as appending to the end of a singly linked list by walking from the head every time: each append is cheap on its own, but the walk gets longer with every element, and building a list of `n` elements costs `n²/2` steps. The classic fixes are the ones this module uses: keep a better structure (a pointer to the end, or here a node that remembers "and then"), or reorganise the work so that you only ever touch the front. String building has the same shape: `s = s + piece` in a loop copies everything so far each time, which is why languages offer string builders and `join`.
### 3. The fix: make "and then" a piece of data
The trouble starts in the `Effect` case of `flatMap`, which immediately builds a new continuation around the old one. What if `flatMap` did nothing clever at all, and just *remembered* what it was asked to do?
```
FlatMap(io, f) "run io; then, with its result, continue with f(result)"
```
A program is now one of three things:
- **`Pure(value)`**: done, the result is `value`;
- **`Effect(op, k)`**: perform `op`, give its answer to `k`, continue with what `k` returns;
- **`FlatMap(io, f)`**: run `io`; when it produces a value `x`, continue with `f(x)`.
`flatMap(io, f)` is now one line: build `FlatMap(io, f)`. It costs the same for any `io`, and it never calls `f`, not even when `io` is a `Pure`. That also quietly removes the eagerness of Module 7 section 9: `flatMap(pure(1), () => { throw … })` no longer throws while building.
All the cleverness moves into the interpreter, which meets `FlatMap` nodes and has to decide what to do with them. That is the subject of the next two sections. First, the type in the three languages.
**JavaScript**
```js
// SafeIO<A> =
// { tag: 'Pure', value } value : A
// { tag: 'Effect', op, k } op : Op, k : (answer) => SafeIO<A>
// { tag: 'FlatMap', io, f } io : SafeIO<X>, f : (x: X) => SafeIO<A> for some type X
```
**Scala**
```scala
// scala/src/SafeIO.scala. Op, OpResult and Traced are the ones from IO.scala.
enum SafeIO[+A]:
case Pure(a: A)
case Effect(op: Op, k: OpResult => SafeIO[A])
case FlatMap[X, B](io: SafeIO[X], f: X => SafeIO[B]) extends SafeIO[B]
```
**F#**
```fsharp
// fsharp/src/SafeIO.fs, module Snake.SafeIO, compiled after IO.fs.
// Op, OpResult and Traced are the ones from Snake.IO: write them IO.Op, IO.OpResult, IO.Traced.
// (Do not `open Snake.IO`: its cases are also called Pure and Effect.)
type SafeIO<'a> =
| Pure of 'a
| Effect of IO.Op * (IO.OpResult -> SafeIO<'a>)
| FlatMap of SafeIO<obj> * (obj -> SafeIO<'a>)
```
**Language note: the hidden type `X`.** Look at `FlatMap(io, f)`: the whole program produces an `A`, but the inner `io` produces something else, an `X`, which only `f` cares about. Different `FlatMap` nodes have different `X`s, and nobody outside the node needs to know which. JS has no types to worry about. The Scala `enum` can say it directly: the case has its own type parameters, `FlatMap[X, B]`, and declares that it `extends SafeIO[B]`; `X` appears in no other place, so it stays hidden inside the node (a so-called existential type). An F# union case cannot introduce a type parameter of its own, so the F# track stores the inner program as a `SafeIO<obj>` and the continuation as `obj -> SafeIO<'a>`: the type `X` is *erased* to `obj`. The stub gives you a small helper, `erase : SafeIO<'a> -> SafeIO<obj>`, which views a program as a `SafeIO<obj>` with constant work (it boxes a `Pure`'s value and wraps continuations without calling them). `bind` is then `FlatMap (erase io, fun x -> f (unbox<'a> x))`. The `unbox` cannot fail, because only `bind` builds a `FlatMap`, and it always pairs a program of `'a` with a function expecting `'a`. Keep `erase` and `unbox` inside the module and the rest of the world never sees an `obj`.
### 4. The re-association rule, as a picture
An interpreter can only do something with a `Pure` (finished) or an `Effect` (perform it). So when it meets a `FlatMap`, it rewrites the program until the top is one of those two. Look at what can be inside a `FlatMap(io, f)`; there are three cases, one per constructor of `io`:
**Rule 1.** `FlatMap(Pure(a), f)` becomes `f(a)`. The first part is finished with `a`, so continue with `f(a)`. (This is the left identity law of Module 7.)
**Rule 2.** `FlatMap(Effect(op, k), f)` becomes `Effect(op, r => FlatMap(k(r), f))`. The first thing to do is `op`; afterwards, run the rest of the first part, `k(r)`, and then `f`. Note that the new continuation only *builds* a `FlatMap` node; it does not call `flatMap` recursively the way Module 7 did, so no tower can form.
**Rule 3.** `FlatMap(FlatMap(m, g), f)` becomes `FlatMap(m, x => FlatMap(g(x), f))`. This is the important one. As a picture:
```
FlatMap FlatMap
/ \ / \
FlatMap f ==> m x => FlatMap
/ \ / \
m g g(x) f
```
Read it in words: "(run `m`, then `g`), then `f`" is the same program as "run `m`, then (`g`, then `f`)". It is the associativity law of Module 7 section 10, used as an algorithm: the law says both shapes behave the same, so the interpreter is free to replace the left one by the right one. Each application moves one `FlatMap` from the left spine of the tree to the right side, where it waits inside a function until it is needed. A left-nested chain of `n` `flatMap`s needs `n` applications of rule 3 to expose its first `Effect`, then the continuations unwind one at a time. Every node is rewritten a constant number of times, so the total work is proportional to `n`, not `n²`.
Each rule looks at most two levels into the tree, builds at most two new nodes, and calls at most one function (`f` in rule 1). That is what makes the next idea possible.
### 5. Trampolining: an interpreter made of small moves
Put the three rules into one function, **`step(io)`**: given a `FlatMap`, apply the rule that matches and return the result; given a `Pure` or an `Effect`, return it unchanged. `step` is not recursive. After one `step`, the top of the program may still be a `FlatMap` (rule 3 produces one); that is fine, it will be stepped again.
An interpreter is now a loop of small moves:
- if the program is a `Pure`, stop;
- if it is an `Effect(op, k)`, perform `op` and continue with `k(answer)`;
- if it is a `FlatMap`, continue with `step(program)`.
In a runtime file, where a loop is allowed, that is a few lines (this is an illustration; the course's runtime keeps Module 7's `IO`):
```js
// What a stack-safe runIO would look like in runtime.js, where loops and `let` are allowed.
const runSafeIO = (io, platform) => {
let current = io
while (current.tag !== 'Pure') {
current = current.tag === 'FlatMap' ? step(current) : current.k(perform(current.op, platform))
}
return current.value
}
```
Every move returns to the loop before the next one starts, so the stack is the same height after a million moves as after one. This technique is called a **trampoline**: instead of one function calling the next from inside itself (which piles frames on the stack), each move *bounces back* to a driver, which makes the next call. ([Trampoline (computing)](https://en.wikipedia.org/wiki/Trampoline_%28computing%29) on Wikipedia; Rúnar Bjarnason's ["Stackless Scala With Free Monads"](http://blog.higher-order.com/assets/trampolines.pdf) builds exactly this structure.)
> **From everyday programming:** you may have converted a recursive tree walk into a loop with an explicit stack, because a deep tree crashed the recursive version. That is the same move: the work that the call stack used to remember is now held in a data structure that the loop manages. Job runners do it too: instead of each job calling the next job directly, which would make an ever deeper chain of calls, each job returns "what to do next" to a dispatcher that runs it. Here the data structure is the program tree itself, and rule 3 is what keeps its left side shallow.
### 6. A loop without a loop: the pure `trace`
The tests need a *pure* interpreter, `trace(io, seed)`, with the same contract as in Module 7: answer every `RandomSeed` with `seed` and every `Draw` with unit, and return `{result, ops}`. Any other op, such as Module 10's `LoadBest`, is an error: the given `answer` throws rather than inventing a reply. It must survive 100 000 operations. How it achieves that depends on the language.
**Scala and F#** turn a function that calls itself as its very last action (a *tail call*) into a loop: Scala checks it for you with `@tailrec`, F# does it for any `let rec` function that calls itself in tail position ([Tail call](https://en.wikipedia.org/wiki/Tail_call)). So `trace` is an inner function `loop(current, performed)` with one case per constructor, each ending in a call to `loop`: the stack stays flat and nothing is mutated. Collect the ops by putting each one at the *front* of an immutable list (constant work) and reverse the list once at the end.
**JavaScript** is the awkward one. The language standard has asked for proper tail calls since 2015, but only Safari implements them ([WebKit: ECMAScript 6 proper tail calls](https://webkit.org/blog/6240/ecmascript-6-proper-tail-calls-in-webkit/)); in Node and Chrome every call takes a stack frame, so a recursive `trace` overflows. A `while` loop would work, but this file follows the purity rules: no loops, no `let`. The stub therefore gives you a driver, `drive(start, isDone, next)`, which applies a "move" function `next` until `isDone`, using recursion that is only **logarithmically** deep:
- doing `n` moves is doing `n/2` moves and then `n/2` more; each half is split again, so the recursion is `log2(n)` levels deep (17 levels for 100 000 moves);
- the driver does not know in advance how many moves the program needs, so it tries 1 move, then 2 more, then 4 more, doubling the budget each round until the program is finished (another `log2` levels).
Your part is the move: **`traceStep(io, seed)`** returns `[nextProgram, ops]`, where `ops` is `[]` for a `FlatMap` (rewritten with `step`) and `[op]` for an `Effect` (answered and continued). `trace` then runs `drive` with `traceStep` until the program is a `Pure`.
**Collecting the ops.** The obvious way, `ops = [...ops, op]` at every move, copies the whole array each time: `n²/2` copies, exactly the quadratic cost this module set out to remove. Putting each op at the front of a linked list is cheap, but turning that list back into an array needs either a loop or a recursion `n` levels deep. `drive` instead keeps the outputs in a balanced tree that follows the halving above, and flattens it once at the end: every op is copied once per level of the tree, and the tree is only logarithmically deep (a few dozen levels for 100 000 ops). That is `n log n` work, a little more than linear and nowhere near quadratic, with no loop, no mutation and a shallow stack. You don't have to write `drive`, but read it: it is twenty lines.
### 7. What production libraries add
The three-constructor `IO` with a re-associating interpreter is the core of every serious effect library. Real ones add:
- **A faster run loop.** Instead of rewriting the tree, the interpreter keeps the pending continuations of rule 3 on a stack of its own (a mutable array, safely hidden inside the runtime): meeting `FlatMap(io, f)` pushes `f` and continues with `io`; reaching a `Pure(a)` pops the top `f` and continues with `f(a)`. Same behaviour, fewer allocations. cats-effect's run loop works this way; other libraries use variations of the same idea.
- **Suspended side effects.** A constructor such as `Delay(() => a)` turns any side-effecting code into a description, so a library needs no fixed list of operations like our `Op`.
- **Errors, cancellation and concurrency.** Constructors for raising and handling errors, for running programs on lightweight threads (*fibers*), for waiting on asynchronous callbacks, and for cancelling a fiber safely; a scheduler that interleaves thousands of fibers on a few operating-system threads, letting a busy fiber pause regularly so that others get a turn ([cats-effect: thread model](https://typelevel.org/cats-effect/docs/thread-model)).
- **The same trick elsewhere.** Free monads get a `FlatMapped` constructor and a stack-safe interpreter in the same way ([cats: Free monads](https://typelevel.org/cats/datatypes/freemonad.html)), and Scala's standard library ships a small general-purpose trampoline, [`scala.util.control.TailCalls`](https://www.scala-lang.org/api/current/scala/util/control/TailCalls$.html).
## Exercises
> **About test names.** The test titles quoted under "Proved by" are the JavaScript ones (in `js/test/m07b-safeio.test.js`). The Scala and F# suites (`scala/test/M07bSafeIO.test.scala`, `fsharp/tests/M07bSafeIO.fs`) test the same things in each track's vocabulary. Two differences: `traceStep` and `drive` exist only in JS (section 6), and functions cannot be compared, so where the JS tests check that a node holds the very same `f`, the Scala and F# tests check the node's shape and then observe the program through `trace`.
All exercises are in the one file per track. Write them in order. The purity rules apply as usual (no `let`, no loops, no mutation; Scala/F#: no `var`/`mutable`); the given `drive` follows them too.
### Exercise 7b.1 — `pure`, `effect`, `flatMap`, `map`, `sequence`
**JavaScript**
```js
export const pure = (value) => /* SafeIO<A>: { tag: 'Pure', value } */
export const effect = (op) => /* SafeIO<answer>: { tag: 'Effect', op, k: pure } */
export const flatMap = (io, f) => /* SafeIO<B>: only builds { tag: 'FlatMap', io, f } */
export const map = (io, f) => /* SafeIO<B>, from flatMap and pure */
export const sequence = (ios) => /* SafeIO<undefined>, left to right; either fold is fine now */
```
**Scala**
```scala
enum SafeIO[+A]:
...
def flatMap[B](f: A => SafeIO[B]): SafeIO[B] = ???
def map[B](f: A => B): SafeIO[B] = ???
object SafeIO:
def pure[A](a: A): SafeIO[A] = ???
def effect(op: Op): SafeIO[OpResult] = ???
def sequence(ios: List[SafeIO[Unit]]): SafeIO[Unit] = ???
```
**F#** (data-last; the helper `erase` is given)
```fsharp
let pure (a: 'a) : SafeIO<'a> = failwith "TODO m07b"
let effect (op: IO.Op) : SafeIO<IO.OpResult> = failwith "TODO m07b"
let bind (f: 'a -> SafeIO<'b>) (io: SafeIO<'a>) : SafeIO<'b> = failwith "TODO m07b"
let map (f: 'a -> 'b) (io: SafeIO<'a>) : SafeIO<'b> = failwith "TODO m07b"
let sequence (ios: SafeIO<unit> list) : SafeIO<unit> = failwith "TODO m07b"
```
**Proved by:** `pure builds a Pure`, `effect(op) is an Effect whose continuation is pure`, `flatMap only builds a FlatMap node`, `building flatMap(pure(1), throwingF) does not throw`, `building map(io, throwingF) does not throw`. `sequence` is checked through `trace` in Exercise 7b.3.
**Watch out for:** writing `flatMap` the Module 7 way. If your `flatMap` looks at `io.tag` at all, it is doing the interpreter's job.
### Exercise 7b.2 — `step`, the re-association step
**JavaScript**
```js
export const step = (io) => /* SafeIO<A>: one rewrite (section 4); Pure and Effect come back unchanged */
```
**Scala**
```scala
object SafeIO:
def step[A](io: SafeIO[A]): SafeIO[A] = ???
```
**F#**
```fsharp
let step (io: SafeIO<'a>) : SafeIO<'a> = failwith "TODO m07b"
```
**Proved by:** `step leaves a Pure unchanged`, `step leaves an Effect unchanged`, `step of FlatMap(Pure(a), f) is f(a)`, `step of FlatMap(Effect(op, k), f) is an Effect with the same op that continues with FlatMap(k(r), f)`, `step re-associates FlatMap(FlatMap(m, g), f) into FlatMap(m, x => FlatMap(g(x), f))`, `step does not call the functions it re-associates`, `step of a left-nested chain is one rewrite, not a whole walk`, and `a while loop over step is a complete interpreter for 100 000 left-nested flatMaps` (the loop of section 5, written in the test file, where loops are allowed).
**Watch out for:** recursion. `step` applies *one* rule and returns. If it calls itself, a left-nested chain of 100 000 overflows the stack inside `step`, and the "one rewrite" test sees that the top is no longer a `FlatMap`.
### Exercise 7b.3 — the pure interpreter
**JavaScript** (`drive` is given)
```js
export const traceStep = (io, seed) => /* [nextProgram, ops]: ops is [] or [op] */
export const trace = (io, seed) => /* { result, ops }, using drive and traceStep */
```
**Scala** (a `@tailrec` inner function)
```scala
object SafeIO:
def trace[A](io: SafeIO[A], seed: Long): Traced[A] = ???
```
**F#** (an inner `let rec` that calls itself only in tail position)
```fsharp
let trace (seed: uint32) (io: SafeIO<'a>) : IO.Traced<'a> = failwith "TODO m07b"
```
Answer every `RandomSeed` with the seed (`Seed(seed)` in Scala/F#) and every `Draw` with unit (`Done`), as in Module 7.
**Proved by:** `traceStep answers an Effect and records its op`, `traceStep rewrites a FlatMap and records nothing`, `traceStep leaves a Pure unchanged`, `trace of pure(3) has result 3 and no ops`, `trace answers RandomSeed with the seed and Draw with undefined`, `tracing map(io, throwingF) runs f and throws`, `trace refuses operations of later modules`, `sequence of nothing has no ops`, `sequence runs effects left to right`; the stack-safety tests `100 000 left-nested flatMaps of draws run, in order`, `100 000 right-nested flatMaps of draws run, in order`, `100 000 left-nested maps run without any op`, `sequence of 100 000 draws runs`; the nine monad-law tests (`left identity holds for …`, `right identity holds for …`, `associativity holds for …`, for `a pure value`, `one effect` and `a draw followed by reading the seed`); and `trace equals Module 7's trace for …` on four programs, each written once and built with both `IO`s. (`drive collects every output in order over 200 000 moves` and `drive of a state that is already done makes no move` test the given driver; they pass from the start.)
**Run (all of Module 7b):**
- JS: `cd js && node --test "test/m07b-*.test.js"`
- Scala: `cd scala && ./test.sh M07b`
- F#: `cd fsharp && ./test.sh M07b`
## Hints
<details><summary>Exercise 7b.1 — nudge</summary>
`pure` and `effect` are the same as in Module 7. `flatMap` is shorter than in Module 7: it does not inspect anything. `map` and `sequence` can be written exactly as in Module 7, on top of the new `flatMap` and `pure`.
</details>
<details><summary>Exercise 7b.1 — stronger hint</summary>
`flatMap(io, f)` returns an object with three fields, `tag`, `io` and `f`, and nothing else happens. `map` is "and then finish with `f` of the value". For `sequence`, a left fold that starts from `pure(undefined)` and glues each program on with `flatMap(acc, () => io)` is fine now: that is the shape that broke Module 7, and the tests run 100 000 of them.
</details>
<details><summary>Exercise 7b.1 — near-solution (try the others first)</summary>
**JavaScript**
```js
export const flatMap = (io, f) => ({ tag: ___, io, f })
export const map = (io, f) => flatMap(io, (a) => ___)
export const sequence = (ios) => ios.reduce((acc, io) => flatMap(___, () => ___), pure(undefined))
```
**Scala**
```scala
def flatMap[B](f: A => SafeIO[B]): SafeIO[B] = FlatMap(___, f)
def map[B](f: A => B): SafeIO[B] = flatMap(a => ___)
def sequence(ios: List[SafeIO[Unit]]): SafeIO[Unit] = ios.foldLeft(pure(()))((acc, io) => ___)
```
**F#**
```fsharp
let bind (f: 'a -> SafeIO<'b>) (io: SafeIO<'a>) : SafeIO<'b> = FlatMap(erase ___, fun x -> f (unbox<'a> ___))
let map (f: 'a -> 'b) (io: SafeIO<'a>) : SafeIO<'b> = io |> bind (fun a -> ___)
let sequence (ios: SafeIO<unit> list) : SafeIO<unit> = List.fold (fun acc io -> ___) (pure ()) ios
```
</details>
<details><summary>Exercise 7b.2 — nudge</summary>
First decide what to do when `io` is not a `FlatMap` (the doc comment says). Otherwise look one level down, at `io.io`: it is a `Pure`, an `Effect` or a `FlatMap`, and section 4 has one rule for each. Write each rule's right-hand side as an object literal or a `flatMap` call.
</details>
<details><summary>Exercise 7b.2 — stronger hint</summary>
Name the parts: the outer node is `FlatMap(inner, f)`. Rule 1 needs `inner.value`; rule 2 needs `inner.op` and `inner.k`; rule 3 needs `inner.io` (the `m` of the picture) and `inner.f` (the `g`). Building a `FlatMap` node is what your `flatMap` does, so you can use it on the right-hand sides. Nothing in `step` calls `step`.
</details>
<details><summary>Exercise 7b.2 — near-solution (try the others first)</summary>
**JavaScript**
```js
export const step = (io) => {
if (io.tag !== 'FlatMap') return ___
const { io: inner, f } = io
if (inner.tag === 'Pure') return ___
if (inner.tag === 'Effect') return { tag: 'Effect', op: inner.op, k: (r) => flatMap(___, f) }
return flatMap(inner.io, (x) => flatMap(___, ___))
}
```
**Scala**
```scala
def step[A](io: SafeIO[A]): SafeIO[A] = io match
case FlatMap(Pure(x), f) => ___
case FlatMap(Effect(op, k), f) => Effect(op, r => FlatMap(___, f))
case FlatMap(FlatMap(m, g), f) => FlatMap(m, x => FlatMap(___, ___))
case other => other
```
**F#**
```fsharp
let step (io: SafeIO<'a>) : SafeIO<'a> =
match io with
| FlatMap(Pure x, f) -> ___
| FlatMap(Effect(op, k), f) -> Effect(op, fun r -> FlatMap(___, f))
| FlatMap(FlatMap(m, g), f) -> FlatMap(m, fun x -> FlatMap(___, ___))
| other -> other
```
In F# the inner parts already have the erased types (`m : SafeIO<obj>`, `g : obj -> SafeIO<obj>`), so no `erase` or `unbox` is needed here.
</details>
<details><summary>Exercise 7b.3 — nudge</summary>
`traceStep` has three cases, one per constructor; section 6 lists what each returns. For `trace`: what is the start state, when is the driver done, and what is the move? `drive` returns `[last, ops]`; the result is in `last`.
</details>
<details><summary>Exercise 7b.3 — stronger hint</summary>
The `Effect` case of `traceStep` is the body of Module 7's `trace` without the recursion: answer the op, call `k`, and return the new program together with `[op]` instead of tracing it further. The driver is done when the program is a `Pure`. In Scala and F#, the same three cases go into the inner tail-recursive function, with the list of performed ops as an extra argument that grows at the front; the `Pure` case returns the result with that list reversed.
</details>
<details><summary>Exercise 7b.3 — near-solution (try the others first)</summary>
**JavaScript**
```js
export const traceStep = (io, seed) => {
if (io.tag === 'FlatMap') return [___, []]
if (io.tag === 'Effect') return [io.k(answer(io.op, seed)), ___]
return [io, []]
}
export const trace = (io, seed) => {
const [last, ops] = drive(io, (p) => ___, (p) => traceStep(p, seed))
return { result: ___, ops }
}
```
**Scala**
```scala
def trace[A](io: SafeIO[A], seed: Long): Traced[A] =
@tailrec def loop(current: SafeIO[A], performed: List[Op]): Traced[A] = current match
case Pure(a) => Traced(a, ___)
case Effect(op, k) => loop(k(___), op :: performed)
case other => loop(___, performed)
loop(io, Nil)
```
**F#**
```fsharp
let trace (seed: uint32) (io: SafeIO<'a>) : IO.Traced<'a> =
let rec loop current performed =
match current with
| Pure a -> { IO.Result = a; IO.Ops = ___ }
| Effect(op, k) -> loop (k ___) (op :: performed)
| FlatMap _ -> loop ___ performed
loop io []
```
</details>
## Check your understanding
1. With Module 7's `IO`, why does a `while` loop in the interpreter not save a left-nested chain from overflowing the stack? Where exactly is the depth?
2. Count, for Module 7's `IO`, the calls needed to run a left-nested chain of 4 effects, then of 8. How does the count grow? Now count the `step`s that `SafeIO` needs for the same chains.
3. Draw the tree of `flatMap(flatMap(flatMap(e, f1), f2), f3)` and apply rule 3 until the top is `FlatMap(e, …)`. How many rewrites did it take? Then write out the continuation you ended with.
4. Rule 3 replaces one program by another. Which law of Module 7 says that this does not change what the program does? What would go wrong if an interpreter applied rule 3 to an `IO` type that did not obey that law?
5. `step` never calls itself. Why is that essential in JavaScript, and why would it still be a good idea in Scala or F#?
6. Module 7 section 9 said that `flatMap(pure(1), f)` calls `f` while building. Is that still true for `SafeIO`? When does `f` run now?
7. Why doesn't `trace` collect its ops with `[...ops, op]`? Estimate how many element copies that would make for 100 000 ops, and compare with what `drive` does.
8. Rules 1 and 3 never look at an `Op`. Suppose you add a new operation (say `PlaySound`): which parts of `safeio.js` change, and which don't?
9. **Challenge.** If you knew the exact number of moves in advance, you could write a driver with no recursion at all, using `Array.from` and `reduce`. How? Why doesn't that work for `trace`, and how does the doubling budget of `drive` get around it?
## In production
- **cats-effect** (Scala) and **ZIO** (Scala) represent `flatMap` as a data constructor and interpret programs in a run loop that makes any shape of program stack-safe; they add suspended side effects, error handling, fibers, cancellation and a fair scheduler ([cats-effect](https://typelevel.org/cats-effect/), [ZIO](https://zio.dev/)).
- **Free monads** in libraries such as cats get the same treatment: a `FlatMapped` constructor and a stack-safe interpreter ([cats: Free monads](https://typelevel.org/cats/datatypes/freemonad.html)).
- **Haskell** handles left-nested binds of free monads with *type-aligned sequences*, which keep a chain of continuations in a queue that can be extended at either end in constant time ([van der Ploeg and Kiselyov, "Reflection without Remorse"](https://okmij.org/ftp/Haskell/zseq.pdf)).
- **General-purpose trampolines** appear in many standard libraries; Scala's [`scala.util.control.TailCalls`](https://www.scala-lang.org/api/current/scala/util/control/TailCalls$.html) is one, and in JavaScript you meet the same idea whenever a long recursion is turned into a loop that calls "the next step" returned by the previous one.
## Further reading
- [Rúnar Bjarnason, "Stackless Scala With Free Monads"](http://blog.higher-order.com/assets/trampolines.pdf) — trampolines, the `FlatMap` constructor and the re-association rule, step by step.
- [Atze van der Ploeg and Oleg Kiselyov, "Reflection without Remorse"](https://okmij.org/ftp/Haskell/zseq.pdf) — why left-nested binds are quadratic in general, and a data structure that fixes it.
- [Oleg Kiselyov and Hiromi Ishii, "Freer Monads, More Extensible Effects"](https://okmij.org/ftp/Haskell/extensible/more.pdf) — the "operation plus continuation" representation of Module 7, with the continuations kept in such a queue.
- [Trampoline (computing)](https://en.wikipedia.org/wiki/Trampoline_%28computing%29) and [Tail call](https://en.wikipedia.org/wiki/Tail_call) on Wikipedia.
- [WebKit: ECMAScript 6 proper tail calls](https://webkit.org/blog/6240/ecmascript-6-proper-tail-calls-in-webkit/) — what JavaScript was supposed to get, and why most engines did not ship it.
- [Scala 3: algebraic data types with enums](https://docs.scala-lang.org/scala3/reference/enums/adts.html) and [F#: recursive functions](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/functions/recursive-functions-the-rec-keyword) — the language features the Scala and F# tracks rely on here.