Skip to article
ALGORITHMICSPatterns
Patterns5 min read

Interpreter

A grammar as a class hierarchy — and why you should almost always use a parser generator instead.


Users need to write filter rules: status = "paid" AND total > 100. You could add a checkbox per condition, but the combinations are open-ended, and eventually someone wants OR.

So you need a tiny language.

The pattern

One class per grammar rule, each able to evaluate itself against a context.

interface Expression {
evaluate(row: Row): boolean;
}
class Equals implements Expression {
constructor(private readonly field: string, private readonly value: unknown) {}
evaluate(row: Row) { return row[this.field] === this.value; }
}
class GreaterThan implements Expression {
constructor(private readonly field: string, private readonly value: number) {}
evaluate(row: Row) { return (row[this.field] as number) > this.value; }
}
class And implements Expression {
constructor(private readonly parts: Expression[]) {}
evaluate(row: Row) { return this.parts.every((p) => p.evaluate(row)); }
}

status = "paid" AND total > 100 becomes a tree:

const rule = new And([
new Equals('status', 'paid'),
new GreaterThan('total', 100),
]);
rows.filter((row) => rule.evaluate(row));

Change the rule and watch the tree and its verdict move together:

the tree

And
├─ Equals("status", "paid")
└─ GreaterThan("total", 100)

rows it matches

  • #1 paid · 240
  • #2 paid · 65
  • #3 draft · 900
  • #4 refunded · 120
  • #5 paid · 101

Two terminal nodes do the comparing; the non-terminal combines their answers. Evaluating the rule is one recursive walk — and the tree is data, so it could equally have come from a database row instead of from these controls.

Terminals (Equals, GreaterThan) do the work. Non-terminals (And, Or, Not) combine children. That is Composite with an evaluate method, which is precisely what this pattern is.

Why it is the least-used pattern in the book

It does not scale with the grammar. One class per rule means a real language is dozens of classes. C’s grammar would be hundreds.

It is slow. Walking a tree of objects, with a virtual call per node, is roughly an order of magnitude slower than a bytecode loop and further still from compiled code.

Parsing is the hard part and you are on your own. Precedence, associativity, error messages with useful positions — none of it is addressed here, and all of it is where the bugs live.

What to do instead

For expressions users type, use an existing library. CEL, JSONLogic and JEXL are sandboxed, bounded, and have had their edge cases found by other people.

For a real language, use a parser generator — ANTLR, Tree-sitter, or a hand-written recursive-descent parser producing a plain AST. Then walk that AST with a visitor, which is the same evaluation step with a better separation.

For configuration, consider whether you need a language at all. A structured format — a list of {field, op, value} objects — is often the real requirement, and it can be validated with a schema, stored in a database, and rendered as a UI. None of which is true of a string.