all_lessons/SICP JavaScript/06 · Symbolic datalesson 6 / 22

Symbolic data

Lesson 05 gave us unbounded hierarchical structure and a clean way to process it — but every leaf, every accumulate, every result, bottomed out in a number to be evaluated. That hid a rule we never questioned: data must be evaluated to its value. This lesson lifts exactly that rule. With quotation we can refer to a symbol itself — hold the name x rather than the number it stands for — and the moment we can, the same pairs and the same conventional interfaces from Lesson 05 begin processing representations: an algebraic expression to be differentiated, a set, a Huffman code. Programs stop computing with quantities and start computing with the structure of expressions.

Book source
Maps to SICP 2.3 (symbolic data): quotation, symbolic differentiation as structural rewriting and the need to simplify output, representing sets as unordered/ordered lists and binary trees with their cost trade-offs, and Huffman encoding trees. We follow that arc and section coverage; the JavaScript code, the differentiation trace, and the cost tables are original.
Linear position
Prerequisite: Lesson 05 (hierarchical data and conventional interfaces — pairs build trees; map/filter/accumulate process them).
New capability: represent and manipulate symbols and expressions as data — quote a name instead of evaluating it — so a program can rewrite an algebraic expression, build sets, and construct codes; and reason about the cost trade-offs of competing representations of the same abstract object.
The plan
Four moves. (1) Quotation: why we need to refer to a name, not its value. (2) Symbolic differentiation as tree rewriting, and why the output must be simplified. (3) Sets in three representations with their cost trade-offs. (4) Huffman trees, where the cost model is visible in the tree's shape.

1 · Quotation — referring to a symbol, not its value

