all_lessons/SICP JavaScript/07 · Multiple representationslesson 7 / 22

Multiple representations of data

Lesson 06 ended on a warning: one symbolic object — a set, a polynomial, a number — can be represented in several incompatible ways, each with its own cost profile, and a tower of ifs asking "which representation is this?" does not scale. The constructor/selector barrier from Lesson 04 hid one representation behind a contract. This lesson breaks the assumption that there is only one. We attach a type tag to each datum, and replace the tower of conditionals with a table of operations indexed by typedata-directed programming. The payoff is additivity: you can install an entirely new representation by adding rows to a table, editing not one line of code that already exists.

Book source
Maps to SICP 2.4 (multiple representations for abstract data): tagged data, the operation/type table, apply-generic, and message passing. We use complex numbers in rectangular and polar form as the running example, exactly as the book does in spirit, but every trace, table, and the third-representation worked example are original and written in JavaScript.
Linear position
Prerequisite: Lesson 06 (one symbolic object admits many representations) and Lesson 04 (constructor/selector abstraction barriers separate clients from a representation).
New capability: support several coexisting representations of one abstract object behind a single set of selectors, by tagging data with its type and dispatching operations through a table — and add a new representation without touching existing code.
The plan
Five moves. (1) Set up the abstract object — a complex number — and its two honest representations, rectangular and polar. (2) Try the obvious fix, explicit dispatch: one generic selector per operation, branching on a tag, and watch it fail to be additive. (3) Invert the structure into an operation/type table driven by one universal apply_generic. (4) Demonstrate additivity by installing a third representation and changing zero existing lines. (5) Show message passing as the dual: instead of a table indexed by (op, type), let each datum be a function that answers operation names.

1 · One abstract object, two honest representations

A complex number z = a + b·i can be described two equally valid ways. Rectangular form stores the real part a and imaginary part b. Polar form stores the magnitude r and angle θ, where a = r·cos θ and b = r·sin θ. Neither is "the truth"; each makes some operation cheap. Addition is trivial in rectangular form (add components); multiplication is trivial in polar form (multiply magnitudes, add angles). A serious system wants both available at once.

Whatever the storage, the four selectors are the same abstract interface, exactly the contract idea from Lesson 04:

// the abstract object's contract — clients only ever call these four
real_part(z)   // the real component a
imag_part(z)   // the imaginary component b
magnitude(z)   // the radius r
angle(z)       // the angle θ

So that we can tell the two representations apart at runtime, we tag each datum: glue a type symbol onto the front. A tagged datum is just a pair (tag, contents).

const attach_tag = (tag, contents) => ({ tag, contents });
const type_tag    = d => d.tag;
const contents    = d => d.contents;

// rectangular: store [a, b]
const make_from_real_imag_rect = (a, b) => attach_tag("rect", [a, b]);
// polar: store [r, theta]
const make_from_mag_ang_polar  = (r, t) => attach_tag("polar", [r, t]);

2 · The obvious fix and why it is not additive

The first instinct is one generic selector per operation that asks the tag and branches. Call this explicit dispatch:

const real_part = z => {
  if (type_tag(z) === "rect")  { const [a, b] = contents(z); return a; }
  if (type_tag(z) === "polar") { const [r, t] = contents(z); return r * Math.cos(t); }
  throw new Error("unknown type: real_part");
};
const magnitude = z => {
  if (type_tag(z) === "rect")  { const [a, b] = contents(z); return Math.sqrt(a*a + b*b); }
  if (type_tag(z) === "polar") { const [r, t] = contents(z); return r; }
  throw new Error("unknown type: magnitude");
};
// ... and the same shape for imag_part and angle

This works, and for two representations it even looks tidy. But measure what it costs to grow. The structure is a matrix of operations × types, and explicit dispatch slices it the wrong way: each operation contains a branch for every type. Add one type and you must reopen and edit every operation.

The failure — adding a representation forces edits everywhere
Suppose you add a third representation, "unit" (complex numbers known to lie on the unit circle, stored as just an angle). To support it you must reopen real_part, imag_part, magnitude, and angle and add a "unit" branch to each. Four functions edited for one new type. With m operations and a new type, that is m edits to working, tested, shipped code — and every edit is a chance to break the other types. Worse, two teams adding two representations both edit the same four functions and collide in source control. The design is not additive: new capability is not added, it is spliced into existing code. There is also a hidden global hazard — every author must use distinct tags, or one team's "polar" silently shadows another's.

