docs(course): replace Scala bridges with general programming in 06–11, method, reference

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
Lucas Winther
2026-10-29 01:55:36 +01:00
co-authored by Claude Opus 5.5
parent b46a3611af
commit 9891bb596d
5 changed files with 27 additions and 17 deletions
+6 -4
View File
@@ -114,7 +114,7 @@ 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.
> **From everyday programming:** compare an event listener. In the [observer pattern](https://refactoring.guru/design-patterns/observer) — `addEventListener('keydown', handler)` is an instance of it — a handler is called once per event and typically reacts by changing some variable that lives outside it. A stream's `step` is also called once per event, but instead of changing anything it *returns* what happened (the outputs) and the handler to use for the next event. You may also have written a state machine as a `switch` over a `currentState` variable that each event reassigns. A stream is that state machine with the variable removed: each state is a stream, and the transition is "return the next one". Unlike an iterator, which *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.
@@ -193,7 +193,7 @@ interval(10) = from(10, last=0) ──Time 5──▶ from(10, 5) ──Time 10
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.
> **From everyday programming:** in a loop you would keep `last` in a variable declared outside the loop body, `let last = 0; for (const ev of events) { …; last = ev.t }`, so that each iteration sees what the previous one left behind. The helper does the same job without the variable: it takes `last` as a parameter, and each step builds the next stream by calling the helper again with the new value. The accumulator is carried from step to step *between events*, inside the next stream.
### 5. Combinators: building streams from streams
@@ -231,7 +231,9 @@ scan(keyDowns, (acc, k) => acc + k, '') events: KeyDown 'a', KeyDown 'b'
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`.
> **From everyday programming:** `map` and `filter` are the array methods of the same names — or, in SQL terms, the expression list of a `SELECT` and the `WHERE` clause — applied to a list that hasn't happened yet: the outputs are transformed or dropped as each event arrives, instead of all at once. `scan` is a `reduce` that reports every intermediate result instead of only the last one, like the running-balance column of a bank statement, or a spreadsheet column where each cell is "the cell above plus the amount on this row". `[3, 4, 5].reduce((a, x) => a + x, 0)` gives `12`; scanning the same list gives `[3, 7, 12]`.
**Language note.** In the Scala track these 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
@@ -435,7 +437,7 @@ All combinators have the same shape: step the inner stream(s), compute the outpu
<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 `scan`: from the inner outputs `[o1, o2, o3]` and accumulator `z`, you need `[f(z,o1), f(f(z,o1),o2), …]` — that's a running total, which you can write as a `reduce` whose accumulator is the list of accumulators so far (or, in the Scala track, as `scanLeft` without its first element). 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.