Cons List (Closures as Data)

Context: FIT2102_MOC · the W1 tutorial’s punchline — a data structure made of nothing but functions, proving HOFs are independent of any built-in container · closures come from JavaScript Functions as Values; the same chain shape, done with objects, is List (ADT)

Quick Revision

  • 🎯 Objective: hide a pair inside a closure and read it back by handing the closure a selector ➔ a linked list with no class, no object literal, no mutation.
  • 📦 Core Components: cons ➔ closure capturing (head, rest) | head/rest ➔ selectors asking the closure a question | recursive map/filter/reduce ➔ the Array HOFs re-derived for a type the language has never seen.
  • ⚡ Key Constraint: the list IS a functioncons(1, null) returns a callable, so console.log(list) prints source text, and the terminator is null, which is not callable ⟹ head(null) must throw before it applies anything.

📝 How It Works

1. The Pair, Encoded as a Closure

  • Core Mechanism: Store, don’t exposepair(a, b) returns sel => sel(a, b). The two values live only in the captured scope; there is no field, no .head, no way in except by supplying a function.
  • The caller chooses the projectionfst and snd differ only in which argument their selector returns. The structure has no accessors — the accessor is passed in.
  • Type signature. Read it as: give me two values, I give you back something that answers questions of the form .
  • Why this is a paradigm point ➔ objects and functions are interchangeable encodings of state; JavaScript already had everything needed for data abstraction before it had classes ➔ Programming Paradigms.

2. Cons — the Pair, Right-Nested

  • Core Mechanism: A node is a pair of head and resthead is the stored value, rest is the rest of the list (itself a cons, or null).
  • Shapecons(1, cons(2, cons(3, null))) nests to the right; the whole list is one outermost closure, and each rest call peels exactly one layer.
  • Selectorshead(l) applies l to a selector that returns its first argument; rest(l) applies l to one that returns its second. Two functions, one line each, and the type is complete.
  • Type signature where is a cons list of or null.

3. HOFs Re-derived over Cons

  • The universal skeleton ➔ base case on null ➔ act on head(list) ➔ recurse on rest(list)recombine with cons (or fold into an accumulator). Every cons HOF is that shape with one slot changed.
  • What changes per HOFmap transforms the head then re-conses · filter decides whether to re-cons the head or drop it · reduce never re-conses, it threads an accumulator.
  • Tail-position splitmap and filter are non-tail (the cons runs after the call returns); reduce is tail (the accumulator goes forward) ➔ Recursion.
  • The pointmap/filter/reduce are not “array methods” — they are shapes of recursion that any inductively defined type admits.

4. Purity and Referential Transparency

  • Nothing is ever mutatedmap and filter build a new chain; the original list is untouched and still valid afterwards.
  • Structural sharingrest(l) is not a copy — it is the same closure the original already held, so tails are shared for free. Immutability makes sharing safe.
  • Referential transparencyhead(l) returns the same value on every call for the same l, so any occurrence can be replaced by its value without changing the program’s meaning.

⚙️ Core Implementation

🔹 Selector encoding (pair first, list second)

🔹 The given traversal template

⚖️ Core Decision Matrix

EncodingHow a node stores dataCost of head / restWhat it buysWhat it costs
JS Arraycontiguous indexed slots random accessbuilt-in map/filter/reduce, printablemap copies the whole array each stage; no tail sharing
Object linked list ({head, rest})named fields on a record field readreadable, debuggable, JSON.stringify-ablefields are mutable by default ⟹ purity is a convention, not a guarantee
Closure cons (this note)captured scope + selector call, one closure invocationinaccessible except through the interface ⟹ immutability is enforced by the encoding; free tail sharingopaque to the debugger; traversal for anything positional; recursion depth is the list length

When It Flips: the closure encoding wins whenever the lesson is that data and behaviour are the same substance — which is why it appears in W1 and again in lambda calculus, where there are no records at all, only functions. For production JS you would use an Array; the closure form's payoff is conceptual, plus genuinely free structural sharing.

📊 Exam Execution Trace

Manual Execution Trace

Evaluating head(rest(cons(1, cons(2, null)))), where cons(h, r) = sel => sel(h, r):

StepExpression being reducedClosure appliedCaptured (h, r)Result
0 (Init)cons(2, null)closure
1cons(1, C₂)closure
2rest(C₁) applied to (_, r) => r
3head(C₂) applied to (h, _) => h2

Reading: each selector application discards one of the two captured values. No data structure was traversed — a function call was the traversal.

✍️ Practice

⚠️ Common Mistakes

  • 💡 Forgetting to re-cons in map ➔ recursing and returning the mapped head alone (or pushing into an array) produces a value of the wrong type. map over a cons list must return a cons list: apply f to the head, then cons it onto the mapped rest.
  • 💡 The filter reject branch must return the recursion, not null ➔ dropping a value means skipping it and continuing with the filtered rest; returning null truncates the list at the first rejected element and silently passes any test whose data happens to reject nothing early.
  • 💡 reduce’s parameter order is (acc, value) ➔ swapping them still runs and still returns a number for +, so a sum test passes while a non-commutative reducer (subtraction, string concat, list building) is quietly wrong. The tutorial’s own reduce test uses (acc, x) => x - acc precisely because it detects this.
  • 💡 head(null) / rest(null)null is not a function, so applying it throws a bare TypeError from deep inside the call chain. Guard at the top of the selector with an explicit throw so the error names the actual problem.
  • 💡 No tail-call optimisation in V8 ➔ a cons list long enough to matter overflows the stack in Chrome even for the tail-recursive reduce. The encoding is stack frames, not Recursion. (Not from the slides — but the unit tells you to run in Chrome.)

🧠 Active Recall