3 · Inverting the structure — the operation/type table

The fix is to stop hard-coding the matrix into the operations and instead store it in a table indexed by (operation name, type tag). Each cell holds the implementation. A representation registers its own column of the table by calling put; one universal dispatcher, apply_generic, reads the right cell by looking up the datum's tag.

// a 2-D table keyed first by operation name, then by type tag
const _table = new Map();
const put = (op, type, fn) => { _table.set(op + "|" + type, fn); };
const get = (op, type)     => _table.get(op + "|" + type);

// the ONE dispatcher every generic selector funnels through
const apply_generic = (op, ...args) => {
  const tag = type_tag(args[0]);
  const fn  = get(op, tag);
  if (!fn) throw new Error(`no method ${op} for type ${tag}`);
  // strip the tag, call the bare implementation on raw contents
  return fn(...args.map(contents));
};

// the generic selectors are now one line each — no branching, ever
const real_part = z => apply_generic("real_part", z);
const imag_part = z => apply_generic("imag_part", z);
const magnitude = z => apply_generic("magnitude", z);
const angle     = z => apply_generic("angle", z);

Each representation lives in its own self-contained install function that registers its column and never mentions any other type:

const install_rectangular = () => {
  put("real_part", "rect", ([a, b]) => a);
  put("imag_part", "rect", ([a, b]) => b);
  put("magnitude", "rect", ([a, b]) => Math.sqrt(a*a + b*b));
  put("angle",     "rect", ([a, b]) => Math.atan2(b, a));
};
const install_polar = () => {
  put("real_part", "polar", ([r, t]) => r * Math.cos(t));
  put("imag_part", "polar", ([r, t]) => r * Math.sin(t));
  put("magnitude", "polar", ([r, t]) => r);
  put("angle",     "polar", ([r, t]) => t);
};
install_rectangular();
install_polar();

