Behavioral Pattern Pattern Deep Dive

Interpreter

Think of a simple calculator that understands expressions like 3 + 4 * 2. Under the hood, it doesn't just scan the string character by character with a pile of special cases — it builds a small tree out of the pieces ("multiply 4 and 2," "add 3 to that result") and evaluates the tree. The Interpreter pattern formalizes that idea: represent each rule of a simple grammar as its own class with an interpret() method, and build complex expressions by composing simpler expression objects into a tree.

⚠️ The Problem

Say you're building a basic rules engine that needs to evaluate conditions like "age > 18 AND hasLicense" against a user object, and this kind of condition string shows up in several places in your app. The tempting approach is ad hoc string parsing — splitting on "AND", checking for comparison operators with regex, handling "OR" as another special case — scattered across whatever code happens to need it. As soon as the grammar grows even slightly (nested parentheses, a new operator, a new comparison type), this hand-rolled parsing logic becomes fragile and duplicated.

✅ The Solution

Define a common Expression interface with one method, interpret(context). Each grammar rule gets its own class implementing that interface — a NumberExpression just returns its value, an AddExpression holds two child expressions and returns the sum of interpreting each of them. Complex expressions are built by composing simpler ones into a tree: AddExpression(NumberExpression(3), MultiplyExpression(NumberExpression(4), NumberExpression(2))). Interpreting the root of the tree recursively interprets every node beneath it, evaluating the whole expression in one consistent way regardless of how deep or complex it is.

Example 1 — an arithmetic expression tree evaluator

Primary Example — JavaScript
class NumberExpression {
  constructor(value) {
    this.value = value;
  }
  interpret() {
    return this.value;
  }
}

class AddExpression {
  constructor(left, right) {
    this.left = left;
    this.right = right;
  }
  interpret() {
    return this.left.interpret() + this.right.interpret();
  }
}

class MultiplyExpression {
  constructor(left, right) {
    this.left = left;
    this.right = right;
  }
  interpret() {
    return this.left.interpret() * this.right.interpret();
  }
}

// Represents: 3 + (4 * 2)
const expression = new AddExpression(
  new NumberExpression(3),
  new MultiplyExpression(new NumberExpression(4), new NumberExpression(2))
);

console.log(expression.interpret()); // 11

Nothing in this code parses a raw string at runtime — the tree itself already represents the expression's structure. Adding a new operation (say, SubtractExpression) means writing one more small class with the same interpret() shape; nothing else in the tree needs to change.

Example 2 — a secondary context: a simple rule engine

Secondary Example — Python
class Expression:
    def interpret(self, context):
        raise NotImplementedError

class GreaterThanExpression(Expression):
    def __init__(self, field, value):
        self.field = field
        self.value = value

    def interpret(self, context):
        return context.get(self.field, 0) > self.value

class HasFlagExpression(Expression):
    def __init__(self, field):
        self.field = field

    def interpret(self, context):
        return bool(context.get(self.field, False))

class AndExpression(Expression):
    def __init__(self, left, right):
        self.left = left
        self.right = right

    def interpret(self, context):
        return self.left.interpret(context) and self.right.interpret(context)

class OrExpression(Expression):
    def __init__(self, left, right):
        self.left = left
        self.right = right

    def interpret(self, context):
        return self.left.interpret(context) or self.right.interpret(context)

# Represents: age > 18 AND hasLicense
rule = AndExpression(
    GreaterThanExpression("age", 18),
    HasFlagExpression("hasLicense"),
)

applicant = {"age": 21, "hasLicense": True}
print(rule.interpret(applicant))  # True

rejected_applicant = {"age": 16, "hasLicense": True}
print(rule.interpret(rejected_applicant))  # False

The rule itself — age > 18 AND hasLicense — is expressed entirely as a tree of small objects, each of which knows how to evaluate itself against a context. New condition types (an InExpression, a NotExpression) slot in the same way, without touching the classes that already exist.

👍 When To Use It

  • You have a small, stable, well-defined grammar that needs to be evaluated repeatedly — simple rule conditions, basic arithmetic, small configuration expressions.
  • You want the grammar's rules to be individually extensible — adding a new kind of expression should mean adding a new class, not editing a shared parser function.
  • The grammar is genuinely simple — Interpreter trees are a great fit for small, well-bounded languages, not general-purpose ones.

👎 When NOT To Use It

  • When the grammar is anything beyond small and simple — expression trees get slow and unwieldy fast as grammar complexity grows, since every rule becomes another class and every evaluation walks the whole tree recursively.
  • When you need real parsing features — operator precedence, error recovery, detailed syntax errors — a dedicated parser generator (ANTLR, PEG.js) or parser-combinator library handles this far better than a hand-built expression tree.
  • When an existing expression library already covers your use case — most mainstream languages have mature libraries for exactly this (evaluating math expressions, JSON-based rule conditions) that are better tested than a one-off Interpreter implementation.

❓ FAQ

Is this how real programming language parsers and compilers work?

The general shape — build a tree, then evaluate it — is the same idea behind an abstract syntax tree (AST), which every real compiler and interpreter uses. But production language tooling adds a lot Interpreter alone doesn't cover: proper tokenizing/parsing stages, error recovery, and usually a separate, more efficient execution step rather than naive tree-walking. Interpreter-the-pattern is closer to "the smallest possible version of that idea," useful for small embedded grammars, not full languages.

Is Interpreter something people actually hand-build today?

Rarely, beyond small, simple grammars. For anything beyond a toy expression language or a handful of rule conditions, most real-world parsing uses dedicated parser generators (ANTLR, PEG.js) or existing expression/rule-engine libraries rather than a hand-rolled tree of Expression classes — hand-built interpreter trees get slow and hard to maintain as the grammar grows, since every new syntax feature means another class and the tree-walking evaluation doesn't scale well to complex grammars. Interpreter is genuinely useful, but its sweet spot is intentionally small.

How is this different from just using eval() or a scripting engine?

eval() and embedded scripting engines run a full general-purpose language, with all the power (and risk — arbitrary code execution) that implies. Interpreter is usually used for something much narrower and safer: a tiny, fixed grammar you fully control, evaluated by objects you wrote, with no ability to execute arbitrary code beyond what your Expression classes explicitly support.

🙋 There Are No Dumb Questions

Q: Doesn't building a tree of tiny classes seem like overkill compared to just writing an if/else chain to check conditions?
A: For a handful of fixed conditions, an if/else chain is often genuinely simpler and that's fine. Interpreter starts paying off once the grammar has real structure — nested conditions, operator combinations, reusable sub-expressions — where a tree of composable objects scales better than a string of boolean checks glued together with && and ||.

✅ Quick Check

In the Interpreter pattern, how is a complex expression represented?


Next up: what if code that loops over a collection shouldn't need to know whether that collection is an array, a linked list, or a tree underneath? That's the Iterator pattern.

← Back to
Next →