Files
functional-programming-snake/course/09-wiring-it-together.md
T

535 lines
31 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 9 — Wiring it together
> **Time:** ~4 h · **You'll edit:** JS `js/src/input.js`, `js/src/app.js`, `js/src/runtime.js`, `js/src/browser.js` · Scala `scala/src/Input.scala`, `scala/src/App.scala`, `scala/src/Runtime.scala` · F# `fsharp/src/Input.fs`, `fsharp/src/App.fs`, `fsharp/src/Runtime.fs`
> **Tests:** JS `cd js && node --test "test/m09-*.test.js"` · Scala `cd scala && ./test.sh M09` · F# `cd fsharp && ./test.sh M09`
> **Prerequisites:** Modules 0–8. You need working `update` (m05), `newGame` (m04), `view` (m06), `IO` (m07) and streams (m08).
## Why this module
You now have every piece of a game, and none of them does anything:
- `update(state, action)` says how the game changes (Module 5);
- `view(state)` describes a picture (Module 6);
- `IO` describes effects such as drawing and asking for a seed (Module 7);
- `Stream` describes how to react to time and keys (Module 8).
In this module you connect them into **one value that is the whole program**, and then write the **one small function** that brings that value to life in a browser. At the end of the module, Snake is playable.
The connection has a striking shape. Try to read this type out loud:
```
main : IO<Stream<IO<unit>>>
```
"`main` is an effect (ask for a random seed) that produces a stream (react to time and keys) of effects (draw this frame)." Or, in one sentence that sums up the course so far:
> **A program is an effect that produces a stream of effects.**
Everything in that sentence is a value you can test with `trace` and `run`. The only thing that is *not* a value is the runtime that performs it, and you'll write it yourself and see how small it is: one loop, three callbacks and **one mutable variable**.
## Learning objectives
By the end of this module you can:
- **implement** `keyToAction`, the action stream, the state stream (`startWith` + `scan`), `render` and `main` from the pieces of Modules 4–8;
- **explain** the type `IO<Stream<IO<unit>>>` layer by layer, and why `main` is a value;
- **implement** an interpreter `runIO` as a loop and a driver `runApp` that holds the only mutable variable of the program;
- **explain** how a `Platform` interface lets the runtime be tested with a fake instead of a browser;
- **run** the game in a browser in your track, and **trace** one key press from the keyboard to the pixels.
## Concepts
### 1. From keys to actions
The browser tells us which key went down as a string (`KeyboardEvent.key`: `'ArrowUp'`, `'w'`, `'W'`, `'x'` …). The game speaks in `Action`s. The translation is a pure function:
- `'ArrowUp'`, `'w'`, `'W'` → `Turn Up`; `'ArrowDown'`, `'s'`, `'S'` → `Turn Down`; `'ArrowLeft'`, `'a'`, `'A'` → `Turn Left`; `'ArrowRight'`, `'d'`, `'D'` → `Turn Right`;
- any other key → *nothing* (`null` in JS, `None` in Scala/F#).
It returns "maybe an action" because most keys mean nothing to the game. That's exactly the shape `filterMap` (F#: `choose`) from Module 8 wants.
A small JS trap is tested on purpose: if you implement the table as an object lookup, `table['constructor']` and `table['toString']` are **not** `undefined` — every JS object inherits those from `Object.prototype`. The fixture cases `constructor-is-not-a-key` and `toString-is-not-a-key` catch it. A `switch`, a `Map`, or an object created with `Object.create(null)` avoids the problem. (Scala and F# pattern matches don't have this issue.)
### 2. From events to actions: one stream of everything that can happen
What can happen to the game? A tick every 150 ms, and the actions of recognised key presses. As streams:
```
ticks = map(interval(TICK_MS), _ => Tick) Stream<Action>
turns = filterMap(keyDowns, keyToAction) Stream<Action>
classicActions = merge(ticks, turns) Stream<Action>
```
Read it as a diagram:
```
events ──┬──▶ interval(150) ──map(→Tick)──────────────┐
│ ├──merge──▶ classicActions : Stream<Action>
└──▶ keyDowns ──filterMap(keyToAction)───────┘
```
Each `Time` event goes to both branches (that's what `merge` does); `interval` decides whether a tick happened, `keyDowns` ignores it. Each `KeyDown` also goes to both; `interval` ignores it, `keyToAction` decides whether it means something.
Check that you can predict the test:
```js
run(classicActions, [time(150), down('ArrowUp'), down('x'), time(300)])
// → [Tick, Turn('Up'), Tick] 'x' produced nothing
```
### 3. From actions to states: a running fold
Module 5's insight was that a game is a fold over actions: `replay(newGame(seed), actions)`. A **stream** of actions folds the same way, except we want every intermediate state, not only the last one, because we must draw each of them. That's `scan`:
```
scan(actions, update, newGame(seed)) Stream<GameState>: one new state per action
```
There's one more thing: `scan` emits only *new* accumulators, never the initial one. If we stopped here, nothing would be drawn until the first action — 150 ms of blank screen, and a board that's never shown in its starting position. So we put the initial state in front with `startWith`:
```
states(seed, actions) = startWith(scan(actions, update, newGame(seed)), newGame(seed))
```
> **From everyday programming:** in the classic game loop, `while (running) { input(); update(); render(); }`, the game state is one variable that each iteration overwrites, and the sequence of states exists only as the history of that variable. `startWith(scan(…), s0)` gives you that history as a value: `s0, s1, s2, …`, one state per action, the way a bank statement lists the opening balance followed by the balance after each transaction. `scan` alone is the list of balances *after* each transaction; `startWith` adds the opening balance in front.
You may have seen this exact shape elsewhere: **Redux** reducers are `(state, action) → state` folded over a stream of dispatched actions, and **Elm**'s `update : Msg -> Model -> Model` is the same fold run by the Elm runtime. (The argument order differs per library; the idea doesn't.)
### 4. From states to drawings, and `main`
To draw a state: describe the picture (`view`, Module 6) and turn the description into a drawing program (`drawScene`, Module 7):
```
render(state) = drawScene(view(state)) IO<unit>
```
Mapping `render` over the states gives a stream of drawing programs, `Stream<IO<unit>>`. The only thing still missing is the seed, which must come from the outside world — that's an `IO` (`randomSeed`). Put together:
```
main = map(randomSeed, seed => map(states(seed, classicActions), render))
```
Here the two `map`s are *different* functions: the outer one is `IO`'s `map`, the inner one is `Stream`'s `map`. (In JS they come from two modules, so you'll import one of them under another name, e.g. `import { map as mapIO } from './io.js'`. In Scala they're methods, so the types pick the right one. In F# they're `IO.map` and `Stream.map`.) Let's read the type from the outside in:
```
IO< an effect: ask the world for a seed, and then produce…
Stream< …a machine that, for every event…
IO<unit> …produces the effects that draw a frame.
>>
```
Why must the outer layer be an `IO`? Because the stream of states depends on the seed, and the seed isn't known until the outside world is asked. Why is the inner layer an `IO` and not a `Scene`? Because the runtime should only know how to perform `IO`s; turning scenes into effects is the program's business, not the runtime's. (In Module 10 you could make frames that also play sounds, without touching the runtime.)
And `main` is a **value**. Defining it performs nothing — no seed is asked for, no listener is registered. Look at the test:
```js
const { ops, result: stream } = trace(main, 42)
assert.deepStrictEqual(ops, [{ tag: 'RandomSeed' }]) // main asks for a seed, nothing else
const [outs] = stream.step(time(0)) // the first event…
assert.equal(outs.length, 1) // …produces exactly one drawing: the new game
```
The whole program, with its randomness, time and keyboard, is tested with the pure interpreter from Module 7 and `step` from Module 8. No browser.
**About `classicActions` and Module 10.** In Module 10, `main` will switch from `classicActions` to `speedActions`. The m09 tests that check the 150 ms behaviour therefore use a copy of `main` built from `classicActions` (`classicMain` in the test helpers), so they keep passing after Module 10.
### 5. The edge: a Platform and a runtime
Now somebody has to *do* it. Let's list everything the outside world must provide:
- draw a rectangle; draw a text;
- give a random seed;
- call us back with the time, again and again;
- call us back when a key goes down; when a key goes up.
That list is an interface, which the course calls a **Platform**:
**JavaScript**
```js
// Platform = {
// fillRect(x, y, w, h, color),
// fillText(x, y, text, color, size, align),
// randomSeed() → number (uint32),
// startClock(onTime: (t) => void),
// onKeyDown(cb: (key) => void),
// onKeyUp(cb: (key) => void),
// }
```
**Scala** (`scala/src/Runtime.scala`)
```scala
trait Platform:
def fillRect(x: Int, y: Int, w: Int, h: Int, color: String): Unit
def fillText(x: Int, y: Int, text: String, color: String, size: Int, align: String): Unit
def randomSeed(): Long
def startClock(onTime: Long => Unit): Unit
def onKeyDown(cb: String => Unit): Unit
def onKeyUp(cb: String => Unit): Unit
```
**F#** (`fsharp/src/Runtime.fs`) — a record of functions
```fsharp
type Platform =
{ FillRect: int -> int -> int -> int -> string -> unit
FillText: int -> int -> string -> string -> int -> string -> unit
RandomSeed: unit -> uint32
StartClock: (int64 -> unit) -> unit
OnKeyDown: (string -> unit) -> unit
OnKeyUp: (string -> unit) -> unit }
```
The runtime only ever talks to a `Platform`. The browser implementation (`browser.js` in JS, `web/BrowserPlatform.scala`, `fsharp/web/BrowserPlatform.fs`) wraps a canvas, `requestAnimationFrame` and `crypto.getRandomValues`. The tests use a **fake platform** that records every call in a list and lets the test fire events by hand — a test double in the sense of [Martin Fowler's "Test Double"](https://martinfowler.com/bliki/TestDouble.html):
```js
const fakePlatform = (seed = 42) => {
const calls = []; const handlers = {}
return {
calls,
fillRect: (...args) => calls.push(['fillRect', ...args]),
fillText: (...args) => calls.push(['fillText', ...args]),
randomSeed: () => { calls.push(['randomSeed']); return seed },
startClock: (cb) => { handlers.time = cb },
onKeyDown: (cb) => { handlers.down = cb },
onKeyUp: (cb) => { handlers.up = cb },
fireTime: (t) => handlers.time(t),
fireKey: (k) => handlers.down(k),
fireKeyUp: (k) => handlers.up(k),
}
}
```
(Yes, the fake uses `push` and mutation; it's test code, which is allowed to be impure. It plays the role of the outside world.)
### 6. `runIO`: the real interpreter
`runIO(io, platform)` is `trace` from Module 7 with real answers:
- `Draw` of a `Rect` → `platform.fillRect(x, y, w, h, color)`; of a `Text` → `platform.fillText(x, y, text, color, size, align)`; the answer is unit (`Done`);
- `RandomSeed` → `platform.randomSeed()`; the answer is that number (`Seed(n)`);
- pass the answer to `k`, and continue with the program it returns, until you reach `Pure(value)`; return `value`.
One requirement is new: **it must be a loop, not recursion.** A single frame is a few hundred draws, a game runs for minutes, and the test `runIO handles 100000 effects without overflowing the stack` makes sure. In JS that means a `while` loop over a local "current program" variable (allowed here, and only here). In Scala, write a `@annotation.tailrec` recursive function: the compiler turns it into a loop, and you keep your one `var` for `runApp`. In F#, either a loop or a tail-recursive function works: F# compiles a self-recursive call in tail position into a loop, so `let rec runIO p io = match io with ... | Effect (op, k) -> runIO p (k ...)` doesn't grow the stack.
Remember section 12 of Module 7: the loop only helps if each continuation does a small, constant amount of work. If your `sequence` is left-nested, the 100 000-effect test overflows *inside* `k`, and the fix is in `io`, not in the runtime.
### 7. `runApp`: one mutable variable
`runApp(main, platform)` brings the program to life:
1. **Interpret `main`** with `runIO`. This asks the platform for a seed and gives you the stream (`Stream<IO<unit>>`).
2. **Keep the current stream in a mutable variable.** This is the *only* mutable variable in the whole program that outlives a function call. In F# it is a **ref cell** (`let stream = ref …`, read with `stream.Value`, written with `stream.Value <- next`), because F# closures can't capture a `let mutable`, and the callbacks are closures. (The loop in `runIO` in JS reassigns a local "current program" variable, but that variable exists only during one call and nobody else can see it; it is a loop counter, not state. The stream variable is the program's entire memory between events.)
3. **Register three callbacks** on the platform — key down, key up, clock — each of which turns what it receives into an event (`KeyDown key`, `KeyUp key`, `Time t`) and **feeds** it:
- step the current stream with the event → `(ios, next)`;
- store `next` in the variable;
- run each IO in `ios` with `runIO`, in order.
That's the whole runtime. Go back to the stateful code at the start of Module 8, with its `let state` and logic in callbacks, and compare. There, the mutable variable held the *game state*, and game logic lived in the callbacks. Here, the variable holds the *current stream* — the runtime doesn't even know there's a game. It has no idea what a snake, a tick or a score is. All the game logic is in pure values; the runtime is a few lines of plumbing. That is what "pushing effects to the edge" means in practice.
```
┌────────────────────────────── runtime (impure, ~20 lines) ─────────────────────────────────┐
browser ──key──▶│ feed(KeyDown key): [ios, next] = stream.step(ev); stream = next; ios.forEach(runIO) │
──time─▶│ feed(Time t) │
└────────────────────────────────────────────────────────────────────────────────────────────┘
▲ the one `let` / `var` / ref cell: stream
```
The tests describe the behaviour precisely; read them before writing the code:
- `runApp asks the platform for a seed once, at start, and draws nothing before the first input` — after `runApp`, the only call is `['randomSeed']`. Nothing is drawn until an event arrives (remember `startWith`).
- `runApp draws the initial scene on the first clock tick, then the ticked scene after 150 ms` — a single `fireTime(150)` produces two drawings: the `startWith` state and the state after one `Tick`. Can you explain why one event produces two frames?
- `runApp feeds key presses to the game`, and `runApp ignores key releases and unmapped keys (no redraw)`.
### 8. Who is impure, exactly?
Let's take inventory. In JS, three files are allowed to be impure and are excluded from the purity audit: `runtime.js` (`runIO`, `runApp`), `browser.js` (the canvas `Platform`) and `main.js` (one line: `runApp(main, browserPlatform(document.getElementById('game'), window))`). In Scala it's `Runtime.scala` plus the browser glue in `scala/web/`; in F#, `Runtime.fs` plus `fsharp/web/` (whose `SnakeMain.fs` is the one line `Runtime.runApp (BrowserPlatform.create (BrowserPlatform.gameCanvas ())) (App.main ())` — note `main ()`: in F#, `main` is a function of unit, like `IO.randomSeed ()`, for the reason explained in Module 7). Everything else — rules, randomness, view, `IO`, streams, `main` — is pure and passes the audit.
Count the lines of your runtime when you're done. It's probably around twenty. That's the whole imperative shell around a functional core.
### 9. Running the game
**JavaScript.** You also write `browser.js` (see Exercise 9.4). Then, from the repository root:
```
node scripts/serve.mjs
```
and open <http://localhost:8000/js/>. (A static server is needed because browsers refuse to load ES modules from `file://` URLs.)
**Scala.** The browser glue is given (`scala/web/BrowserPlatform.scala`, `scala/web/Main.scala`); read it — it's short. Compile your code to JavaScript with Scala.js:
```
cd scala && ./build-web.sh
```
The first run downloads the Scala.js linker and takes a while. It writes `scala/web/dist/snake.js`. Then serve the repository root (`node scripts/serve.mjs`) and open <http://localhost:8000/scala/>.
**F#.** The browser glue is given in `fsharp/web/`. Compile with Fable:
```
cd fsharp && ./build-web.sh
```
The first run restores Fable (a local .NET tool listed in `fsharp/.config/dotnet-tools.json`) and takes a while. It writes `fsharp/web/dist/`. Then serve the repository root (`node scripts/serve.mjs`, run from the repository root) and open <http://localhost:8000/fsharp/>.
Use the arrow keys or WASD. **You built a game in which every line of logic is pure.** Play a round. Then, before moving on, do the trace exercise in "Check your understanding": follow one key press from the keyboard to the pixels, naming each function it passes through.
## Exercises
> **About test names.** The test titles quoted under "Proved by" are the JavaScript ones. The Scala and F# suites test the same things one-to-one, but their titles use each track's vocabulary: "events" instead of "inputs", `None` instead of `null`, `()` instead of `undefined`, `bind`/`choose` instead of `flatMap`/`filterMap`, `IO.map throwingF io` instead of `map(io, throwingF)`, and so on. Failing tests are reported by name when you run the module's tests; to read all titles, open `scala/test/M09*.test.scala` or `fsharp/tests/M09*.fs`. Fixture-based tests have the same case names in every track.
### Exercise 9.1 — `keyToAction`
**JavaScript** (`js/src/input.js`)
```js
export const keyToAction = (key) => /* Action or null */
```
**Scala** (`scala/src/Input.scala`)
```scala
object Input:
def keyToAction(key: String): Option[Action] = ???
```
**F#** (`fsharp/src/Input.fs`)
```fsharp
// module Snake.Input
let keyToAction (key: string) : Action option = failwith "TODO m09"
```
**Proved by:** `fixtures/m09-input.json` (`ArrowUp-turns-up` … `D-turns-right`, `x-is-ignored`, `Enter-is-ignored`, `empty-string-is-ignored`, `constructor-is-not-a-key`, `toString-is-not-a-key`).
### Exercise 9.2 — the app: `classicActions`, `states`, `render`, `main`
**JavaScript** (`js/src/app.js`)
```js
export const TICK_MS = 150
export const classicActions = /* Stream<Action> */
export const states = (seed, actions) => /* Stream<GameState> */
export const render = (state) => /* IO<undefined> */
export const main = /* IO<Stream<IO<undefined>>> */
```
**Scala** (`scala/src/App.scala`)
```scala
object App:
val TICK_MS: Long = 150
lazy val classicActions: Stream[Action] = ???
def states(seed: Long, actions: Stream[Action]): Stream[GameState] = ???
def render(state: GameState): IO[Unit] = ???
lazy val main: IO[Stream[IO[Unit]]] = ???
```
**F#** (`fsharp/src/App.fs`)
```fsharp
// module Snake.App
let TICK_MS = 150L
let classicActions : Stream<Action> = (* ... *)
let states (seed: uint32) (actions: Stream<Action>) : Stream<GameState> = failwith "TODO m09"
let render (state: GameState) : IO<unit> = failwith "TODO m09"
let main () : IO<Stream<IO<unit>>> = failwith "TODO m09"
```
**Proved by:** `the game ticks every 150 ms`, `classicActions turns Tick into actions on the clock and keys into Turn actions`, `states starts with the new game, then yields one state per action`, `render draws the whole view of the state`, `main asks for a random seed, then builds the stream from it`, `the program built from classicActions draws the new game first, then the ticked game after 150 ms`, `main draws again once time has passed`.
**Watch out for:** the JS stubs of `classicActions` and `main` are placeholder objects so that the file loads; replace them with real values. In JS, import the two `map`s under different names. Each of these definitions is one line; if yours is longer, re-read sections 2–4.
### Exercise 9.3 — the runtime: `runIO` and `runApp`
**JavaScript** (`js/src/runtime.js` — `let` and loops allowed here)
```js
export const runIO = (io, platform) => /* the final value */
export const runApp = (main, platform) => /* undefined; registers callbacks */
```
**Scala** (`scala/src/Runtime.scala` — exactly one `var`)
```scala
object Runtime:
def runIO[A](io: IO[A], p: Platform): A = ???
def runApp(main: IO[Stream[IO[Unit]]], p: Platform): Unit = ???
```
**F#** (`fsharp/src/Runtime.fs` — one ref cell allowed here)
```fsharp
// module Snake.Runtime
let runIO (p: Platform) (io: IO<'a>) : 'a = failwith "TODO m09"
let runApp (p: Platform) (main: IO<Stream<IO<unit>>>) : unit = failwith "TODO m09"
```
**Proved by** (`m09-runtime`): `runIO of a pure value returns it without touching the platform`, `runIO maps Draw Rect to fillRect(x, y, w, h, color)`, `runIO maps Draw Text to fillText(x, y, text, color, size, align)`, `runIO answers RandomSeed with platform.randomSeed() and continues with it`, `runIO performs effects in order`, `runIO handles 100000 effects without overflowing the stack`, and the four `runApp …` tests from section 7.
**Watch out for:** registering the callbacks *before* the stream exists, or reading the variable in a callback without updating it. Each callback must step the *current* stream and store the next one. In Scala, `runIO` with an `Op.Draw` must match on the shape inside (`Shape.Rect` / `Shape.Text`).
### Exercise 9.4 (JS only) — `browserPlatform`
**JavaScript** (`js/src/browser.js`)
```js
export const browserPlatform = (canvas, win) => /* Platform */
```
Build a `Platform` from a canvas and a window: `fillRect`/`fillText` use `canvas.getContext('2d')` (font `` `${size}px monospace` ``, `textAlign` from `align`, `fillStyle` from `color`); `startClock` calls `onTime(t)` on every `win.requestAnimationFrame`, with `t` = milliseconds since the first frame; `onKeyDown`/`onKeyUp` listen on `win` and call `preventDefault()` for arrow keys and space so the page doesn't scroll; `randomSeed` returns `crypto.getRandomValues(new Uint32Array(1))[0]`. There is no automated test for this file; the test is that the game runs. Scala and F# students: read the given `BrowserPlatform` in your track and compare it with this description.
**Run:**
- JS: `cd js && node --test "test/m09-*.test.js"`, then `node scripts/serve.mjs` (from the repo root) and open <http://localhost:8000/js/>
- Scala: `cd scala && ./test.sh M09`, then `./build-web.sh`; from the repository root run `node scripts/serve.mjs` and open <http://localhost:8000/scala/>
- F#: `cd fsharp && ./test.sh M09`, then `./build-web.sh`; from the repository root run `node scripts/serve.mjs` and open <http://localhost:8000/fsharp/>
## Hints
<details><summary>Exercise 9.1 — nudge</summary>
A `switch` (JS) or `match` (Scala/F#) with several keys per case is the most readable. In JS, `case 'ArrowUp': case 'w': case 'W': return Action.Turn('Up')`, and `default: return null`. Why is a `switch` safe from the `constructor` trap and a plain object lookup isn't?
</details>
<details><summary>Exercise 9.2 — nudge</summary>
Each definition is a direct translation of a line in sections 2–4. Write them in the order `classicActions`, `states`, `render`, `main`, and run the tests after each one.
</details>
<details><summary>Exercise 9.2 — near-solution (try the others first)</summary>
**JavaScript**
```js
import { map as mapIO, randomSeed, drawScene } from './io.js'
import { interval, keyDowns, map as mapS, filterMap, merge, scan, startWith } from './stream.js'
export const classicActions = merge(mapS(interval(TICK_MS), () => /* ? */), filterMap(/* ? */, /* ? */))
export const states = (seed, actions) => startWith(scan(actions, /* ? */, /* ? */), /* ? */)
export const render = (state) => /* ? */
export const main = mapIO(randomSeed, (seed) => /* ? */)
```
**Scala**
```scala
lazy val classicActions: Stream[Action] =
Stream.interval(TICK_MS).map(_ => /* ? */).merge(Stream.keyDowns.filterMap(/* ? */))
def states(seed: Long, actions: Stream[Action]): Stream[GameState] =
actions.scan(/* ? */)(Game.update).startWith(/* ? */)
lazy val main: IO[Stream[IO[Unit]]] = IO.randomSeed.map(seed => /* ? */)
```
**F#**
```fsharp
let classicActions = Stream.merge (Stream.interval TICK_MS |> Stream.map (fun _ -> (* ? *))) (Stream.keyDowns |> Stream.choose (* ? *))
let states seed actions = actions |> Stream.scan (* ? *) (* ? *) |> Stream.startWith (* ? *)
let main () = IO.randomSeed () |> IO.map (fun seed -> (* ? *))
```
(F#'s `Game.update` takes `state` then `action`, which is the order `Stream.scan`'s folder wants.)
</details>
<details><summary>Exercise 9.3 — nudge</summary>
`runIO`: keep a "current program" variable. While it's an `Effect`, perform its op on the platform and replace the current program with `k(answer)`. When it's `Pure`, return the value. Write a small helper `perform(op, platform)` that returns the answer.
`runApp`: one variable for the stream, one helper `feed(event)`, three registrations. Which of the three registrations should come last, and does it matter?
</details>
<details><summary>Exercise 9.3 — near-solution (try the others first)</summary>
**JavaScript**
```js
export const runIO = (io, platform) => {
let current = io
while (current.tag === 'Effect') current = current.k(/* perform current.op, get the answer */)
return /* ? */
}
export const runApp = (main, platform) => {
let stream = /* ? */
const feed = (input) => {
const [ios, next] = /* ? */
stream = /* ? */
ios.forEach(/* ? */)
}
platform.onKeyDown((key) => feed({ tag: 'KeyDown', key }))
platform.onKeyUp(/* ? */)
platform.startClock(/* ? */)
}
```
**Scala**
```scala
@annotation.tailrec
def runIO[A](io: IO[A], p: Platform): A = io match
case IO.Pure(a) => a
case IO.Effect(op, k) => runIO(k(/* perform op on p, giving an OpResult */), p)
def runApp(main: IO[Stream[IO[Unit]]], p: Platform): Unit =
var stream = /* ? */
def feed(e: Event): Unit =
val (ios, next) = /* ? */
stream = next
/* ? */
p.onKeyDown(key => feed(Event.KeyDown(key)))
/* ... */
```
**F#**
```fsharp
let rec runIO (p: Platform) (io: IO<'a>) : 'a =
match io with
| Pure a -> a
| Effect (op, k) -> runIO p (k ((* perform op on p, giving an OpResult *))) // tail call: compiled to a loop
let runApp (p: Platform) (main: IO<Stream<IO<unit>>>) : unit =
let stream = ref ((* ? *)) // the one ref cell
let feed (e: Event) =
let ios, next = (* step stream.Value with e *)
stream.Value <- next
(* run every io in ios, in order *)
p.OnKeyDown (fun key -> feed (KeyDown key))
(* ... *)
```
</details>
## Check your understanding
1. Read `IO<Stream<IO<unit>>>` aloud and explain each layer. Why can't the outer `IO` be removed? Why isn't the inner one a `Scene`?
2. Trace one key press: the player presses `ArrowUp`. Name, in order, every function and value it passes through until pixels change. When does the snake actually turn on screen, and why not immediately?
3. One call `fireTime(150)` right after `runApp` produces two full drawings. Explain exactly which outputs of which streams cause each.
4. The runtime's only variable holds a *stream*, not a *game state*. What would be worse if it held the game state instead? Which code would have to move into the runtime?
5. Why must `runIO` be a loop while `trace` may be recursive? Is there a program for which even the loop overflows? Whose fault would it be?
6. What makes the runtime testable without a browser? What would you have to do to test it if it called `canvas.getContext('2d')` directly?
7. Count the impure lines in your track (runtime + browser glue + main). Compare with the number of pure lines. What does that ratio tell you?
8. `main` is defined at module load time. Prove to yourself that defining it performs no effect. (Hint: what does `trace(main, 1).ops` contain, and when does the platform's `randomSeed` get called?)
## In production
- **The Elm Architecture** is this module, packaged: you provide `init`, `update`, `view` and `subscriptions` (Elm's version of our input streams); the Elm runtime holds the model, feeds it messages and draws your view. You never write the runtime ([The Elm Architecture](https://guide.elm-lang.org/architecture/)). **Elmish** gives F# the same shape ([Elmish](https://elmish.github.io/elmish/)).
- **Redux** is the `scan(actions, update, initial)` part: a store holds the current state, and a pure reducer computes the next one for each dispatched action ([Redux fundamentals: state, actions and reducers](https://redux.js.org/tutorials/fundamentals/part-3-state-actions-reducers)).
- **cats-effect `IOApp`** and **ZIO's `ZIOAppDefault`** are the "one call at the edge": your `run` method returns an effect value, and the library's runtime performs it ([cats-effect](https://typelevel.org/cats-effect/), [ZIO](https://zio.dev/)). An fs2 application is literally an `IO` that runs a `Stream` of effects ([fs2](https://fs2.io/)).
- **"Functional core, imperative shell"** is Gary Bernhardt's name for this architecture: keep decisions in pure functions and push the effects to a thin shell ([Functional Core, Imperative Shell](https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell)). Our shell is thinner than usual: even the *wiring* is pure, only the interpreter isn't.
## Further reading
- [The Elm Architecture](https://guide.elm-lang.org/architecture/) — compare `init`/`update`/`view`/`subscriptions` with `newGame`/`update`/`view`/`classicActions`.
- [Gary Bernhardt, "Functional Core, Imperative Shell"](https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell) — the architecture you just built, explained in 13 minutes.
- [MDN: `requestAnimationFrame`](https://developer.mozilla.org/en-US/docs/Web/API/Window/requestAnimationFrame) — where the browser platform's clock comes from.
- [MDN: `Crypto.getRandomValues`](https://developer.mozilla.org/en-US/docs/Web/API/Crypto/getRandomValues) — where the seed comes from.
- [Martin Fowler, "Test Double"](https://martinfowler.com/bliki/TestDouble.html) — the vocabulary for fakes, stubs and mocks; our fake platform is a "fake".
- [Scala.js](https://www.scala-js.org/) and [Fable](https://fable.io/) — how the Scala and F# tracks run in the browser.