Until now, writing x in a program meant "the value bound to x." But to do algebra we must talk about the symbol x — the thing that appears in x + 3 — without asking what number it holds. That is quotation: a way to say "take this literally as data, do not evaluate it." In Lisp it is the quote mark; in our JavaScript we represent a quoted symbol as a string and an expression as a list (built from Lesson 05's pairs).

// the SYMBOL x, as data — a string, never looked up
const x = "x";
// the EXPRESSION  x + 3  as a tagged list:  ["+", "x", 3]
const e = list("+", "x", 3);
// without quotation we could only ever store the *number* x holds;
// with it we store the name itself and inspect its structure.

This is the rule from Lesson 05 lifted: a leaf no longer has to be a value to be evaluated — it can be a symbol that stands for itself. Equality of symbols becomes meaningful ("x" === "x" asks "same name?", not "same value?"), and lists of symbols become expressions, rules, and codes. Everything below is a consequence of being able to hold structure instead of evaluating it.

2 · Symbolic differentiation — tree rewriting

Differentiation is defined by structural rules: the derivative of a sum is the sum of the derivatives; the derivative of a product follows the product rule; and so on. Each rule says "an expression of this shape rewrites to an expression of that shape." So differentiation is a tree rewrite: walk the expression tree, dispatch on its shape, build a new tree. First, selectors behind an abstraction barrier (Lesson 04) so the algorithm never touches the list layout (isPair(x) is the primitive predicate from Lesson 05 — true for a pair, false for an atom):

const isVar    = e => typeof e === "string";
const isSame   = (e, v) => e === v;
const isSum    = e => isPair(e) && head(e) === "+";
const isProd   = e => isPair(e) && head(e) === "*";
const addend   = e => head(tail(e));            // a in ["+", a, b]
const augend   = e => head(tail(tail(e)));      // b
const multiplier   = e => head(tail(e));
const multiplicand = e => head(tail(tail(e)));

function deriv(e, v) {
  if (typeof e === "number") return 0;                 // d/dv c = 0
  if (isVar(e))   return isSame(e, v) ? 1 : 0;          // d/dv v = 1, else 0
  if (isSum(e))   return makeSum(deriv(addend(e), v),   // sum rule
                                 deriv(augend(e), v));
  if (isProd(e))  return makeSum(                       // product rule
        makeProd(multiplier(e),   deriv(multiplicand(e), v)),
        makeProd(deriv(multiplier(e), v), multiplicand(e)));
  throw new Error("unknown expression");
}
Worked example — differentiate x * y with respect to x
Expression tree: ["*", "x", "y"]. It is a product, so apply the product rule (uv)' = u·v' + u'·v:
deriv(["*","x","y"], "x")
  = makeSum( makeProd("x",            deriv("y","x")),    // u * v'
             makeProd(deriv("x","x"), "y") )              // u' * v
  = makeSum( makeProd("x", 0),                            // deriv("y","x") -> 0
             makeProd(1,   "y") )                         // deriv("x","x") -> 1
With naive constructors (makeProd = (a,b) => list("*",a,b)) this is the literal, ugly tree ["+", ["*","x",0], ["*",1,"y"]] — algebraically x*0 + 1*y, i.e. y, but unreduced. The algorithm is correct yet the output is unreadable.
Representation choices leak — so simplify in the constructors
The fix is to put algebraic simplification inside the constructors, below the barrier — exactly the Lesson 04 move of pushing policy below the line:
function makeSum(a, b) {
  if (a === 0) return b;            // 0 + b = b
  if (b === 0) return a;
  if (typeof a === "number" && typeof b === "number") return a + b;
  return list("+", a, b);
}
function makeProd(a, b) {
  if (a === 0 || b === 0) return 0; // 0 * _ = 0
  if (a === 1) return b;            // 1 * b = b
  if (b === 1) return a;
  if (typeof a === "number" && typeof b === "number") return a * b;
  return list("*", a, b);
}
Now the same deriv call collapses x*0 -> 0 and 1*y -> y, then 0 + y -> y, returning just "y". The deep point: differentiation produced a correct but badly-shaped representation, and which shape you get depends on how the constructors build it. The choice of representation leaks into the usefulness of the result — correctness is not the same as a good representation.

3 · Representing sets — three layouts, three cost models

A set is an abstract object (a collection with no duplicates) with operations isMember, adjoin, union, intersection. Lesson 04 told us one abstract object admits many representations behind one barrier; here three layouts honor the same contract but cost wildly different amounts. n is the set size.

RepresentationisMemberadjoinintersectiontrade-off
Unordered listΘ(n)Θ(n) (must check dup)Θ(n²)simplest; scans the whole list every time
Ordered listΘ(n) avg (stop early)Θ(n)Θ(n) (merge-walk)sortedness lets membership stop early and lets intersection walk two lists once
Balanced binary treeΘ(log n)Θ(log n)Θ(n)fastest lookups; needs balancing or it degrades to a list
// unordered-list membership: linear scan
function isMember(x, set) {
  if (isEmpty(set)) return false;
  return head(set) === x ? true : isMember(x, tail(set));
}
// ordered-list membership: stop as soon as we pass x
function isMemberOrdered(x, set) {
  if (isEmpty(set) || head(set) > x) return false;   // can't be further on
  return head(set) === x ? true : isMemberOrdered(x, tail(set));
}

The abstract operations are identical; the cost model is the representation's signature. Choosing a layout is choosing which operation you want to be cheap — and there is no free lunch (the tree's fast lookup costs balancing discipline). This is the same lesson as rational reduction (04), now with the trade-off made quantitative.

4 · Huffman trees — the cost model is the shape

A Huffman encoding tree assigns short bit-codes to frequent symbols and long codes to rare ones, minimizing total message length. It is built bottom-up: repeatedly merge the two lowest-weight nodes into a parent whose weight is their sum, until one tree remains. A symbol's code is the path from the root (left = 0, right = 1); frequent symbols end up near the root with short paths.

freqs: A=8  B=3  C=1  D=1            merge two smallest each step
                                      ((C1 D1)=2)  then (B3 . 2)=5  then (A8 . 5)=13
        ( . )13
        /     \
      A=8     ( . )5          A -> 0          (1 bit, most frequent)
              /    \          B -> 10         (2 bits)
            B=3   ( . )2      C -> 110        (3 bits, rarest)
                  /    \      D -> 111        (3 bits)
                C=1    D=1

