All questions

Persistent Immutable List

Premium

Persistent Immutable List

A persistent (immutable) list is one where every update returns a new version of the list and the old version keeps working, unchanged, forever. The trick that makes this affordable is structural sharing: the new version reuses the nodes that did not change instead of deep-copying the whole list. This is the idea behind Immutable.js, Clojure's collections, and the way Redux and React state produce a new object on each update while keeping the old one intact. You will build one backed by a singly-linked chain of immutable { value, next } cells — a cons list — where items[0] is the front. See persistent data structure for background.

Signature

function persistentImmutableList(...items: unknown[]): ImmutableList;

interface ImmutableList {
  prepend(value: unknown): ImmutableList;         // NEW list with value at the front (O(1))
  first(): unknown | undefined;                   // front value, or undefined if empty
  rest(): ImmutableList;                          // the shared tail, or the empty list
  get(i: number): unknown | undefined;            // value at index i, or undefined if out of range
  set(i: number, value: unknown): ImmutableList;  // NEW list with index i replaced; RangeError if out of range
  size(): number;                                 // element count
  isEmpty(): boolean;                             // no elements?
  toArray(): unknown[];                           // values front-to-back
}

Every method that "changes" the list returns a brand-new list; the list you called it on is never modified.

Examples

const base = persistentImmutableList(1, 2, 3);
base.toArray(); // [1, 2, 3]
base.first();   // 1
base.size();    // 3

const bigger = base.prepend(0);
bigger.toArray(); // [0, 1, 2, 3]
base.toArray();   // [1, 2, 3] — the original is untouched
const changed = base.set(1, 99);
changed.toArray(); // [1, 99, 3]
base.toArray();    // [1, 2, 3] — still intact
// The tail is shared, not copied: rest() hands back the very list we grew from.
base.prepend(0).rest() === base; // true

Notes

  • Immutable — every update returns a new list. No method mutates the list it was called on, so a version handed out earlier always reads the same.
  • O(1) prepend — adding to the front allocates a single node whose next points at the entire current list, shared by reference. Nothing is copied.
  • set copies only the prefixset(i, value) rebuilds the nodes at indices 0..i and shares everything from i + 1 onward. An out-of-range i throws a RangeError.
  • Out of rangeget(i) returns undefined for an index outside 0..size - 1; set throws instead. Mind the asymmetry.
  • The empty list — call persistentImmutableList() with no arguments for the empty list: size() is 0, first() and get() return undefined, and rest() is the empty list again.
  • Front-indexed — index 0 is the front (the head). items[0] becomes the first element; prepend and rest both act on the front.

FAQ

What is structural sharing and why does it matter?
Structural sharing means a new version of a data structure reuses the nodes that did not change instead of deep-copying them. It is what makes an immutable list affordable: prepend costs one node and set copies only the prefix, so keeping many versions around no longer costs a full copy each, and updates stay fast even on large lists.
How can prepend be O(1) if the list is immutable?
Because immutability lets you share safely. prepend allocates a single new cell whose next pointer is the entire current list, reused by reference. Nothing is copied, so the cost is constant no matter how long the list is, and the old list is untouched because its cells are never modified.
Why does set copy part of the list instead of the whole thing?
set(i, value) rebuilds only the cells from the front up to and including index i, then points the last new cell at the existing cell for index i+1, sharing the suffix. Copying the prefix is what keeps the old version intact; sharing the suffix is what keeps the update cheaper than a full copy. It touches i+1 cells rather than n.

Unlock the solution & editor

  • Runnable editor + tests

    Solve in the browser with instant Jest feedback.

  • Detailed solutions

    Walkthroughs, edge cases, and complexity notes.

  • Multi-framework variants

    React, Vue, Vanilla, Angular — same question, different stacks.

Upgrade to Premium