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

+9 -3
View File
@@ -31,7 +31,7 @@ Take a sheet of paper and fifteen minutes. **Do not read past the "Stop here" li
Your task: write down the type of "the state of a game of Snake", in the language of your track or in pseudo-code. Use these questions to guide you:
1. **What is the minimum information** you need to (a) draw the current screen and (b) compute the next state when time passes or a key is pressed? Is there anything you were tempted to include that can be *computed* from the rest? (Candidates: the snake's length, the set of occupied cells, pixel coordinates.)
2. **How do you represent the snake's body?** A list of cells? In which order? Moving the snake means "add a new cell at the front, remove the last". Which end of a list is cheap to add to in Scala's `List` and F#'s `list`?
2. **How do you represent the snake's body?** A list of cells? In which order? Moving the snake means "add a new cell at the front, remove the last". Which end of a list is cheap to add to? (Scala's `List` and F#'s `list` are singly linked lists, like the ones from a data structures course: each cell holds one element and a pointer to the rest.)
3. **How many directions do you need to store?** Imagine the player presses two keys very quickly, both before the snake moves again. What do you need to remember to handle that well? (Hint: there is "the way the snake is going" and "the way the player has asked it to go".)
4. **Is there always an apple?** Think of the moment right after the snake eats it, and of a board completely filled by the snake. How do you represent "no apple"?
5. **Where does randomness come from?** The apple appears on a random free cell. If the generator's state lives outside the game state, is a function `nextState(state)` still pure? Could you replay a game?
@@ -111,7 +111,9 @@ Now compare with your design, question by question.
**3. Two directions.** `dir` is the direction the snake **last moved**. `nextDir` is the direction it **will move** on the next step, the most recent valid key press. A key press only changes `nextDir`; a step of the game moves the head in `nextDir` and then records it as `dir`. Why both? You will discover in Module 3 that if you only keep one, a fast player can make the snake reverse into itself. Keep that question in mind.
**4. Optional apple.** There is not always an apple: at the very start (before the first one is placed), and when the snake has filled the whole board. JS uses `null` for "no apple"; Scala and F# use `Option[Point]` / `Point option`, so the compiler forces every reader to handle the "none" case. The board-full situation becomes the status `Won` in Module 4.
**4. Optional apple.** There is not always an apple: at the very start (before the first one is placed), and when the snake has filled the whole board. JS uses `null` for "no apple"; Scala and F# use `Option[Point]` / `Point option`. An `Option` is a small sum type (see "Algebraic data types" below) with two cases: `Some(p)`, which holds a point, and `None`, which holds nothing. You cannot use the point inside without first checking which case you have (usually with a `match`), so the compiler forces every reader to handle the "none" case. The board-full situation becomes the status `Won` in Module 4.
> **From everyday programming:** `null` is the usual way to say "nothing here", and the `NullPointerException` (or `TypeError: Cannot read properties of null`) is the usual price: nothing reminds the reader that the value might be missing. Java's `Optional<Point>`, C#'s nullable types and TypeScript's `Point | null` with strict null checks are all attempts to make "might be missing" visible in the type, which is what `Option` does.
**5. The seed is in the state.** The apple's position comes from a pseudo-random generator, and a generator is a little machine with an internal state (a 32-bit number, the *seed*). If that state lived in a global variable, every function that places an apple would read and change a global, so it would not be pure, and two runs could differ. By putting the seed **inside** the game state, "place an apple" becomes a pure function `state → state` that takes the seed out, uses it, and stores the new seed. Same starting seed, same game, every time. Module 4 is all about this.
@@ -141,7 +143,9 @@ A **product type** holds several values *at the same time*: a `Point` has an `x`
A **sum type** is *one of* several alternatives: a `Direction` is `Up` *or* `Down` *or* `Left` *or* `Right`. An `Action` is `Tick` *or* `Turn` (carrying a direction) *or* ... The number of possible values is the sum of the alternatives'. Each alternative is called a *case* (or *variant*, or *constructor*).
> **From Scala:** you know both. `case class` is a product, `enum` (or a `sealed trait` with case classes) is a sum, and `match` takes a sum apart. F# spells the sum as a *discriminated union* (DU): `type Shape = Circle of float | Square of float`. JavaScript has no sum types, so we encode them by convention, as *tagged unions*.
> **From everyday programming:** a class with fields is a product type. A plain Java or C# `enum` is a sum type whose cases carry no data. In object-oriented code, a sum type whose cases carry *different* data is usually an abstract base class with one subclass per case (`Shape` with subclasses `Circle` and `Square`), plus `instanceof` checks or the visitor pattern to tell them apart. A sum type says the same thing more directly: here is the complete list of cases, and nothing else can be a `Shape`.
Each track spells them differently. In Scala, a product is a `case class` (an immutable record) and a sum is an `enum` (or a `sealed trait` with case classes). F# spells products as records and sums as *discriminated unions* (DU): `type Shape = Circle of float | Square of float`. JavaScript has no sum types, so we encode them by convention, as *tagged unions*.
Here is the same small sum type, unrelated to the game, in all three languages: a traffic light, and a function that gives the next light.
@@ -196,6 +200,8 @@ let area s =
| Square side -> side * side
```
The `match` in Scala and F# is called **pattern matching**: it takes a sum apart. It looks like a `switch` statement, with two differences. First, each case can *take the value apart*: `Circle(r)` both checks that `s` is a circle and gives its radius the name `r`. Second, the whole `match` is an expression that returns a value, so no `return` or `break` is needed. Patterns work on lists too: in F#, `[]` matches the empty list and `x :: rest` matches a list whose first element is `x`, followed by the list `rest`. (In Scala the same patterns are written `Nil` and `x :: rest`.) In JavaScript, a `switch` on the `tag` field plays the role of `match`, and you read the data out of the object yourself.
Note the JS lookup-table trick in `nextLight`: an object literal used as a function from a fixed set of strings to values. It is often the clearest way to write a function over a nullary sum type.
The important difference between the languages is **exhaustiveness checking**. If you add `case Triangle` to Scala's `Shape` and forget to handle it in `area`, the compiler warns you. F# does the same. JavaScript will silently return `undefined`. In JS you must be your own exhaustiveness checker; the fixtures, which cover every case, help.