The encode/decode procedures are tree walks over this structure (a perfect fit for Lesson 05's tree recursion, operating on symbols quoted as data from §1). The point worth keeping: the cost model is visible in the tree's geometry. Depth equals code length; merging the two smallest weights at each step is precisely what pushes rare symbols deep and frequent ones shallow, so the shape is the optimization. Reading the tree, you read the cost. Different symbol frequencies grow a different-shaped tree — a different representation of the same code idea, each tuned to its data.

Common mistakes / failure modes

Evaluating instead of quoting
Treating x as a variable lookup when you meant the symbol. Symbolic code must hold the name as data; the moment it evaluates, it computes a number and the structure is gone.
Naive constructors that never simplify
Building ["+", ["*","x",0], ["*",1,"y"]] and shipping it. The algorithm is correct but the representation is unusable; simplification belongs in the constructors, below the barrier.
Choosing a set layout by habit
Defaulting to an unordered list when membership is the hot path — Θ(n) where a tree gives Θ(log n). Pick the representation by which operation must be cheap, not by what is easiest to type.
Comparing symbols by identity wrongly
Using value equality where you need symbol equality (or vice versa). With quoted data, "x" === "x" means "same name"; conflating that with "same value" reintroduces the rule you just lifted.

Checkpoint exercise

Try it
(a) Hand-trace deriv(list("+", list("*","x","x"), "x"), "x") (that is, d/dx of x*x + x) using the simplifying constructors, and state the final expression. (b) For a set used mostly for membership tests on a large, rarely-changing collection, which of the three representations do you pick and why? (c) In the Huffman tree above, decode the bit string 0 10 111.

Answers: (a) sum rule splits into deriv(x*x) + deriv(x). deriv(x*x,x) = makeSum(makeProd("x",1), makeProd(1,"x")); makeProd("x",1) -> "x", makeProd(1,"x") -> "x", so that piece is makeSum("x","x") -> ["+","x","x"] (we did not implement x+x => 2*x, so it stays a sum — representation choices still leak). deriv("x",x) -> 1. Total: makeSum(["+","x","x"], 1) -> ["+", ["+","x","x"], 1], i.e. x + x + 1 (correctly 2x+1, just not collapsed). (b) Balanced binary tree — membership is Θ(log n) vs Θ(n), and the collection rarely changes so the balancing cost is amortized away. (c) 0→A, 10→B, 111→D, giving "ABD".

Where this points next

Symbolic data already showed the crack: the same abstract object — an expression, a set, a code — admits many representations with different costs and shapes. Our deriv dispatched on shape with a tower of if (isSum) … else if (isProd) …, and our set operations would need a separate version per layout. Add complex numbers in rectangular and polar form, or one more expression type, and that conditional tower grows quadratically and must be edited in every operation whenever a representation is added — it does not scale and it is not additive. Lesson 07 confronts exactly this pressure: how to support multiple representations of one object cleanly, with tagged data and data-directed programming so a new representation installs itself without editing any existing operation. The unscalable conditional dispatch we just wrote is what forces it.

Takeaway
Quotation lifts the rule that data must be evaluated: a leaf can be a symbol that stands for itself, so the pairs and conventional interfaces of Lesson 05 begin processing representations instead of quantities. Symbolic differentiation is then a tree rewrite — dispatch on an expression's shape behind selectors (Lesson 04), build a new tree by the structural rules — but it reveals that a correct algorithm can emit a badly-shaped result (x*0 + 1*y); the fix is to push algebraic simplification into the constructors below the barrier, because representation choices leak into usefulness. Sets show one abstract object under three layouts (unordered list, ordered list, balanced tree) whose cost models — Θ(n) vs Θ(log n) membership — are the representation's true signature, with no free lunch. Huffman trees make the cost model literally the geometry: depth = code length, and merging the two lowest weights pushes rare symbols deep, so the shape is the optimization. The recurring fact — one object, many representations, dispatched today by a brittle conditional tower — is the pressure that forces tagged, data-directed programming next.

Interview prompts