# 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>> ``` "`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>>` 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 turns = filterMap(keyDowns, keyToAction) Stream classicActions = merge(ticks, turns) Stream ``` Read it as a diagram: ``` events ──┬──▶ interval(150) ──map(→Tick)──────────────┐ │ ├──merge──▶ classicActions : Stream └──▶ 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: 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 ``` Mapping `render` over the states gives a stream of drawing programs, `Stream>`. 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 …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>`). 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 . (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 . **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 . 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 */ export const states = (seed, actions) => /* Stream */ export const render = (state) => /* IO */ export const main = /* IO>> */ ``` **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 = (* ... *) let states (seed: uint32) (actions: Stream) : Stream = failwith "TODO m09" let render (state: GameState) : IO = failwith "TODO m09" let main () : IO>> = 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>>) : 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 - Scala: `cd scala && ./test.sh M09`, then `./build-web.sh`; from the repository root run `node scripts/serve.mjs` and open - F#: `cd fsharp && ./test.sh M09`, then `./build-web.sh`; from the repository root run `node scripts/serve.mjs` and open ## Hints
Exercise 9.1 — nudge 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?
Exercise 9.2 — nudge 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.
Exercise 9.2 — near-solution (try the others first) **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.)
Exercise 9.3 — nudge `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?
Exercise 9.3 — near-solution (try the others first) **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>>) : 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)) (* ... *) ```
## Check your understanding 1. Read `IO>>` 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.