Files
functional-programming-snake/course/00-setup.md
T
Lucas WintherandClaude Opus 5.5 82f47fb3ee docs(course): consistency pass
- m04: F# mulberry32 hints and skeleton route * and + through uint64 helpers;
  new 'F# in the browser' note on the Fable uint32 trap
- m01-m05: 'About test names' notes like m06-m11; m00 explains JS vs Scala/F# titles
- m00/m09: first build-web.sh run restores Fable; m09 JS file list (main.js is given)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-29 01:17:35 +01:00

347 lines
23 KiB
Markdown

# Module 0 — Setup and how to use this course
> **Time:** ~1 h · **You'll edit:** nothing yet · **Tests:** JS `cd js && node --test "test/m00-*.test.js"` · Scala `cd scala && ./test.sh M00` · F# `cd fsharp && ./test.sh M00`
> **Prerequisites:** one course in Scala (or similar): you know `map`, `filter`, `fold`, case classes and pattern matching
## Why this module
You probably already know how to write Snake. Something like this:
```js
// The usual way. Every line changes something somewhere.
let snake = [{ x: 10, y: 10 }, { x: 9, y: 10 }]
let apple = { x: 3, y: 4 }
setInterval(() => {
const head = { x: snake[0].x + dx, y: snake[0].y + dy }
snake.unshift(head)
if (head.x === apple.x && head.y === apple.y) apple = randomCell()
else snake.pop()
draw()
}, 150)
document.addEventListener('keydown', (e) => { /* change dx, dy */ })
```
It works. But try to answer these questions about it:
- How would you write a test that checks "the snake grows when it eats"? You would have to set global variables, wait for a timer, and somehow control `randomCell()`.
- A player reports "the snake died when it shouldn't have". Can you reproduce exactly what happened?
- What does `draw()` depend on? What does it change? You have to read the whole program to know.
In this course you write the same game in a way where every one of those questions has an easy answer. The game rules, the randomness, the drawing, the clock, the keyboard and even "the program" itself are ordinary values and pure functions. Only one tiny file at the very edge actually *does* anything. By the end you will have done the conversion yourself, step by step, and you will be able to explain why each step helps.
This module gets your tools working and shows you how the course is organised. There is nothing to implement yet.
## Learning objectives
After this module you can:
- **describe** the architecture you are going to build, and which part of it is allowed to have side effects;
- **run** the tests of one module, or all of them, in your chosen track;
- **read** a red test and find the fixture case it is checking;
- **explain** what the purity audit forbids and why;
- **choose** between solo mode and AI-mentor mode, and know when to use the checkpoints in `solutions/`.
## Concepts
### What you will build
First **Snake**: a 20 x 20 board, a snake that moves one cell per tick, apples that appear on random free cells, a score, pause, restart, a speed-up as you score. It runs in the browser on a `<canvas>`.
Then, in the capstone (Module 11), **Pong**: you convert a second game on your own, applying a recipe you have practised through the whole course.
### The big picture
Here is the whole program you are going to build. Keep coming back to this picture; every module fills in one box.
```
+-------------------------------- PURE CORE ----------------------------------+
| |
| m02 model m03 rules m04 random, spawn m05 game |
| GameState --> turn, advance --> spawnApple, tick --> update(state, a) |
| replay, history |
| | |
| m06 view v |
| view : State -> Scene (shapes) |
| |
| EFFECTS AS VALUES (descriptions, nothing happens yet) |
| m07 IO<A> "draw this rectangle", "give me a random seed" |
| m08 Stream<A> a pure automaton: Input -> (outputs, next stream) |
| m09 main : IO<Stream<IO<unit>>> -- the whole program, as ONE value |
+-------------------------------------+---------------------------------------+
| main
v
+---------------- THE EDGE (m09 runtime, the only impure file) ---------------+
| runIO / runApp: interprets IO, feeds clock ticks and key presses into the |
| stream, performs the drawing. One mutable variable, about a page of code. |
+-------------------------------------+---------------------------------------+
^ keys, time | pixels
| v
browser (canvas, keyboard, clock, crypto)
```
Read it from the top. The **pure core** is plain data and plain functions: given a state and an action, `update` computes the next state; given a state, `view` computes a list of shapes. Below that, **effects as values**: an `IO` is a *description* of something to do (like a recipe), and a `Stream` is a *description* of how to react to inputs over time. Building these descriptions changes nothing in the world. Finally, at **the edge**, a small runtime takes the one value `main` and actually runs it.
> **From Scala:** if you have heard of cats-effect or ZIO, this is the same idea in miniature. You will build `IO` and `Stream` yourself, from scratch, in about a hundred lines each, so there is nothing magic left.
This design is sometimes called *functional core, imperative shell* (Gary Bernhardt). We push it further than usual: even the shell is mostly pure, and the truly imperative part is one function.
### Choosing a track
The course text is shared. Every code listing that you would write appears three times, always in this order: **JavaScript**, **Scala**, **F#**. Pick one track and stay with it; you can read the others for comparison.
| Track | Why pick it | Tests with | Runs in the browser via |
|---|---|---|---|
| **JavaScript** (primary) | No compiler, no types: you must hold the discipline yourself, which teaches the most. Zero dependencies. | Node's built-in `node --test` | plain ES modules |
| **Scala 3** | Closest to what you already know. The compiler helps you (exhaustive matches, immutable by default). | munit via `scala-cli` | Scala.js |
| **F#** | An ML-family language: lighter syntax, data-last functions and pipelines. A good second functional language. | Expecto via `dotnet` | Fable |
If you are unsure, take JavaScript. It is the primary track, and Module 1 bridges from Scala to JavaScript. The three tracks use the same names where they can. Where they differ, the text tells you the first time it matters, and `course/reference.md` has the full table. The most common differences:
- JS functions take the data first: `map(io, f)`. Scala uses methods: `io.map(f)`. F# takes the data last so that pipelines work: `io |> IO.map f`.
- `flatMap` is called `bind` in F#.
- "No apple" is `null` in JS and `None` (an `Option`) in Scala and F#.
- Random seeds are `number` in JS, `Long` in Scala, `uint32` in F#.
### Installing the tools
You need Git and the toolchain for your track. You can install all three if you like.
**JavaScript:** Node.js **22 or newer** (the course is tested on Node 24). Download it from [nodejs.org/en/download](https://nodejs.org/en/download). There are no npm packages to install.
```sh
node --version # v22.x or higher
```
**Scala:** [scala-cli](https://scala-cli.virtuslab.org/install). It downloads a JDK, the Scala 3 compiler and the test library by itself the first time you run it, so the first run is slow; later runs are fast.
```sh
scala-cli version
```
**F#:** the [.NET SDK 10](https://dotnet.microsoft.com/en-us/download/dotnet/10.0) (installation help: [learn.microsoft.com/dotnet/core/install](https://learn.microsoft.com/en-us/dotnet/core/install/)). The first test run restores the Expecto package from NuGet.
```sh
dotnet --version # 10.x
```
An editor with language support helps a lot: VS Code with the Metals extension (Scala) or Ionide (F#), or IntelliJ / Rider. For JavaScript any editor is fine.
### A tour of the repository
```
README.md front page
MENTOR.md instructions for an AI mentor (see "Solo or with a mentor")
course/ this text: 00-setup.md ... 11-capstone-pong.md, reference.md, method.md
fixtures/ shared test cases as JSON, used by all three tracks
js/ src/ YOUR code: stubs to fill in, one file per topic
solutions/ reference solutions, same file names (try first!)
test/ tests, one file per module: m03-rules.test.js, ...
scala/ src/ solutions/ test/ (same idea; files are PascalCase: Rules.scala)
fsharp/ src/ solutions/ tests/ (same idea: Rules.fs)
scripts/serve.mjs a tiny web server for playing the game in the browser
```
Four folders matter:
- **`src/`** is where you work. Every file already exists with the final function names, signatures and a doc comment that says *what* the function must do. The bodies are placeholders: `todo('m03')` in JS, `???` in Scala, `failwith "TODO m03"` in F#. Your job is to replace them.
- **`test/`** (F#: `tests/`) holds the tests. Each module has its own test file, so finishing Module 3 does not depend on Module 6.
- **`fixtures/`** holds most of the pure-logic test cases as language-neutral JSON. For example `fixtures/m03-advance.json` contains cases named `advance-plain-move`, `advance-into-vacating-tail`, and so on, each with `args` and the `expected` result. All three tracks read the same files, so the game behaves identically in every language. When a test fails, the fixture tells you exactly which input was given and what was expected.
- **`solutions/`** holds a reference solution for every file in `src/`. It is there so you are never stuck for good (see the checkpoint policy below), and so a mentor can check your work. It is not there to read before you try.
Each module tells you, in its header, which files you will edit. The map is the same in every track:
| Module | JS file(s) in `js/src/` | Scala (`scala/src/`) | F# (`fsharp/src/`) |
|---|---|---|---|
| 01 | `util.js` | `Util.scala` | `Util.fs` |
| 02 | `model.js` | `Model.scala` | `Model.fs` |
| 03 | `rules.js` | `Rules.scala` | `Rules.fs` |
| 04 | `random.js`, `spawn.js` | `Random.scala`, `Spawn.scala` | `Random.fs`, `Spawn.fs` |
| 05 | `game.js` | `Game.scala` | `Game.fs` |
| 06 | `view.js` | `View.scala` | `View.fs` |
| 07 | `io.js` | `IO.scala` | `IO.fs` |
| 08 | `stream.js` | `Stream.scala` | `Stream.fs` |
| 09 | `input.js`, `app.js`, `runtime.js`, `browser.js` (`main.js` is given) | `Input.scala`, `App.scala`, `Runtime.scala` | `Input.fs`, `App.fs`, `Runtime.fs` |
| 10 | edits `game`, `input`, `app` | same | same |
| 11 | `pong/` | `pong/` | `Pong/` |
### Running the tests
From the repository root:
**JavaScript**
```sh
cd js
node --test "test/m03-*.test.js" # one module
./test.sh # everything, including the purity audit
```
**Scala**
```sh
cd scala
./test.sh M03 # one module
./test.sh # everything
```
**F#**
```sh
cd fsharp
./test.sh M03 # one module
./test.sh # everything
```
Try it now with Module 0, which has a few sanity tests that already pass. They check that your toolchain works and that the fixtures folder can be read. If you see three green tests (Scala and F#: a short summary with no failures), you are set up.
### What a red test means
Run the Module 3 tests in a fresh checkout. Every one of them fails, and at the bottom Node repeats each failure with details:
```
✖ m03 move: move-up (0.91ms)
✖ m03 move: move-down (0.13ms)
...
✖ failing tests:
test at test/m03-rules.test.js:9:5
✖ m03 move: move-up (0.91ms)
Error: TODO: implement in m03
at todo (.../js/src/todo.js:3:9)
at move (.../js/src/rules.js:4:33)
```
This is expected, and it is the starting point of every module. In Scala you see `scala.NotImplementedError: an implementation is missing` (that is what `???` throws), and in F# an exception with `TODO m03`.
A red test is information, not a verdict. Read it in this order:
1. **The test name.** Test names are sentences written for you: `dropLast leaves its input untouched`. Fixture tests are named after the function and the fixture case: `m03 advance: advance-into-vacating-tail`. (That is the JavaScript title; JS puts the module in front. Scala and F# print `advance: advance-into-vacating-tail`, and their other titles sometimes use their own track's vocabulary, for example `bind` instead of `flatMap`. The modules quote the JavaScript titles; each module's "About test names" note says how the other tracks differ.)
2. **The message.** `TODO: implement in m03` means you have not written that function yet. An assertion failure shows *expected* versus *actual*: compare them field by field. With game states, the difference is often in one field (`dir`, `seed`, `status`).
3. **The fixture.** For a fixture test, open `fixtures/<module>-<function>.json` and search for the case name. You will see the exact input. Draw it on paper; the boards in fixtures are deliberately tiny (3 x 2, 5 x 5) so that you can.
A green test does not prove your code is right, and a red test never means you are bad at this. The tests are a conversation partner: they tell you which situation you have not thought about yet.
### The purity rules
> **The purity audit.** A test in every track (JS `test/purity.test.js`, Scala `PuritySuite` in `test/Purity.test.scala`, F# `tests/Purity.fs`) scans your `src/` files (comments are ignored) and fails if it finds any of these:
>
> | Track | Banned outside the runtime files |
> |---|---|
> | JS | `let`, `var`, `++`, `--`, `.push(`, `.pop(`, `.shift(`, `.unshift(`, `.splice(`, `.sort(`, `.reverse(`, `Math.random`, `Date.now`, `performance.now`, `document`, `window`, `addEventListener` |
> | Scala | `var`, `mutable`, `while`, `scala.util.Random`, `System.`, `println` |
> | F# | `mutable`, `<-`, `System.Random`, `DateTime`, `printf`, `ref ` |
>
> Only the runtime files (JS `runtime.js`, `browser.js`, `main.js`; Scala `Runtime.scala`; F# `Runtime.fs`, plus the `web/` folders) may use them.
>
> **Why so strict?** Each banned word is a way to make a result depend on something other than the function's arguments, or to change something other than the function's result:
> - `let`/`var`/`++`/`--`/`<-`/`mutable`: reassignment. A name that means different things at different times.
> - `.push(`, `.sort(` and friends: they change an array in place, so everyone else holding that array sees the change. (Use `[...xs, x]`, `toSorted()`, `toReversed()` instead; they return new arrays.)
> - `Math.random`, `Date.now`, `System.Random`, `DateTime`: hidden inputs. The same call gives a different answer each time.
> - `document`, `window`, `addEventListener`, `println`, `printf`: talking to the outside world.
>
> Without `let` or `var` you cannot write a classic `for (let i = 0; ...)` loop. That is deliberate: you will use `map`, `filter`, `reduce`, recursion and small helper functions instead, which is what you know from Scala.
>
> The audit is a blunt text scan. It can be fooled, and it sometimes complains about harmless code (for instance a string containing `--`). It is a guard rail, not a proof. The real rule is the one behind it: **outside the edge, a function's result depends only on its arguments, and calling it changes nothing.**
### Checkpoints: what to do when you are stuck
Each module builds on the previous ones. If you get stuck, work through this list in order:
1. Re-read the doc comment in the stub and the "Concepts" section of the module.
2. Open the failing fixture case and work it out on paper.
3. Open the module's **Hints**. From Module 1 on, every coding exercise has three tiers: a *nudge*, a *stronger hint*, and a *near-solution* skeleton with holes. Open them one at a time. (Module 0's setup exercises have only the hints they need.)
4. Ask your mentor (human or AI), explaining what you tried.
5. Only then: **take the checkpoint.** Copy that module's files from `solutions/` into `src/` (for example `cp js/solutions/rules.js js/src/rules.js`) and continue with the next module. Because tests are organised per module, all earlier tests stay green.
If you take a checkpoint, read the solution carefully and compare it with your attempt. Then close it and write the function again from memory a day later. That is where the learning happens.
### Solo, or with an AI mentor
The course works in two modes.
- **Solo.** The text, the tiered hints and the tests carry you. Answer the "Check your understanding" questions at the end of each module out loud or on paper before moving on. If you cannot answer one, that is the part to re-read.
- **With an AI as a Socratic mentor.** [`MENTOR.md`](../MENTOR.md) at the repository root tells an AI assistant how to help you without doing the work for you: it asks questions before it explains, gives hints in the same order as the hint tiers, asks you to predict and explain, and writes solution code only if you explicitly give up on an exercise. Many coding assistants pick it up automatically (through `AGENTS.md` / `CLAUDE.md`); with others, start the conversation with "Read MENTOR.md and be my mentor for Module 3."
A mentor that hands you answers feels productive and teaches very little. If your assistant starts writing your functions for you, remind it of `MENTOR.md`.
### How each module is laid out
Every module has the same parts, in this order:
1. **Why this module**: the problem, usually what goes wrong in the stateful version.
2. **Learning objectives**: what you should be able to do afterwards.
3. **Concepts**: the ideas, with Scala bridges (blockquotes starting with **From Scala:**) and code in all three languages.
4. **Exercises**: the functions to write, with signatures in three languages, the tests and fixtures that check them, and pitfalls.
5. **Hints**: folded away; from Module 1 on, every coding exercise has a nudge, a stronger hint and a near-solution. Open them one tier at a time.
6. **Check your understanding**: questions you should be able to answer before moving on. Several ask you to predict an output or to say what would break if something changed.
7. **In production**: how real libraries and frameworks solve the same problem.
8. **Further reading**: links to the sources of the ideas.
### Playing the game
You can play the game once Module 9 is done. For reference, this is how:
- **JS:** `node scripts/serve.mjs` from the repository root, then open http://localhost:8000/js/
- **Scala:** `cd scala && ./build-web.sh` (the first run downloads the Scala.js linker), then serve as above and open http://localhost:8000/scala/
- **F#:** `cd fsharp && ./build-web.sh` (the first run restores the Fable compiler), serve, and open http://localhost:8000/fsharp/
## Exercises
### Exercise 0.1 — Green lights
Install the toolchain for your track and run the Module 0 tests. They should pass without any changes.
- Run: JS `cd js && node --test "test/m00-*.test.js"` · Scala `cd scala && ./test.sh M00` · F# `cd fsharp && ./test.sh M00`
### Exercise 0.2 — Read a red test
Run the Module 3 tests (`m03` / `M03`). Pick the failing test whose name ends in `advance-into-vacating-tail`. Find that case in `fixtures/m03-advance.json` and draw the board from `args` on squared paper: mark the head, the body and the tail. Then draw the `expected` board. Write one sentence describing what happened between the two pictures. (You will meet this exact case again in Module 3.)
### Exercise 0.3 — Break the purity audit on purpose
In your track's Module 1 file (`js/src/util.js`, `scala/src/Util.scala`, `fsharp/src/Util.fs`) add a line that uses a banned construct, for example `let counter = 0` (JS), `var counter = 0` (Scala) or `let mutable counter = 0` (F#). Run the full test suite (`./test.sh`) and find the purity failure. Then remove the line again and check that the failure is gone. Now you know what the audit looks like when it catches you.
## Hints
<details><summary>Exercise 0.1 — nudge</summary>
If a command is not found, the tool is not on your `PATH`. Open a new terminal after installing, and check with `node --version`, `scala-cli version` or `dotnet --version`.
</details>
<details><summary>Exercise 0.1 — stronger hint</summary>
The first Scala and F# runs download a lot (compiler, JDK, NuGet packages). Let them finish; if they fail with a network error, run them again. Make sure you run the command from inside the track folder (`js/`, `scala/` or `fsharp/`).
</details>
<details><summary>Exercise 0.2 — nudge</summary>
`snake` is a list of points, head first. `y` grows downward. The fixture board is 5 x 5, so coordinates run from 0 to 4.
</details>
## Check your understanding
1. In the big-picture diagram, which box is allowed to change things in the world? Why is it useful that this box is small?
2. Your friend says "an `IO` value draws a rectangle". Rephrase that sentence so that it is accurate for this course.
3. Why do the three tracks share their test cases as JSON files instead of each track writing its own?
4. A test fails with `TODO: implement in m04`. What does that tell you, and what does it not tell you?
5. Why does the purity audit ban `.sort(` in JavaScript but not `.map(`?
6. The purity audit can be fooled. Write down one line of JS that mutates something but would pass the scan. What does that tell you about relying on tools versus understanding the rule?
7. When would taking a checkpoint from `solutions/` be the right decision, and what should you do right after taking one?
## In production
The architecture of this course is close to how several real systems are built:
- **The Elm Architecture** ([guide.elm-lang.org/architecture](https://guide.elm-lang.org/architecture/)) structures every Elm web app as `model`, `update : Msg -> Model -> Model` and `view : Model -> Html`. The Elm runtime is the only thing that performs effects. F# programmers use the same pattern through [Elmish](https://elmish.github.io/elmish/).
- **Redux** ([redux.js.org](https://redux.js.org/)) brought the same idea to JavaScript: state changes only through pure *reducers* `(state, action) => state`.
- **cats-effect** ([typelevel.org/cats-effect](https://typelevel.org/cats-effect/)) and **ZIO** ([zio.dev](https://zio.dev/)) in Scala, with **fs2** ([fs2.io](https://fs2.io/)) for streams, make whole server applications out of `IO` values and run them with one call at the edge. Your hand-built `IO` and `Stream` (Modules 7 and 8) are small cousins of these.
## Further reading
- [Functional Core, Imperative Shell](https://www.destroyallsoftware.com/screencasts/catalog/functional-core-imperative-shell) — Gary Bernhardt's short screencast on splitting a program into a pure core and a thin effectful shell; this course takes the idea further.
- [Why Functional Programming Matters](https://www.cs.kent.ac.uk/people/staff/dat/miranda/whyfp90.pdf) — John Hughes' classic paper on why higher-order functions and laziness make programs modular. Short and still current.
- [Node.js test runner](https://nodejs.org/api/test.html) — documentation for `node --test`, used by the JS track.
- [munit](https://scalameta.org/munit/) — the Scala test library used by the Scala track.
- [Expecto](https://github.com/haf/expecto) — the F# test library used by the F# track.
- [Scala.js](https://www.scala-js.org/) and [Fable](https://fable.io/) — the compilers that turn the Scala and F# tracks into JavaScript for the browser.