docs(course): replace Scala bridges with general programming in 00–05, README, mentor, spec

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas WintherandClaude Opus 5.5 committed 2026-10-29 01:57:10 +01:00
1 parent 9891bb596d
commit d8828e1b6a
11 files changed
+109 -54

No files matched your search

+47 -14
View File
@@ -23,7 +23,7 @@ function step() {
`step()` takes no arguments and returns nothing. Everything it does is a change somewhere else. To know what it does you need to know the current value of `snake`, `apple`, `score`, what `randomFreeCell()` will return this time, and who else is holding a reference to `snake.body`. To test it you have to set up globals, call it, and inspect the globals afterwards. To replay a game you would have to record every random number.
The fix is not a library. It is two habits you already practised in Scala: **write functions whose result depends only on their arguments**, and **never change data; build new data instead**. This module makes those habits precise, shows how they look in JavaScript (which does not enforce them) and in F#, and gives you a few small tools you will use in every later module.
The fix is not a library. It is two habits: **write functions whose result depends only on their arguments**, and **never change data; build new data instead**. This module makes those habits precise, shows how they look in JavaScript (which does not enforce them), Scala and F#, introduces the tools that replace loops (`map`, `filter`, `reduce`) and gives you a few small helpers you will use in every later module.
## Learning objectives
@@ -32,6 +32,7 @@ After this module you can:
- **explain** referential transparency and decide whether a given expression has it;
- **distinguish** a constant binding (`const`, `val`, `let`) from immutable data, and shallow from deep freezing;
- **update** records and arrays without mutation in all three languages (spread, `copy`, `with`);
- **replace** a loop with `map`, `filter` and `reduce` (a fold), and pass functions as arguments;
- **implement** small list helpers (`range`, `dropLast`, `contains`) without loops or reassignment;
- **compose** functions (JS `pipe`/`compose`, Scala `andThen`/`compose`, F# `|>`/`>>`) and predict the order in which they apply.
@@ -62,9 +63,11 @@ const y = next() + next() // 1 + 2 = 3
1. **Same arguments, same result.** The result depends on nothing except the arguments (no globals, no clock, no randomness, no files).
2. **No observable effects.** Calling it changes nothing the rest of the program can see (no mutation of its arguments or globals, no printing, no drawing).
Why care? Because a pure function can be understood by looking at it alone. You can test it by calling it with inputs and checking the output. You can call it twice, or zero times, or cache the result, and nothing breaks. This is the *substitution model* of evaluation you may know from your Scala course: you evaluate a program by replacing names with their definitions, like algebra. It only works when everything is referentially transparent.
Why care? Because a pure function can be understood by looking at it alone. You can test it by calling it with inputs and checking the output. You can call it twice, or zero times, or cache the result, and nothing breaks. This is the *substitution model* of evaluation: you evaluate a program by replacing names with their definitions, like algebra (`add(2, 3) + add(2, 3)` becomes `5 + 5` becomes `10`). It only works when everything is referentially transparent.
> **From Scala:** in Scala you mostly worked with `val` and immutable `List`, so most of your functions were pure without you having to think about it. The compiler does not check purity in Scala, JavaScript or F#. In all three it is a discipline. In this course the tests and the purity audit help you keep it.
> **From everyday programming:** think of a unit test for a method that reads the clock, a global singleton or a database. Before you can test it you need mocks, fakes or dependency injection to take control of those hidden inputs. A pure function has no hidden inputs: everything it depends on is an argument, so the test passes the inputs and checks the output, nothing more. A spreadsheet is another familiar example: a cell such as `=A1 * 2` is a pure function of other cells, which is why the spreadsheet can recompute any cell at any time and always get the same answer.
No compiler checks purity for you, in JavaScript, Scala or F#. In all three it is a discipline. In this course the tests and the purity audit help you keep it.
### Exercise in prose: pure or not?
@@ -114,7 +117,9 @@ So there are two different things:
| Scala `val` + `List`, case class | no | no |
| F# `let` + list, record | no | no |
> **From Scala:** Scala separates these too. `val buf = ListBuffer(1)` cannot be reassigned, but `buf += 2` still mutates it. Immutability comes from the *data type* (`List`, case classes), not from `val`. F# is like Scala here, only stricter: `let` bindings cannot be reassigned at all unless declared `let mutable`, and lists and records are immutable.
> **From everyday programming:** Java's `final` and C#'s `readonly` behave like `const`. `final List<String> names = new ArrayList<>()` cannot be made to point at another list, but `names.add("Ada")` still changes the list it points at. Immutability comes from the *data type*, not from the keyword on the name.
The same holds in the other tracks. In Scala, `val buf = ListBuffer(1)` cannot be reassigned, but `buf += 2` still mutates it; the immutable `List` and case classes are what make the data unchangeable. F# is stricter: `let` bindings cannot be reassigned at all unless declared `let mutable`, and lists and records are immutable.
### Freezing in JavaScript
@@ -148,7 +153,9 @@ If we never change data, do we copy everything all the time? No. Since nobody ca
In JavaScript, `[newHead, ...rest]` creates a new array (copying the references, which is cheap for small arrays) but shares every point object. In Scala and F#, `newHead :: rest` is even cheaper: the new list is one new cell pointing to the old list, which is shared entirely. Data structures designed for this are called **persistent**: old versions stay valid after you "update" them. Sharing is only safe because nobody mutates; that is why immutability and persistence go together.
> **From Scala:** `List`, `Vector` and `Map` are all persistent. `xs :+ x` on a `Vector` returns a new vector that shares almost all of its structure with `xs`. F#'s `list` and `Map` behave the same way.
> **From everyday programming:** Git works this way. A commit is an immutable snapshot of your project. A new commit stores only what changed and points to the unchanged files of earlier commits, so it is cheap; and because no commit is ever modified, every old version stays valid and you can check it out at any time.
In the Scala track, `List`, `Vector` and `Map` are all persistent: `xs :+ x` on a `Vector` returns a new vector that shares almost all of its structure with `xs`. F#'s `list` and `Map` behave the same way.
### Making a changed copy
@@ -174,6 +181,10 @@ let ada = { Name = "Ada"; Hp = 10; Level = 3 }
let hurt = { ada with Hp = ada.Hp - 4 } // { Name = "Ada"; Hp = 6; Level = 3 }
```
> **From everyday programming:** in object-oriented code you might write `hero.setHp(hero.getHp() - 4)`. The object changes, and every part of the program that holds a reference to `hero` sees the new value, whether it expected to or not; "who changed this?" bugs come from exactly that. With a changed copy, `ada` stays as it was, and code that should see the hurt hero has to be handed `hurt`. You already know one type that works like this: strings in Java, C#, Python and JavaScript are immutable, so `s.toUpperCase()` returns a new string and leaves `s` alone. That is why you can pass strings around without worrying who else holds them.
A Scala `case class` and an F# record are immutable records: their fields cannot be reassigned, so `copy` and `with` are the only way to get a changed version.
Note the naming convention: F# record fields are PascalCase (`Hp`, and later `NextDir`), where JS and Scala use camelCase (`hp`, `nextDir`).
Watch out in JS: spread is shallow, like `freeze`. `{ ...game, snake: game.snake }` shares the same snake array. That is fine *as long as nobody mutates it*, which is exactly our rule.
@@ -188,16 +199,32 @@ For arrays in JavaScript, learn which methods build a new array and which change
### Loops without reassignment
A `for` loop needs a counter that changes, so it needs `let`. Without `let` you do what you did in Scala: express the loop as a transformation of a whole collection (`map`, `filter`, `some`), as a fold (`reduce`), or as recursion.
A `for` loop needs a counter that changes, so it needs `let`. Without `let` you express the loop differently: as a transformation of a whole collection, as a fold, or as recursion. Three operations cover most of the loops you have ever written. Each takes a small function that says what to do with *one* element (the next section, "Functions are values", looks at passing functions around), does the looping itself, and returns a new array or value. The input array is left untouched.
- **`map(f)`** calls `f` on every element and returns a new array of the results: same length, same order. `[1, 2, 3].map((x) => x * 10)` is `[10, 20, 30]`.
- **`filter(p)`** keeps the elements for which `p` returns `true`. A function that returns a boolean, like `p`, is called a *predicate*. `[1, 2, 3, 4].filter((x) => x % 2 === 0)` is `[2, 4]`.
- **`reduce(f, init)`**, in general called a **fold**, combines all elements into one value. It starts with an *accumulator* equal to `init`, and for each element computes the next accumulator as `f(acc, x)`. The last accumulator is the result. `[1, 2, 3].reduce((acc, x) => acc + x, 0)` computes `((0 + 1) + 2) + 3`, which is `6`.
Here is one computation written both ways:
```js
// sum of squares of the even numbers, no loop and no let
// With a loop: a counter and a total that change on every step.
let total = 0
for (let i = 0; i < xs.length; i++) {
if (xs[i] % 2 === 0) total = total + xs[i] * xs[i]
}
// Without: the sum of the squares of the even numbers, no loop and no let.
const sumEvenSquares = (xs) => xs.filter((x) => x % 2 === 0).map((x) => x * x).reduce((a, b) => a + b, 0)
```
> **From Scala:** `reduce(f, init)` in JavaScript is `foldLeft(init)(f)` in Scala and `List.fold f init xs` in F#; `reduceRight` is `foldRight`. JavaScript's `reduce` *without* an initial value throws on an empty array, like Scala's `reduce`. Always pass the initial value.
Read the second version as a pipeline: keep the even numbers, square each one, add them up. Each step's input is the previous step's output, and nothing is changed along the way. Notice that the loop's `total` became `reduce`'s accumulator: any loop of the shape "start with a value, update it once per element, use the final value" is a fold, whatever it computes.
`Array.from({ length: n }, (_, i) => ...)` builds an array of length `n` by calling a function with each index. `Array.from` is worth looking up now: it is the JavaScript cousin of Scala's `List.tabulate`.
> **From everyday programming:** if you have written SQL, you know this style. `SELECT x * x FROM xs WHERE x % 2 = 0` is a `filter` (`WHERE`) followed by a `map` (`SELECT`), and wrapping it in `SUM(...)` is a fold. You say *what* you want, and the database does the looping. C#'s LINQ (`Where`, `Select`, `Aggregate`), Java streams (`filter`, `map`, `reduce`) and Python's list comprehensions are the same three ideas under other names.
In the Scala track the three are `xs.map(f)`, `xs.filter(p)` and `xs.foldLeft(init)(f)`; in F#, `List.map f xs`, `List.filter p xs` and `List.fold f init xs`. JavaScript's `reduceRight` folds from the end of the array (Scala `foldRight`, F# `List.foldBack`). One trap: JavaScript's `reduce` *without* an initial value throws on an empty array (so does Scala's `reduce`). Always pass the initial value. ([Fold (Wikipedia)](https://en.wikipedia.org/wiki/Fold_%28higher-order_function%29) has pictures of both directions.)
`Array.from({ length: n }, (_, i) => ...)` builds an array of length `n` by calling a function with each index. `Array.from` is worth looking up now. (In the Scala track, `List.tabulate` does the same.)
### Functions are values
@@ -224,6 +251,10 @@ let addThree n = n + 3
twice addThree 10 // 16
```
`twice` returns a new function that still has access to the `f` it was given, even after `twice` itself has returned. A function that captures values from the place where it was created is called a *closure*. A function that takes or returns another function, like `twice`, `map` or `filter`, is called a *higher-order function*.
> **From everyday programming:** you have passed functions as values whenever you wrote a callback. `button.addEventListener('click', onClick)` hands the function `onClick` to the browser, which calls it later; `setInterval(step, 150)` does the same with a timer; a comparator passed to a sort method is a function that the sort calls whenever it needs to compare two elements. `map` and `filter` take callbacks in exactly the same way. The difference in this course is what the callbacks do: they return a result instead of changing something.
In F#, every function of several arguments is *curried* by default: `let add a b = a + b` has type `int -> int -> int`, and `add 3` is a function waiting for the second number. That is why F# library functions put the data last: `List.map f xs` can be partially applied as `List.map f` and used in a pipeline.
This module's `contains(xs, p, eq)` takes a function as its third argument. Why not compare with `===`? Because in JavaScript `===` on objects compares *identity*: `{x: 1} === {x: 1}` is `false`. Passing `eq` lets the caller decide what "equal" means, for instance "same x and same y".
@@ -263,7 +294,9 @@ let double x = x * 2
(double << inc) 3 // 8: backward composition, like compose
```
> **From Scala:** `f andThen g` is F#'s `f >> g` and the JS `pipe` order; `g compose f` is F#'s `g << f` and the JS `compose` order. The F# pipe `x |> f` is `f x` written backwards, which makes long chains read top to bottom, like a chain of Scala method calls.
Side by side: Scala's `f andThen g` is F#'s `f >> g` and the JS `pipe` order; Scala's `g compose f` is F#'s `g << f` and the JS `compose` order. The F# pipe `x |> f` is `f x` written backwards, which makes long chains read top to bottom, like a chain of method calls `x.f().g()`.
> **From everyday programming:** a shell pipeline such as `cat log.txt | grep ERROR | sort | uniq` is `pipe`: a value goes in on the left and passes through each command in reading order. Each command reads its input and writes a new output; none of them edits `log.txt`.
This is why the Scala and F# tracks have a smaller Module 1: `pipe`, `compose` and immutable data are built in. Their tests cover `range`, `dropLast` and `contains`; the composition exercise is a short practice session in the REPL instead.
@@ -390,7 +423,7 @@ When green, also run the full suite once (`./test.sh`) to check the purity audit
<details><summary>Exercise 1.1 (range) — nudge</summary>
You know a function in each language that builds a collection of a given length from the index. In Scala and F# there is even a ready-made range syntax.
Each language has a function that builds a collection of a given length from the index; for JavaScript, "Loops without reassignment" mentions it. In Scala and F# there is even a ready-made range syntax.
</details>
@@ -422,7 +455,7 @@ Which non-mutating method gives you "a piece of the array from here to there"? C
<details><summary>Exercise 1.2 (dropLast) — stronger hint</summary>
JS: `slice(start, end)` returns a new array and accepts negative indices, counted from the end. Try `[].slice(0, -1)` in a Node REPL. Scala: `dropRight` exists and is total; or pattern-match on the list. F#: pattern matching on the list is the clearest. Which three shapes can a list have that matter here? (empty, exactly one element, more than one)
JS: `slice(start, end)` returns a new array and accepts negative indices, counted from the end. Try `[].slice(0, -1)` in a Node REPL. Scala: `dropRight` exists and is total; or pattern-match on the list. F#: pattern matching on the list is the clearest. (A `match` checks which shape a value has and names its parts: `[]` is the empty list, `[ _ ]` a list of exactly one element, and `x :: rest` a list whose first element is `x`, followed by the list `rest`. Module 2 explains pattern matching in general.) Which three shapes can a list have that matter here? (empty, exactly one element, more than one)
</details>
@@ -545,7 +578,7 @@ export const deepFreeze = (obj, ancestors = []) => {
5. In F#, why is it `List.map f xs` (data last) while in JS our functions take the data first? What does the F# order make easy?
6. Your `dropLast` passes the tests. Would it still be correct if the tests did not freeze the input? How could you be sure it never mutates, other than through tests?
7. The purity audit bans `.sort(` but you may use `.toSorted()`. Explain in one sentence what the difference is, in terms of referential transparency.
8. Why does `contains` take an `eq` parameter in JavaScript, but in Scala you can pass `_ == _` and get structural equality for case classes?
8. Why does `contains` take an `eq` parameter in JavaScript? (In the Scala and F# tracks, `==` and `=` compare case classes and records field by field, so `_ == _` or `(=)` would do; Module 2 explains the difference.)
## In production
@@ -563,6 +596,6 @@ export const deepFreeze = (obj, ancestors = []) => {
- [Spread syntax (MDN)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax) — copying arrays and objects with `...`.
- [Array.prototype.toSorted (MDN)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/toSorted) — the non-mutating siblings of `sort`, `reverse` and `splice`.
- [F# functions: composition and pipelining](https://learn.microsoft.com/en-us/dotnet/fsharp/language-reference/functions/#function-composition-and-pipelining) — official docs for `|>`, `>>` and `<<`.
- [Scala 3 book: functions](https://docs.scala-lang.org/scala3/book/taste-functions.html) — a refresher on function values in Scala.
- [Scala 3 book: functions](https://docs.scala-lang.org/scala3/book/taste-functions.html) — function values in the Scala track's syntax.
- [Persistent data structure (Wikipedia)](https://en.wikipedia.org/wiki/Persistent_data_structure) and [Chris Okasaki, *Purely Functional Data Structures* (thesis)](https://www.cs.cmu.edu/~rwh/students/okasaki.pdf) — how immutable structures can be efficient.
- [Rich Hickey, "Are We There Yet?"](https://www.infoq.com/presentations/Are-We-There-Yet-Rich-Hickey/) — a talk on values, identity and time; why mutable objects mix up the three.