A Third Interpretation
One more. Write count-lits — a function that counts how many literal values appear in an expression.
lit(5)has1literalmakeAdd(lit(2), lit(3))has2literalsmakeAdd(lit(2), makeMul(lit(3), lit(4)))has3literals
Same structure as compute and pretty: check the tag, recurse on children. For "lit", return 1. For "add" and "mul", add the counts of both children.
What You Have Built
One expression. Three interpretations:
evaluate(call("compute", "e")) // 14
evaluate(call("pretty", "e")) // "(2 + (3 * 4))"
evaluate(call("count-lits", "e")) // 3
The expression never changed. The interpretation did. Each interpreter has the same structure: check the tag, handle each case, recurse. The only difference is what each tag means.
Now look at your evaluator from the first problem:
return match(expr, {
"n": (n) => n,
"l + r": (l, r) => evaluate(l) + evaluate(r),
});
The rules object is an interpreter. match walks the tree and dispatches to the right rule. You have been writing tagged interpreters since step one.
An expression is just data. An interpreter gives it meaning. Change the interpreter, change the meaning.