Files
functional-programming-snake/course/01-purity-and-immutability.md
T
2026-10-29 00:59:58 +01:00

567 lines
28 KiB
Markdown

# Module 1 — Purity and immutability
> **Time:** ~3 h · **You'll edit:** JS `js/src/util.js` · Scala `scala/src/Util.scala` · F# `fsharp/src/Util.fs`
> **Tests:** JS `cd js && node --test "test/m01-*.test.js"` · Scala `cd scala && ./test.sh M01` · F# `cd fsharp && ./test.sh M01`
> **Prerequisites:** Module 0
## Why this module
Look at this piece of a typical Snake game:
```js
function step() {
const head = nextHead()
snake.body.unshift(head) // changes the array everyone shares
if (samePlace(head, apple)) {
score++ // changes a global
apple = randomFreeCell() // a different answer every call
} else {
snake.body.pop() // changes it again
}
}
```
`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.
## Learning objectives
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`);
- **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.
## Concepts
### Referential transparency
An expression is **referentially transparent** if you can replace it with its value anywhere in the program without changing what the program does.
```js
const add = (a, b) => a + b
const x = add(2, 3) + add(2, 3)
// You may rewrite this as 5 + 5, or as const five = add(2, 3); five + five.
// The program means exactly the same thing.
```
Now compare:
```js
let count = 0
const next = () => { count = count + 1; return count }
const y = next() + next() // 1 + 2 = 3
// Rewrite it as const n = next(); n + n -> 1 + 1 = 2. A different program!
```
`next()` is not referentially transparent: the same expression gives a different value each time, because it reads and changes hidden state. A function whose calls are always referentially transparent is called **pure**. In practice that means two things:
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.
> **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.
### Exercise in prose: pure or not?
Before reading the answers, decide for each function: pure or impure? If impure, which of the two rules does it break?
```js
const a = (xs) => xs.length // (a)
const b = (xs) => { xs.push(0); return xs } // (b)
const c = () => Date.now() // (c)
const d = (p) => ({ ...p, x: p.x + 1 }) // (d)
const e = (msg) => { console.log(msg); return msg } // (e)
const f = (xs) => xs.toSorted() // (f)
const g = (xs) => xs.sort() // (g)
const h = (n) => { throw new Error('no') } // (h)
```
<details><summary>Answers</summary>
- **(a) pure.** Result depends only on `xs`, nothing changes.
- **(b) impure.** It mutates its argument. The caller's array has grown, and anyone else holding it sees the change. (Same input array, second call: a different result.)
- **(c) impure.** No arguments, yet a different result every millisecond: a hidden input (the clock).
- **(d) pure.** It builds a new object; `p` is untouched.
- **(e) impure.** It returns its argument unchanged, but printing is an effect the outside world can observe. Replacing `e('hi')` with `'hi'` changes what the program does.
- **(f) pure.** `toSorted` returns a new sorted array (ES2023).
- **(g) impure.** `sort` sorts *in place* and returns the same array. This is a classic bug source in JavaScript.
- **(h) debatable.** It is deterministic and changes nothing, but it does not return a value. Many people count exceptions as an effect. In this course we avoid throwing in game logic: our rule functions are *total* (they return a value for every input), which you will see in Module 3.
</details>
### `const` is not immutability
In JavaScript, `const` means *this name will always refer to the same value*. It says nothing about whether the value can change:
```js
const xs = [1, 2]
xs.push(3) // allowed! xs is still the same array, which now has 3 elements
xs = [9] // TypeError: Assignment to constant variable.
```
So there are two different things:
| | Can the name point somewhere else? | Can the value change? |
|---|---|---|
| JS `let` | yes | yes (if it is an object/array) |
| JS `const` | no | yes (if it is an object/array) |
| JS `const` + frozen value | no | no |
| 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.
### Freezing in JavaScript
`Object.freeze(obj)` makes an object (or array) read-only: assigning to a property or pushing an element throws in strict mode, which ES modules always are.
```js
const p = Object.freeze({ x: 1, y: 2 })
p.x = 5 // TypeError in a module: Cannot assign to read only property 'x'
```
But `freeze` is **shallow**. It freezes the object's own properties, not the objects they point to:
```js
const game = Object.freeze({ snake: [{ x: 1, y: 1 }] })
game.snake = [] // TypeError: good
game.snake.push({x: 2}) // works! The array inside is not frozen.
```
The tests in this course **deep-freeze** every input they hand to your functions. If your code accidentally mutates its input, it does not silently corrupt the test; it throws a `TypeError` at the exact line that tried. When you see `Cannot add property 3, object is not extensible` or `Cannot assign to read only property`, you have found a mutation. You will write `deepFreeze` yourself in this module.
### Persistent data and structural sharing
If we never change data, do we copy everything all the time? No. Since nobody can change the old value, the new value may **share** the parts that did not change.
```
before: snake ----> [ H0 , B1 , B2 ] (an array of three point objects)
after: snake' ---> [ H1 , H0 , B1 ] (a new array...)
| |
+----+--- ...pointing at the very same point objects
```
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.
### Making a changed copy
The core move of this whole course: *take a record, and produce a new record that differs in a few fields.* Each language has a built-in syntax for it. Here with a small example record (not from the game):
**JavaScript**
```js
const ada = { name: 'Ada', hp: 10, level: 3 }
const hurt = { ...ada, hp: ada.hp - 4 } // { name: 'Ada', hp: 6, level: 3 }; ada is unchanged
```
**Scala**
```scala
final case class Hero(name: String, hp: Int, level: Int)
val ada = Hero("Ada", 10, 3)
val hurt = ada.copy(hp = ada.hp - 4) // Hero(Ada,6,3); ada is unchanged
```
**F#**
```fsharp
type Hero = { Name: string; Hp: int; Level: int }
let ada = { Name = "Ada"; Hp = 10; Level = 3 }
let hurt = { ada with Hp = ada.Hp - 4 } // { Name = "Ada"; Hp = 6; Level = 3 }
```
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.
For arrays in JavaScript, learn which methods build a new array and which change the old one:
| Builds a new value (use these) | Mutates in place (banned by the purity audit) |
|---|---|
| `map`, `filter`, `reduce`, `slice`, `concat`, `[...xs, x]`, `[x, ...xs]` | `push`, `pop`, `shift`, `unshift`, `splice` |
| `toSorted`, `toReversed`, `with(i, v)` | `sort`, `reverse`, `xs[i] = v` |
| `some`, `every`, `find`, `includes`, `Array.from` | |
### 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.
```js
// sum of 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.
`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`.
### Functions are values
A function is a value like any other. You can store it in a `const`, pass it as an argument, and return it from another function:
**JavaScript**
```js
const twice = (f) => (x) => f(f(x)) // takes a function, returns a function
const addThree = (n) => n + 3
twice(addThree)(10) // 16
```
**Scala**
```scala
def twice(f: Int => Int): Int => Int = x => f(f(x))
val addThree = (n: Int) => n + 3
twice(addThree)(10) // 16
```
**F#**
```fsharp
let twice f = fun x -> f (f x)
let addThree n = n + 3
twice addThree 10 // 16
```
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".
### Composition
Programs in this course are mostly small functions plugged together. Two ways of plugging:
- **pipe** (left to right): start with a value and push it through functions in reading order. `pipe(x, f, g)` is `g(f(x))`.
- **compose** (right to left, like maths): `compose(g, f)` is the function `x => g(f(x))`, read "g after f".
```js
const inc = (x) => x + 1
const double = (x) => x * 2
// pipe(3, inc, double) -> double(inc(3)) = 8
// compose(double, inc)(3) -> double(inc(3)) = 8
// compose(inc, double)(3) -> inc(double(3)) = 7
```
JavaScript has neither built in; you write both in this module (Exercises 1.4 and 1.5). Scala and F# do have them:
**Scala**
```scala
val inc = (x: Int) => x + 1
val double = (x: Int) => x * 2
(inc andThen double)(3) // 8: first inc, then double (like pipe)
(double compose inc)(3) // 8: double after inc (like compose)
3.pipe(inc).pipe(double) // 8, needs: import scala.util.chaining.*
```
**F#**
```fsharp
let inc x = x + 1
let double x = x * 2
3 |> inc |> double // 8: the pipe operator, value first
(inc >> double) 3 // 8: forward composition, like andThen
(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.
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.
## Exercises
All functions here must be pure: no mutation (inputs are deep-frozen in the JS tests, so mutation throws), no reassignment.
### Exercise 1.1 — `range`
`range(n)` returns the numbers `0, 1, ..., n-1` in order; `range(0)` is empty. You will use it in Module 4 to list every cell of the board.
**JavaScript**
```js
export const range = (n) => ... // range(3) -> [0, 1, 2]
```
**Scala**
```scala
def range(n: Int): List[Int] // range(3) == List(0, 1, 2)
```
**F#**
```fsharp
let range (n: int) : int list // range 3 = [0; 1; 2]
```
Tests: `range(0) is empty`, `range(3) counts from zero up to n-1`.
Watch out: no `for (let i ...)` in JS. Think "build an array of length `n` from its indices".
### Exercise 1.2 — `dropLast`
`dropLast(xs)` returns `xs` without its last element. The empty list stays empty. It must never modify `xs`. You will use it in Module 3: a moving snake loses its last segment.
**JavaScript**
```js
export const dropLast = (xs) => ... // dropLast([1, 2, 3]) -> [1, 2]; dropLast([]) -> []
```
**Scala**
```scala
def dropLast[A](xs: List[A]): List[A]
```
**F#**
```fsharp
let dropLast (xs: 'a list) : 'a list
```
Tests: `dropLast of an empty list is empty`, `dropLast removes only the last element`, `dropLast leaves its input untouched` (the input is frozen in JS).
Watch out: the empty case. In Scala, `List.empty.init` *throws*. In F#, `List.init` is something else entirely (it builds a list from a length and a function). A function that throws on some inputs is called *partial*; we want *total* functions.
### Exercise 1.3 — `contains`
`contains(xs, p, eq)` is true if some element `x` of `xs` satisfies `eq(x, p)`. In Module 3 you will ask "does the snake's body contain this point?", where points are compared by their coordinates.
**JavaScript**
```js
export const contains = (xs, p, eq) => ... // contains([1, 2, 3], 5, sameParity) -> true
```
**Scala**
```scala
def contains[A](xs: List[A], p: A, eq: (A, A) => Boolean): Boolean
```
**F#**
```fsharp
let contains (xs: 'a list) (p: 'a) (eq: 'a -> 'a -> bool) : bool
```
Tests: `contains finds an element using the supplied equality` (with `sameParity`, 5 "equals" 1 and 3), `contains is false when no element is equal`, `contains is false for an empty list`, `contains compares structurally when given a structural equality`.
Watch out: the argument order of `eq`. The doc comment says `eq(x, p)`: the list element first.
### Exercise 1.4 — `pipe` (JS only)
`pipe(x, ...fns)` threads `x` through the functions from left to right. With no functions it returns `x`.
**JavaScript**
```js
export const pipe = (x, ...fns) => ... // pipe(2, x => x + 1, x => x * 10) -> 30; pipe(5) -> 5
```
Tests: `pipe threads a value through functions left to right`, `pipe with no functions returns the value`.
**Scala / F# practice (no tests).** Open a REPL (`scala-cli repl` or `dotnet fsi`). Define `inc`, `double` and `square`, and predict, *before evaluating*, the results of: Scala `(inc andThen double andThen square)(1)` and `(inc compose double compose square)(1)`; F# `1 |> inc |> double |> square` and `(inc << double << square) 1`. Check your predictions.
### Exercise 1.5 — `compose` (JS only)
`compose(...fns)` returns a function that applies the functions **right to left**. `compose()` with no functions is the identity function.
**JavaScript**
```js
export const compose = (...fns) => ... // compose(x => x * 10, x => x + 1)(2) -> 30; compose()(5) -> 5
```
Tests: `compose applies functions right to left`, `compose with no functions is the identity`.
### Exercise 1.6 — `deepFreeze` (JS only)
`deepFreeze(obj)` freezes `obj` and everything reachable from it, then returns the same object. Two situations make it interesting:
- **An already-frozen parent with unfrozen children.** You must still go into it. (Test: `deepFreeze reaches unfrozen children of an already-frozen parent`.)
- **Cycles.** `o.child.parent === o`. A naive recursion never ends. (Test: `deepFreeze handles cyclic objects`.)
**JavaScript**
```js
export const deepFreeze = (obj) => ... // returns obj itself, frozen all the way down
```
Other tests: `deepFreeze freezes nested objects and arrays`, `deepFreeze returns the same object it was given`.
Watch out: `typeof null === 'object'`. And you may not keep a mutable "already seen" set in a `let`. How else could a recursive call know where it has already been?
One honest remark: `Object.freeze` itself changes the object (it flips it to read-only). That is the one deliberate "mutation" in this module, and it only ever *removes* the ability to change things.
### Run
- JS: `cd js && node --test "test/m01-*.test.js"`
- Scala: `cd scala && ./test.sh M01`
- F#: `cd fsharp && ./test.sh M01`
When green, also run the full suite once (`./test.sh`) to check the purity audit is happy with `util`.
## Hints
<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.
</details>
<details><summary>Exercise 1.1 (range) — stronger hint</summary>
JS: `Array.from` takes an "array-like" object (anything with a `length`) and a mapping function `(element, index) => ...`. Scala: `0 until n` is a `Range`; it has `.toList`. F#: `[ a .. b ]` is a list literal for a range (what is `b` when you want `n` numbers starting at 0?), and `List.init` builds from a length and a function.
</details>
<details><summary>Exercise 1.1 (range) — near-solution (try the others first)</summary>
```js
export const range = (n) => Array.from({ length: ___ }, (___, i) => ___)
```
```scala
def range(n: Int): List[Int] = (0 until ___).___
```
```fsharp
let range (n: int) : int list = List.init ___ (fun i -> ___)
```
</details>
<details><summary>Exercise 1.2 (dropLast) — nudge</summary>
Which non-mutating method gives you "a piece of the array from here to there"? Can its "to" index count from the end?
</details>
<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)
</details>
<details><summary>Exercise 1.2 (dropLast) — near-solution (try the others first)</summary>
```js
export const dropLast = (xs) => xs.slice(___, ___)
```
```scala
def dropLast[A](xs: List[A]): List[A] = xs.dropRight(___)
```
```fsharp
let rec dropLast (xs: 'a list) : 'a list =
match xs with
| [] | [ _ ] -> ___
| x :: rest -> ___ :: ___
```
Note the `rec` in F#: recursive functions must say so.
</details>
<details><summary>Exercise 1.3 (contains) — nudge</summary>
The question "is there some element such that ...?" has a name in every collection library.
</details>
<details><summary>Exercise 1.3 (contains) — stronger hint</summary>
JS `Array.prototype.some`, Scala `exists`, F# `List.exists`. Each takes a predicate (a function element → boolean). Build that predicate from `eq` and `p`.
</details>
<details><summary>Exercise 1.3 (contains) — near-solution (try the others first)</summary>
```js
export const contains = (xs, p, eq) => xs.some((x) => ___)
```
```scala
def contains[A](xs: List[A], p: A, eq: (A, A) => Boolean): Boolean = xs.exists(x => ___)
```
```fsharp
let contains (xs: 'a list) (p: 'a) (eq: 'a -> 'a -> bool) : bool = xs |> List.exists (fun x -> ___)
```
</details>
<details><summary>Exercise 1.4 (pipe) — nudge</summary>
You have a starting value and a list of functions. You want to go through the list, carrying a "current value" along. Which list operation carries an accumulator through a list?
</details>
<details><summary>Exercise 1.4 (pipe) — stronger hint</summary>
It is a fold: `fns.reduce(step, x)`. The accumulator is the current value; each step applies the next function to it. With no functions, `reduce` returns the initial value, which is exactly the required behaviour.
</details>
<details><summary>Exercise 1.4 (pipe) — near-solution (try the others first)</summary>
```js
export const pipe = (x, ...fns) => fns.reduce((acc, f) => ___, ___)
```
</details>
<details><summary>Exercise 1.5 (compose) — nudge</summary>
`compose` returns a function. Inside that function you have the argument and the list of functions. How is that like `pipe`, and in which direction must you walk the list?
</details>
<details><summary>Exercise 1.5 (compose) — stronger hint</summary>
`compose(f, g, h)(x)` is `f(g(h(x)))`: apply the *last* function first. `reduceRight` walks an array from the end. (Alternatively: `toReversed()` and reuse your `pipe`.)
</details>
<details><summary>Exercise 1.5 (compose) — near-solution (try the others first)</summary>
```js
export const compose = (...fns) => (x) => fns.reduceRight((acc, f) => ___, ___)
```
</details>
<details><summary>Exercise 1.6 (deepFreeze) — nudge</summary>
Write the simple version first: freeze the object, then call `deepFreeze` on each of its values (`Object.values` works for arrays too). Then think about the two stopping conditions: values that are not objects, and objects you are already inside of.
</details>
<details><summary>Exercise 1.6 (deepFreeze) — stronger hint</summary>
Do not use `Object.isFrozen` to decide whether to stop: that is exactly what breaks for an already-frozen parent with unfrozen children. To detect a cycle without mutable state, give the function a second parameter with a default value: the list of **ancestors** (the objects on the path from the root down to here). A cycle means the current object is one of its own ancestors. Each recursive call passes a *new* list with the current object added.
</details>
<details><summary>Exercise 1.6 (deepFreeze) — near-solution (try the others first)</summary>
```js
export const deepFreeze = (obj, ancestors = []) => {
if (obj === null || typeof obj !== ___ || ancestors.includes(___)) return obj
Object.freeze(obj)
Object.values(obj).forEach((v) => deepFreeze(v, [___, ___]))
return ___
}
```
</details>
## Check your understanding
1. Is `const now = () => Date.now()` referentially transparent? Is the expression `now` (without calling it)? What is the difference?
2. Predict: `const a = Object.freeze({ xs: [1] }); a.xs.push(2); a.xs.length`. What does it evaluate to, and why? What would happen with `deepFreeze` instead?
3. `{ ...state, score: state.score + 1 }` shares the `snake` array between the old and the new state. Why is that safe here, and what single line elsewhere in the program would make it unsafe?
4. Predict `compose(pipe)(3)` and `pipe(3, compose())`. Explain using the definitions, not by running them.
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?
## In production
- **Immer** ([immerjs.github.io/immer](https://immerjs.github.io/immer/)) lets JavaScript developers write code that *looks* like mutation inside a callback, and turns it into a new immutable value with structural sharing. Redux Toolkit uses it under the hood.
- **Immutable.js** ([immutable-js.com](https://immutable-js.com/)) provides persistent `List` and `Map` types for JavaScript, like Scala's and F#'s collections.
- **Scala and F# collections** are persistent by default: [Scala's immutable collections](https://docs.scala-lang.org/overviews/collections-2.13/concrete-immutable-collection-classes.html), F#'s `list`, `Map` and `Set`.
- **Clojure** made persistent data structures the default for a whole language; Rich Hickey's talk below explains why.
## Further reading
- [Referential transparency (Wikipedia)](https://en.wikipedia.org/wiki/Referential_transparency) — the definition and its history, with examples.
- [Mostly Adequate Guide to Functional Programming](https://mostly-adequate.gitbook.io/mostly-adequate-guide/) — a free book on FP in JavaScript; chapters 3 (pure functions) and 5 (composition) match this module.
- [Why Functional Programming Matters](https://www.cs.kent.ac.uk/people/staff/dat/miranda/whyfp90.pdf) — John Hughes on why gluing small functions together makes programs modular.
- [Object.freeze (MDN)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object/freeze) — exact semantics, including why it is shallow.
- [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.
- [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.