codejump

AcademyDesign Patterns

Interpreter pattern

Evaluating a small tree of grammar rules one node at a time, the class per rule that costs fast past a toy language, and the point a real parser beats one more class.

6 min readUpdated

3 exercises

Waiting at the end of this page.

Interpreter evaluates a small, fixed grammar (numbers, operators, keywords) represented as a tree, one grammar-rule object at a time.

Interpreter is what a Composite looks like once every node also knows how to evaluate itself. You build a tree out of small grammar-rule objects, a number, an addition, a subtraction, and evaluating the whole tree is just asking the root node to interpret() itself, which asks its children to do the same.

The problem it solves

You have a rule you want users to write themselves, such as a filter: price < 50 AND tag = js. Or a formula in a spreadsheet, a search query, a routing rule, a discount condition.

Hardcoding each variant is impossible, because users invent new combinations. Parsing the text with string splitting and if chains gets unmanageable as soon as you add parentheses or OR.

  • The rules are data, not code. They come from a user or a database at runtime.
  • They nest. A condition contains conditions.
  • They need to be evaluated repeatedly against many inputs, so re-parsing the text each time wastes work.

The fix is to represent each piece of the grammar as a small class and turn the text into a tree of those objects once. Evaluating is then a simple recursive call.

Structure

(5 + 3) - 2 as a tree. Each node interprets itself by asking its children, so evaluating the root evaluates everything
(5 + 3) - 2 as a tree. Each node interprets itself by asking its children, so evaluating the root evaluates everything

Three parts:

  • An expression interface with interpret(context).
  • Terminal expressions: the leaves (NumberExpr), which return a value directly.
  • Non-terminal expressions: the branches (AddExpr, SubtractExpr), which combine the results of their children.
class NumberExpr {
  constructor(value) { this.value = value; }
  interpret() { return this.value; }
}
class AddExpr {
  constructor(left, right) { this.left = left; this.right = right; }
  interpret() { return this.left.interpret() + this.right.interpret(); }
}
class SubtractExpr {
  constructor(left, right) { this.left = left; this.right = right; }
  interpret() { return this.left.interpret() - this.right.interpret(); }
}

// (5 + 3) - 2, built as a tree:
// new SubtractExpr(new AddExpr(new NumberExpr(5), new NumberExpr(3)), new NumberExpr(2)).interpret() → 6

It is a Composite where every node can evaluate itself. Evaluating the tree needs no special case for depth: interpret() on the root asks each child to interpret itself, and so on down.

Where it is used in real systems

User-written text is parsed once into an expression tree, which can then be evaluated against many items
User-written text is parsed once into an expression tree, which can then be evaluated against many items
Use caseThe grammar
Filters and search queriesprice < 50 AND tag = js turned into a condition tree.
FormulasSpreadsheet cells and calculators.
Rule enginesDiscounts, eligibility and routing rules written by non-developers.
Template enginesHandlebars or Liquid templates evaluated against data.
Regular expressionsA pattern compiled into a matching tree or automaton.
SQL and query languagesParsed into a plan and executed.
Configuration languagesExpressions inside YAML, GitHub Actions or feature flag rules.

Notice what these have in common: a small, well-defined language, used a lot, defined by someone other than you.

Interpreters you already use

  • Regular expressions. /a(b|c)+/ is parsed into a tree the engine walks.
  • JSON path and GraphQL. A query string is parsed into a tree, then evaluated against data.
  • Template literals and Handlebars. Text with placeholders becomes something evaluated per render.
  • CSS selectors. div > .item:hover is parsed into a tree the browser evaluates against elements.
  • Feature flag rules. Tools like LaunchDarkly evaluate targeting rules written as small expression trees.

What the pattern does not include

The classic pattern covers only the evaluation side: the classes for each rule. Turning text into the tree, the parser, is a separate job, and in practice it is the bigger part.

  • For a tiny grammar, a hand-written recursive parser is enough.
  • For anything bigger, use a parser generator or library (Peggy, Ohm, ANTLR) rather than writing one by hand.
  • Often the best choice is to avoid writing a language at all and accept structured JSON ({ "and": [...] }) that maps straight onto the tree.

When not to use it

  • The grammar is large or changes often. One class per rule scales badly. Use a parser generator and a proper evaluator.
  • Performance matters. Walking a tree of objects is slower than compiled code. Heavy workloads compile expressions to functions instead.
  • A structured config is enough. If users pick from a form, you do not need a language.
  • Security. Never evaluate user text with eval or new Function. Interpreter is the safe alternative, because it only performs the operations the grammar defines. It has to be restricted to those operations, or it becomes the same hole.
  • You need a general language. This pattern is for small languages, not for something users will write programs in.

What interviewers ask

  • "What is the Interpreter pattern?" Defining a grammar as classes and evaluating sentences by walking a tree of them.
  • "How does it relate to Composite?" It is a Composite where each node evaluates itself.
  • "Where would you use it?" A rule or filter language, a calculator, a template engine.
  • "Why not just use `eval`?" It runs arbitrary code. An interpreter only does what the grammar allows.
  • "What is the downside?" It does not scale to big grammars, and the parser is a separate problem.

The mistake to avoid

An expression class that reaches outside the tree, such as reading global state or calling a network from inside interpret(). Then the same tree gives different answers on different runs, and the grammar is no longer a pure description. Pass anything an expression needs in as a context argument, and keep interpret() a function of its children and that context.

The mechanism above, a tree whose nodes evaluate themselves recursively, is the same in JavaScript, Python or Java. Only the syntax changes.

Now practice it

1 challenge · 2 quizzes

Reading this page is the cheap half. These are the exercises that make you use it.