Files
functional-programming-snake/course/08-event-streams.md
T
2026-10-29 01:00:23 +01:00

506 lines
29 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 8 — Event streams
> **Time:** ~4 h · **You'll edit:** JS `js/src/stream.js` · Scala `scala/src/Stream.scala` · F# `fsharp/src/Stream.fs`
> **Tests:** JS `cd js && node --test "test/m08-*.test.js"` · Scala `cd scala && ./test.sh M08` · F# `cd fsharp && ./test.sh M08`
> **Prerequisites:** Modules 0–7 (especially Module 5, "a game is a fold", and Module 7, "effects are values")
## Why this module
In Module 5 you showed that a whole game is a fold: `replay(newGame(seed), actions)`. That works beautifully when you have the list of actions *in advance* — in a test, or for a replay. But a real player doesn't hand you a list. Actions arrive **one at a time, over time**: the clock ticks, a key goes down, another key, more ticks. Where do they come from, and how do they become `Action`s?
The usual answer is callbacks and a mutable variable:
```js
// The usual, stateful way (do NOT write this in the course)
let state = newGame(seed)
setInterval(() => { state = update(state, Action.Tick); draw(ctx, state) }, 150)
window.addEventListener('keydown', (e) => {
const action = keyToAction(e.key)
if (action) { state = update(state, action); draw(ctx, state) }
})
```
This works, and it is where most games start. But look at what we've lost. The logic "every 150 ms is a tick; arrow keys are turns; fold them into states; draw each state" is scattered across two callbacks that share a hidden `let`. You can't test the wiring without a browser and a real clock. You can't ask "what would happen if these five events arrived?" without actually making them happen.
This module does for *events over time* what Module 7 did for effects: it turns them into **values**. A **stream** will be a pure description of how to react to events. You'll build a small library of streams and combinators (`map`, `filter`, `merge`, `scan`, …) from scratch, and you'll test any wiring by feeding it a list of events — no browser, no clock, no waiting.
## Learning objectives
By the end of this module you can:
- **explain** a stream as a pure automaton (a Mealy machine): `step : Event → (outputs, nextStream)`;
- **implement** sources (`keyDowns`, `keyUps`, `interval`) and combinators (`map`, `filter`, `filterMap`/`choose`, `merge`, `scan`, `startWith`) and a pure `run`;
- **explain** where a stream "remembers" things when there are no variables, and **trace** `interval`'s `last` through a sequence of events;
- **predict** the output of a small stream pipeline for a given list of events;
- **compare** this design with RxJS observables (push-based, stateful subscriptions) and with classic functional reactive programming.
## Concepts
### 1. What a program receives from the outside world
Our game only reacts to three kinds of event:
- **time passing**: "it is now `t` milliseconds since the program started";
- **a key going down**: which key (`'ArrowUp'`, `'w'`, `' '` …);
- **a key going up**: which key.
As in every module so far, the first step is to make them data:
**JavaScript**
```js
// An input event is a plain object:
// { tag: 'Time', t } t: milliseconds since start
// { tag: 'KeyDown', key } key: the browser's KeyboardEvent.key, e.g. 'ArrowUp'
// { tag: 'KeyUp', key }
```
**Scala**
```scala
// Already in scala/src/Stream.scala
enum Event:
case Time(t: Long)
case KeyDown(key: String)
case KeyUp(key: String)
```
**F#**
```fsharp
// Already in fsharp/src/Stream.fs
type Event =
| Time of int64
| KeyDown of string
| KeyUp of string
```
**Naming note.** The JS doc comments call this type `Input`; Scala and F# call it `Event`, because their m09 module is already named `Input` and the two names would clash. Same thing.
Notice that time arrives as an event too. The program never *asks* for the time (`Date.now()` is banned by the purity audit); the outside world *tells* it what time it is, the same way it tells it about keys. Hidden time becomes an explicit input. Keep that in mind; it's the most important design decision in this module.
### 2. A stream is a machine you feed one event at a time
What is the smallest useful description of "something that reacts to events"? When an event arrives, it should be able to:
1. produce **zero or more outputs** (a key press produces one turn; a clock event may produce no tick at all, or several);
2. **change how it will react to the next event** (an interval needs to remember when it last fired).
Without variables, "change" must mean "give back a *new* machine". That gives the whole definition:
```
Stream<A> = { step : Event → [ A[], Stream<A> ] }
```
Feed one event to `step`; get back the outputs it caused *and the stream to use for the next event*.
**JavaScript**
```js
// Stream<A> = { step: (event) => [outputs: A[], next: Stream<A>] }
```
**Scala**
```scala
final case class Stream[A](step: Event => (List[A], Stream[A]))
```
**F#**
```fsharp
type Stream<'a> = Stream of (Event -> 'a list * Stream<'a>)
```
This is a well-known object in computer science: a **Mealy machine**, a state machine whose output depends on its current state and its input ([Mealy machine](https://en.wikipedia.org/wiki/Mealy_machine)). The usual textbook definition has a set of states and a transition table; ours is the same idea where "the current state" is hidden inside the closure of `step`, and "the next state" is the next stream itself. In Haskell this is also called an *automaton arrow* or a "Mealy" type ([Justin Le, "Intro to Machines & Arrows"](https://blog.jle.im/entry/intro-to-machines-arrows-part-1-stream-and.html) is a gentle introduction).
> **From Scala:** this is a recursive case class whose only field is a function. Think of it as the dual of a lazy list: a `LazyList` *gives* you the next element when you ask; a `Stream` *takes* an event when you push one, and gives you some outputs plus the rest of itself.
Let's write one by hand to get a feel for it — not one of the exercises, but a cousin: a stream that counts key presses and emits the running count.
```js
// counter(n): a stream that has seen n key presses so far.
const counter = (n) => ({
step: (ev) =>
ev.tag === 'KeyDown'
? [[n + 1], counter(n + 1)] // output the new count; the next machine remembers n + 1
: [[], counter(n)], // no output; nothing to remember changes
})
const presses = counter(0)
```
Step it by hand:
```
presses = counter(0)
presses.step(KeyDown 'a') → [ [1], counter(1) ]
counter(1).step(Time 40) → [ [], counter(1) ]
counter(1).step(KeyDown 'b')→ [ [2], counter(2) ]
```
Where is the count stored? Not in a variable. It's the argument `n` of the function that built the current stream, captured in the closure of `step`. Each step returns a new stream that captures the new count. **The state lives in the next stream**, not in a cell that gets overwritten.
### 3. No hidden state: run the same stream twice
To test a stream, we need a function that feeds it a list of events and collects everything it emits. That's `run`:
```
run(s, events) → all outputs, in order
```
It is a **fold** over the events — exactly like `replay` in Module 5, except that each step also produces outputs, which you append. The accumulator is the pair `(outputs so far, current stream)`.
Because `step` never mutates anything, a stream *value* is not "used up" by running it. The test suite checks precisely this:
```js
test('m08 running the same stream value twice gives the same outputs', () => {
const s = scan(merge(interval(10), map(keyDowns, () => 100)), (a, x) => a + x, 0)
const xs = inputs(time(25), down('a'), time(40))
assert.deepStrictEqual(run(s, xs), run(s, xs))
assert.deepStrictEqual(run(s, xs), [10, 30, 130, 160, 200])
})
```
Take a minute and work out by hand why the second list is `[10, 30, 130, 160, 200]` (you'll be able to once you've read the sections on `interval`, `merge` and `scan`). Then think about why the *first* assertion is remarkable. In most event libraries, a stream object holds internal state that changes as events flow; running it a second time would continue where it left off, or not work at all. Here, `s` is a value like the number 3: you can use it as often as you like.
### 4. Sources: where outputs come from
**`keyDowns`** emits the `key` of every `KeyDown` event and ignores everything else. **`keyUps`** does the same for `KeyUp`. These two remember nothing, so the "next stream" can be the very same stream. (Ask yourself: is that safe? Why can a stateless stream return *itself*?)
**`interval(ms)`** is the interesting one. It turns the clock into regular beats: on `Time t`, it emits every multiple of `ms` that has been *crossed* since the last time event — every `k*ms` with `last < k*ms ≤ t` — in ascending order. It starts with `last = 0` and afterwards `last` becomes `t`. The values it emits are those multiples themselves.
Why "every multiple crossed" rather than "one beat per time event"? Because the outside world does not call us on a perfect schedule. The browser reports time about 60 times per second, but if the tab was in the background for two seconds, the next time event might jump from `t = 1000` to `t = 3000`. The game should then catch up on all the ticks it missed, not silently skip them. (This idea, separating game time from the rate at which the outside world calls you, is the classic "fixed timestep" pattern; see [Game Programming Patterns: Game Loop](https://gameprogrammingpatterns.com/game-loop.html).)
Here is how `last` threads through a sequence of events for `interval(10)`:
```
event last before multiples k*10 in (last, t] outputs last after
────────── ─────────── ───────────────────────────── ───────── ──────────
Time 5 0 (0, 5] → none [] 5
Time 10 5 (5, 10] → 10 [10] 10
KeyDown 'a' 10 (ignored) [] 10
Time 35 10 (10, 35] → 20, 30 [20, 30] 35
Time 38 35 (35, 38] → none [] 38
Time 41 38 (38, 41] → 40 [40] 41
```
And as streams: each row hands its "last after" to the stream for the next row.
```
interval(10) = from(10, last=0) ──Time 5──▶ from(10, 5) ──Time 10──▶ from(10, 10) ──KeyDown──▶ from(10, 10) ──Time 35──▶ from(10, 35) ...
[] [10] [] [20, 30]
```
The stub docs tell you how: "write a private helper that carries `last`". `interval(ms)` itself is just that helper started at `last = 0`. Before you code it, answer: what are the outputs of `run(interval(10), [Time 35])`? (Answer: `[10, 20, 30]`; one big jump catches up on everything.)
> **From Scala:** the helper is the familiar "loop with an accumulator" from recursive functions, e.g. `def go(acc: Int, xs: List[Int])`. Here the accumulator is carried from step to step *between events*, by building the next stream with the new value.
### 5. Combinators: building streams from streams
Each combinator takes a stream (or two) and gives a new stream. They all follow the same pattern, and once you see it, you'll be able to write any of them:
> **On each event:** step the inner stream(s); transform the outputs; and return, as the next stream, **the same combinator applied to the inner stream(s)' next stream(s)**.
That last part is what people get wrong at first. If `map(s, f)` returned `s.next` as its next stream, the mapping would be forgotten after one event. The next stream must again be "mapped", so it's `map(next, f)`.
Here is what each one does. Pay attention to the order of outputs; the tests do.
- **`map(s, f)`**: apply `f` to every output.
- **`filter(s, p)`**: keep the outputs for which `p` is true.
- **`filterMap(s, f)`**: apply `f`, which returns either a value or "nothing", and keep only the values. In JS "nothing" is `null`; in Scala `f` returns an `Option` (`Some(b)` / `None`); in F# it's an option, and the function is called **`choose`** like `List.choose`. It's the combination of `map` and `filter` you'll need for `keyToAction` in Module 9 (most keys mean nothing).
- **`merge(a, b)`**: *both* streams step on *every* event; the outputs of `a` come before the outputs of `b`. Both next streams are kept.
- **`scan(s, f, z)`**: fold the outputs into an accumulator starting at `z`, and emit **each new accumulator** (not `z` itself). If one event causes three outputs, `scan` emits three accumulators. The latest accumulator must be remembered for the next event — where? (Same answer as for `counter` and `interval`.)
- **`startWith(s, v)`**: emit `v` once, *before* the outputs of the first step; afterwards behave exactly like the inner stream.
A small worked example to check your understanding of `merge` and ordering:
```
merge(interval(10), map(keyDowns, () => -1)) events: Time 10, KeyDown 'a', Time 20
Time 10 → interval: [10] keys: [] → [10]
KeyDown 'a' → interval: [] keys: [-1] → [-1]
Time 20 → interval: [20] keys: [] → [20]
run → [10, -1, 20]
```
And `scan`, accumulating `+`:
```
scan(keyDowns, (acc, k) => acc + k, '') events: KeyDown 'a', KeyDown 'b'
KeyDown 'a' → outputs ['a'] → acc '' + 'a' = 'a' → emits ['a']; next remembers 'a'
KeyDown 'b' → outputs ['b'] → acc 'a' + 'b' = 'ab' → emits ['ab']; next remembers 'ab'
run → ['a', 'ab']
```
> **From Scala:** these are the methods you know from `List` (`map`, `filter`, `collect`/`flatMap` with `Option`, `scanLeft`), lifted to "lists that haven't happened yet". `scan` is `scanLeft` without the initial element. In Scala they are **methods**: `s.map(f)`, `s.merge(other)`, and `s.scan(z)(f)` with the initial value in its own parameter list, like `foldLeft`. In F# they are data-last module functions: `Stream.map f s`, `Stream.scan f z s`, `Stream.startWith v s`, `Stream.run events s`.
### 6. `startWith` and the "nothing happens until an event arrives" rule
`startWith` has one subtle property worth understanding now, because Module 9 depends on it. A stream can only emit when an event arrives; there is no "at time zero, before anything" moment. So `startWith(s, v)` emits `v` **at the first event**, whatever it is, together with (before) that event's own outputs:
```
run(startWith(keyDowns, 'init'), [Time 1, KeyDown 'a']) → ['init', 'a'] (emitted at Time 1)
run(startWith(keyDowns, 'init'), []) → [] (no event, no output)
```
In the game, `startWith` will put the initial game state at the front of the stream of states, so the very first clock event draws the board before anything has moved.
### 7. Two ways to think about streams: pull and push
There are two classic implementation strategies for event streams, and it's worth knowing which one we chose and why.
**Push (observables, RxJS).** A source *pushes* values to subscribers by calling their callbacks. `fromEvent(window, 'keydown').pipe(map(...), filter(...)).subscribe(cb)` builds a chain of objects, each of which holds a list of subscribers and its own internal state (a `scan` operator holds its accumulator in a field). Subscribing *starts* the machinery; you get a `Subscription` to stop it. It's powerful and practical — this is how [RxJS](https://rxjs.dev/) works — but the stream objects are stateful: you can't run "the same" stream twice and get a fresh run without care ("hot" vs "cold" observables), and testing needs special schedulers that fake time.
**Pure automata (our design).** A stream is a value with a `step` function. Nobody subscribes; the *caller* pushes one event in and gets outputs back, plus the next stream. All the state is in the returned values. The price is that someone must hold "the current stream" between events and call `step` — that's the one mutable variable you'll write in the runtime in Module 9. The reward is that everything else is pure, and testing is `run(s, listOfEvents)`.
| | RxJS observable | Our `Stream` |
|---|---|---|
| What is a stream? | an object with subscribers and internal state | a pure function `Event → (outputs, next)` |
| Where is a `scan`'s accumulator? | a field inside the operator, mutated | captured in the *next* stream value |
| Run it twice | depends (hot/cold); usually not "fresh" | always the same result |
| Test it | marble tests with a virtual scheduler | `run(s, events)` and `deepEqual` |
| Who drives it? | the source, via callbacks | the runtime, one `step` at a time |
### 8. Where this comes from: functional reactive programming
The idea of describing interactive programs as pure values that change over time is called **functional reactive programming (FRP)**. It was introduced by Conal Elliott and Paul Hudak in ["Functional Reactive Animation"](http://conal.net/papers/icfp97/) (ICFP 1997), which modelled animations as *behaviours* (values that vary continuously with time, `Time → a`) and *events* (streams of timestamped occurrences). Our streams are a discrete, simplified version of FRP events: we only see time at the moments the outside world reports it.
Later work in Haskell, such as [Yampa](https://wiki.haskell.org/Yampa) (used for games, including a Haskell Quake clone), represents reactive programs as *signal functions*, which are exactly automata stepped by the runtime — very close to what you're building here. Elm started as an FRP language and later moved to the simpler model of *subscriptions* that deliver messages to `update` ([Elm guide: Time](https://guide.elm-lang.org/effects/time.html)) — which, as you'll see in Module 9, is what our stream of actions folded with `scan` amounts to.
## Exercises
Do them in this order; later ones use earlier ones (`filterMap` can be written using `map` and `filter`; the tests for everything use `run`, so write `run` early).
### Exercise 8.1 — `run`
**JavaScript**
```js
export const run = (s, inputs) => /* A[]: every output, in order */
```
**Scala**
```scala
def run(events: List[Event]): List[A] = ??? // a method on Stream[A]: s.run(events)
```
**F#**
```fsharp
let run (events: Event list) (Stream step: Stream<'a>) : 'a list = failwith "TODO m08"
```
A pure fold over the events (no `let` reassignments, no `push`). **Proved by:** `run of no inputs is empty` (Scala/F#: `run of no events is empty`), and every other test.
### Exercise 8.2 — sources: `keyDowns`, `keyUps`, `interval`
**JavaScript**
```js
export const keyDowns = /* Stream<string> */
export const keyUps = /* Stream<string> */
export const interval = (ms) => /* Stream<number> */
```
**Scala** (in `object Stream`)
```scala
lazy val keyDowns: Stream[String] = ???
lazy val keyUps: Stream[String] = ???
def interval(ms: Long): Stream[Long] = ???
```
**F#**
```fsharp
let keyDowns : Stream<string> = (* ... *)
let keyUps : Stream<string> = (* ... *)
let interval (ms: int64) : Stream<int64> = failwith "TODO m08"
```
**Proved by:** `keyDowns emits keys of KeyDown inputs only`, `keyUps emits keys of KeyUp inputs only`, `interval emits each multiple once as time passes`, `interval emits several multiples after one big time jump`, `interval ignores key inputs`.
**Watch out for:** a stream that refers to itself (`keyDowns`'s next stream is `keyDowns`). In JS a `const` object can mention itself inside a function body, because the body only runs later. In Scala that's why these are `lazy val`s. In F# a value can't refer to itself directly; write a small recursive function (`let rec`) that builds the stream and apply it. For `interval`, the private helper takes `ms` and `last`. Counting the multiples: the first multiple strictly after `last` is `(floor(last / ms) + 1) * ms`, and the last one not after `t` is `floor(t / ms) * ms`.
### Exercise 8.3 — `map`, `filter`, `filterMap`
**JavaScript**
```js
export const map = (s, f) => /* Stream<B> */
export const filter = (s, p) => /* Stream<A> */
export const filterMap = (s, f) => /* Stream<B>; f returns a value or null */
```
**Scala** (methods)
```scala
def map[B](f: A => B): Stream[B] = ???
def filter(p: A => Boolean): Stream[A] = ???
def filterMap[B](f: A => Option[B]): Stream[B] = ???
```
**F#** (`choose` is F#'s name for `filterMap`)
```fsharp
let rec map (f: 'a -> 'b) (s: Stream<'a>) : Stream<'b> = failwith "TODO m08"
let rec filter (p: 'a -> bool) (s: Stream<'a>) : Stream<'a> = failwith "TODO m08"
let choose (f: 'a -> 'b option) (s: Stream<'a>) : Stream<'b> = failwith "TODO m08"
```
**Proved by:** `map transforms every output`, `filter keeps outputs satisfying the predicate`, `filterMap transforms outputs and drops nulls` (Scala/F#: `... drops Nones`).
### Exercise 8.4 — `merge`, `scan`, `startWith`
**JavaScript**
```js
export const merge = (a, b) => /* Stream<A> */
export const scan = (s, f, z) => /* Stream<B>; f(acc, output) */
export const startWith = (s, v) => /* Stream<A> */
```
**Scala** (methods)
```scala
def merge(other: Stream[A]): Stream[A] = ???
def scan[B](z: B)(f: (B, A) => B): Stream[B] = ???
def startWith(v: A): Stream[A] = ???
```
**F#**
```fsharp
let rec merge (a: Stream<'a>) (b: Stream<'a>) : Stream<'a> = failwith "TODO m08"
let rec scan (f: 'b -> 'a -> 'b) (z: 'b) (s: Stream<'a>) : Stream<'b> = failwith "TODO m08"
let startWith (v: 'a) (s: Stream<'a>) : Stream<'a> = failwith "TODO m08"
```
**Proved by:** `merge puts outputs of the first stream before the second at the same input`, `merge interleaves sources by input order`, `scan emits each new accumulator, not the initial value`, `scan passes the output to the folding function`, `startWith emits the value once, before the outputs of the first step`, `startWith emits at the first input even if that step has no outputs`, `startWith emits nothing when no input ever arrives`, and `running the same stream value twice gives the same outputs`.
**Watch out for:** in `scan`, one event can produce several outputs, so you fold *those* outputs too, emitting every intermediate accumulator, and the next stream remembers the *last* accumulator (or the old one if there were no outputs). In `startWith`, only the first step is special — what should the next stream be so that `v` is never emitted again?
**Run (all of Module 8):**
- JS: `cd js && node --test "test/m08-*.test.js"`
- Scala: `cd scala && ./test.sh M08`
- F#: `cd fsharp && ./test.sh M08`
## Hints
<details><summary>Exercise 8.1 — nudge</summary>
`run` is `replay` from Module 5 with outputs: fold over the events with an accumulator that holds two things, the outputs so far and the current stream. At the end, return only the outputs.
</details>
<details><summary>Exercise 8.1 — near-solution (try the others first)</summary>
```js
export const run = (s, inputs) =>
inputs.reduce(
([outs, stream], i) => {
const [out, next] = /* ? */
return [/* ? */, next]
},
[[], s],
)[0]
```
Scala: `events.foldLeft((List.empty[A], this)) { case ((outs, s), e) => ... }._1`. F#: `List.fold` with a tuple accumulator, then `fst`.
</details>
<details><summary>Exercise 8.2 — nudge</summary>
`keyDowns` looks at the event's tag: one output for `KeyDown`, none otherwise; next stream: itself. For `interval`, write `from(ms, last)` returning a stream; on `Time t` it emits the multiples in `(last, t]` and returns `from(ms, t)`; on keys it emits nothing and returns a stream that still remembers the same `last`.
</details>
<details><summary>Exercise 8.2 — stronger hint</summary>
To list the multiples without a loop: compute `first = floor(last / ms) + 1` and `count = max(0, floor(t / ms) - first + 1)`; the outputs are `(first + n) * ms` for `n` from `0` to `count - 1`. In JS `Array.from({ length: count }, (_, n) => ...)`; in Scala `(first to floor(t/ms)).map(_ * ms).toList` (Long arithmetic); in F# a list expression `[ first .. t / ms ] |> List.map (fun k -> k * ms)` (integer division on `int64` already floors for non-negative numbers).
</details>
<details><summary>Exercise 8.3 / 8.4 — nudge</summary>
All combinators have the same shape: step the inner stream(s), compute the outputs, and return the **same combinator** around the inner next stream(s). Write `map` first and copy its shape. `filterMap` can be built from `map` and `filter` without writing a `step` at all.
</details>
<details><summary>Exercise 8.4 — stronger hint (scan and startWith)</summary>
For `scan`: from the inner outputs `[o1, o2, o3]` and accumulator `z`, you need `[f(z,o1), f(f(z,o1),o2), …]` — that's Scala's `scanLeft` without its first element, which you can also write as a `reduce` that builds the list of accumulators. The next stream is `scan(next, f, lastAcc)` where `lastAcc` is the last emitted accumulator, or `z` if nothing was emitted.
For `startWith`: on the first step, step the inner stream, put `v` in front of its outputs, and return the inner **next** stream unchanged, without wrapping it again.
</details>
<details><summary>Exercise 8.3 / 8.4 — near-solution (try the others first)</summary>
**JavaScript**
```js
export const map = (s, f) => ({
step: (i) => {
const [out, next] = s.step(i)
return [/* ? */, map(/* ? */, f)]
},
})
export const merge = (a, b) => ({
step: (i) => {
const [outA, nextA] = a.step(i)
const [outB, nextB] = /* ? */
return [/* ? */, /* ? */]
},
})
```
**Scala**
```scala
def map[B](f: A => B): Stream[B] = Stream { e =>
val (out, next) = step(e)
(/* ? */, next.map(f))
}
```
**F#**
```fsharp
let rec map (f: 'a -> 'b) (Stream step) : Stream<'b> =
Stream (fun e ->
let out, next = step e
((* ? *), map f next))
```
</details>
## Check your understanding
1. Where does `interval` keep `last`? Where does `scan` keep its accumulator? Neither uses a variable; answer precisely.
2. Predict `run(interval(100), [Time 50, Time 250, KeyDown 'x', Time 300, Time 300])`. Why does the second `Time 300` emit nothing?
3. Predict `run(startWith(scan(keyDowns, (n, _) => n + 1, 0), 0), [Time 5, KeyDown 'a', KeyDown 'b'])`. (This exact shape, `startWith(scan(...))`, is how Module 9 builds the stream of game states.)
4. Why does `map(s, f)` have to return `map(next, f)` and not just `next`? Write down what `run` would produce with the wrong version on two key presses.
5. The test "running the same stream value twice gives the same outputs" would fail for a typical RxJS pipeline with a `scan`. Explain why in terms of where the accumulator lives.
6. `merge(a, b)` steps *both* streams on *every* event. What would go wrong with an `interval` inside `b` if `merge` only stepped `b` on events that `a` ignored?
7. The browser reports time about 60 times per second, and the snake ticks every 150 ms. Using `interval`, explain what happens if the browser stops calling us for 1 second (say, the tab was hidden) and then resumes. Is that what you want for this game?
8. Our streams never ask what time it is. Why is that important for testing? Where *will* the time come from?
## In production
- **RxJS** is the most widely used stream library in JavaScript: `fromEvent`, `interval`, `map`, `filter`, `merge`, `scan`, `startWith` all exist with the same meaning as ours, but as push-based observables with subscriptions and internal state ([RxJS](https://rxjs.dev/), [operators](https://rxjs.dev/guide/operators)).
- **fs2** (Scala) builds pure, pull-based streams on top of cats-effect: a `Stream[IO, A]` is a description that does nothing until compiled and run, and can be run any number of times ([fs2](https://fs2.io/)). It's the industrial version of our "a stream is a value" idea.
- **Elm subscriptions** turn the clock and the keyboard into messages delivered to `update` ([Elm guide: Time](https://guide.elm-lang.org/effects/time.html)); **Elmish** offers the same for F# ([Elmish](https://elmish.github.io/elmish/)).
- **Yampa** and other arrowized-FRP libraries in Haskell represent reactive systems as signal functions stepped by a driver — the same automaton idea, used for games and robotics ([Yampa](https://wiki.haskell.org/Yampa)).
## Further reading
- [Conal Elliott & Paul Hudak, "Functional Reactive Animation"](http://conal.net/papers/icfp97/) — the paper that started FRP; read sections 1–2 for the vision of interaction as pure values over time.
- [Mealy machine (Wikipedia)](https://en.wikipedia.org/wiki/Mealy_machine) — the formal definition of what each of our streams is.
- [Justin Le, "Intro to Machines & Arrows"](https://blog.jle.im/entry/intro-to-machines-arrows-part-1-stream-and.html) — builds the same `step`-returns-next-machine type in Haskell and shows how far it goes.
- [Functional reactive programming (Wikipedia)](https://en.wikipedia.org/wiki/Functional_reactive_programming) — an overview of the family of ideas and implementations.
- [Robert Nystrom, Game Programming Patterns: Game Loop](https://gameprogrammingpatterns.com/game-loop.html) — why games decouple game time from the rate at which the outside world calls them; the reason `interval` catches up.
- [RxJS: Overview](https://rxjs.dev/guide/overview) — the push-based alternative, to compare with what you built.