The matrix is the same; we only changed which way we slice it. Explicit dispatch sliced by operation (each op knows every type). Data-directed slices by type (each type's install knows every op, and ops know nothing).

Worked trace — magnitude of a polar number
Evaluate magnitude(make_from_mag_ang_polar(5, 0.3)):
make_from_mag_ang_polar(5, 0.3)
        -> { tag: "polar", contents: [5, 0.3] }

magnitude(z)
   = apply_generic("magnitude", z)
        tag = type_tag(z)          -> "polar"
        fn  = get("magnitude","polar")  -> ([r,t]) => r
        fn(contents(z))            -> fn([5, 0.3])  -> 5
   = 5
No if ran. The dispatch was a single table lookup keyed by the tag the datum carried with it. The selector magnitude did not know polar numbers exist — it asked the table.

4 · Additivity — install a third representation, change nothing

Now redo the experiment that broke explicit dispatch: add the "unit" representation (a complex number on the unit circle, stored as a lone angle). With the table, this is one new self-contained block — and crucially, no existing function or install changes.

const make_from_angle_unit = t => attach_tag("unit", [t]);

const install_unit = () => {
  put("real_part", "unit", ([t]) => Math.cos(t));   // r = 1
  put("imag_part", "unit", ([t]) => Math.sin(t));
  put("magnitude", "unit", ([t]) => 1);
  put("angle",     "unit", ([t]) => t);
};
install_unit();   // the ONLY new top-level call

// works immediately through the same selectors — no edits upstream
magnitude(make_from_angle_unit(0.3));   // -> 1
real_part(make_from_angle_unit(0));     // -> 1
This is the whole point
Under explicit dispatch, the third type cost m edits to m existing operations. Under the operation/type table it cost one new block and zero edits. The generic selectors real_part/magnitude were written before "unit" existed and still serve it correctly. New capability was added, not spliced. That is additivity, and it is the property that lets large systems with many representations be built — and extended by independent teams — without a combinatorial edit storm.

5 · The dual view — message passing

The table is indexed by (operation, type). There are two ways to decompose a table into code: fix the column and vary the row (that is the install functions above — group by type), or make each datum itself a procedure that, given an operation name, returns the answer. The latter is message passing: the datum is its own dispatch.

// a polar number that responds to operation-name "messages"
const make_from_mag_ang_msg = (r, t) => (op) => {
  if (op === "real_part") return r * Math.cos(t);
  if (op === "imag_part") return r * Math.sin(t);
  if (op === "magnitude") return r;
  if (op === "angle")     return t;
  throw new Error(`unknown message ${op}`);
};
// the generic selector now just sends the message
const apply_generic_msg = (op, z) => z(op);
const magnitude2 = z => apply_generic_msg("magnitude", z);

magnitude2(make_from_mag_ang_msg(5, 0.3));   // -> 5

This is the same matrix decomposed the other way — and it is the seed of object orientation: a "message-passing object" bundles state with the operations that respond to it. The table view makes adding an operation easy (add a row); the message-passing view makes adding a type easy (write one new closure). Hold that asymmetry — it returns, sharpened, in Lesson 08.

Common mistakes / failure modes

Calling into another type from inside an install
If install_polar references "rect", the modules are coupled again and additivity is lost. Each install must mention only its own tag. Cross-type work belongs above the barrier (the topic of Lesson 08's coercion).
Forgetting to strip the tag
apply_generic hands the implementation the contents, not the tagged datum. A method written to expect the tagged form re-tags and the abstraction barrier leaks. Keep implementations operating on raw contents only.
Tag collisions across modules
Two teams both using "polar" overwrite each other's table cells silently. Tags are a shared global namespace; treat them like one (namespacing, registration discipline).
Leaving one stray conditional
A single if (type_tag(z) === ...) surviving in a selector quietly reintroduces the non-additive coupling for that one operation. Data-directed dispatch only pays off if every selector funnels through the table.

Checkpoint exercise

Try it
You are given the working table-driven system above (rectangular + polar installed). Tasks:
// (a) Add a generic operation, not a type: a constructor-agnostic
//     `add_complex(z1, z2)` built ONLY from the four selectors.
// (b) Then install a brand-new type "imag_only" (purely imaginary,
//     stored as just the imaginary part b) WITHOUT editing add_complex.
Answers. (a) Because the selectors are generic, add_complex needs no dispatch of its own and works for every type, present and future:
const add_complex = (z1, z2) => make_from_real_imag_rect(
  real_part(z1) + real_part(z2),
  imag_part(z1) + imag_part(z2));
(b) One new self-contained block; add_complex is never touched, and it immediately accepts "imag_only" values:
const make_imag_only = b => attach_tag("imag_only", [b]);
const install_imag_only = () => {
  put("real_part", "imag_only", ([b]) => 0);
  put("imag_part", "imag_only", ([b]) => b);
  put("magnitude", "imag_only", ([b]) => Math.abs(b));
  put("angle",     "imag_only", ([b]) => b >= 0 ? Math.PI/2 : -Math.PI/2);
};
install_imag_only();
Note the asymmetry the exercise reveals: adding the operation add_complex was free across all types; adding the type imag_only was free across all operations — but only because each axis was made data-directed. Lose either and one of these tasks becomes an edit storm.

Where this points next

We taught a single family — complex numbers — to wear several representations, dispatched through one table. But a real arithmetic system has whole kinds of number — ordinary, rational, complex — and you want add to mean the right thing for each, and you want to stack them: a complex number whose parts are themselves rational. The dispatch mechanism we just built does not stop at one family; it generalizes into a whole layered system of generic operations across many types, with one twist this lesson dodged — what happens when you add a rational to a complex, two different tags? Lesson 08 builds the generic-arithmetic tower on top of apply_generic, introduces coercion between types, and confronts the tension this lesson only hinted at in §5: making it easy to add a new type versus a new operation — the expression problem, in embryo.

Takeaway
One abstract object can have many honest representations (complex numbers as rectangular vs polar), each cheap for different operations. Explicit dispatch — a generic selector per operation that branches on a type tag — works but is not additive: adding one representation forces edits to every operation, because it slices the operation×type matrix by operation. Data-directed programming inverts this: tag each datum with its type, store implementations in an operation/type table, and route every selector through one apply_generic that looks up the cell by the datum's tag. Now a new representation is one self-contained install block that registers its column and touches nothing else — change zero existing lines, as the third ("unit") representation demonstrated. Message passing is the dual decomposition: make each datum a closure that answers operation-name messages — the seed of objects. The two views trade off which axis is cheap to extend: a row (operation) versus a closure (type).

Interview prompts