Interpreter Pattern

Interpreter Pattern

GoF intent (p.243): "Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language."


When NOT to Use

This is the most important section for this pattern. The Interpreter pattern does not scale and is rarely appropriate in production code.

Do not use Interpreter when:

  • The grammar has more than ~5-10 rule types. Each rule requires its own class; grammars grow quickly. Use a parser generator instead: ANTLR, PEG.js, or Lark.
  • Performance is a concern. Every expression node creates an object. Tree traversal is slow at scale. Parser generators produce far more efficient parsers.
  • The grammar changes frequently. Updating grammar rules means modifying or adding expression classes across the codebase. Parser generators centralise rule changes in a single grammar file.
  • You are building a production parser. Production parsers for SQL, query languages, templating, or DSLs should use a parser generator, not hand-coded Interpreter trees.

If you catch yourself building more than a handful of expression types, stop and use a parser generator.


When to Use

  • The grammar is simple (5 or fewer rule types) and stable — rules will not change.
  • Efficiency is not a concern — throughput and latency are acceptable with tree traversal overhead.
  • You are implementing a learning exercise or studying compiler front-end concepts.
  • You need a DSL with a very constrained expression set: simple config rule evaluators, basic query filters, permission rule engines with a fixed operator set.

How It Works

The Interpreter pattern is a recursive application of Composite-Pattern — the expression tree is a Composite tree where interpret() is the operation.

Participants:

RoleResponsibility
AbstractExpressionInterface declaring interpret(context)
TerminalExpressionLeaf — interprets a single token (no children)
NonTerminalExpressionComposite — holds child expressions; calls interpret() recursively
ContextGlobal state shared during interpretation (variable bindings, etc.)
ClientBuilds the expression tree and calls interpret() on the root

Structure: NonTerminalExpression nodes are the composite nodes; TerminalExpression nodes are the leaves. The Client assembles the tree, then a single call to root.interpret(context) walks the entire tree.


Class Diagram

classDiagram
    class AbstractExpression {
        <<interface>>
        +interpret(context : Context)*
    }
    class TerminalExpression {
        -value
        +interpret(context : Context)
    }
    class NonterminalExpression {
        -left : AbstractExpression
        -right : AbstractExpression
        +interpret(context : Context)
    }
    class Context {
        -variables : Map
        +lookup(name) value
        +assign(name, value)
    }
    class Client {
        -ast : AbstractExpression
    }
    AbstractExpression <|.. TerminalExpression
    AbstractExpression <|.. NonterminalExpression
    NonterminalExpression o-- AbstractExpression : left, right (recursive)
    Client --> AbstractExpression : builds and interprets AST
    AbstractExpression --> Context : reads/writes during interpret

    note for NonterminalExpression "Composite structure:\neach node interprets by\nrecursing on children"

TypeScript Example

Boolean expression evaluator (GoF p.243).

// AbstractExpression
interface BoolExpression {
  interpret(): boolean;
}
 
// TerminalExpression — leaf node holding a literal value
class Literal implements BoolExpression {
  constructor(private value: boolean) {}
  interpret(): boolean {
    return this.value;
  }
}
 
// NonTerminalExpression — AND
class AndExpression implements BoolExpression {
  constructor(private left: BoolExpression, private right: BoolExpression) {}
  interpret(): boolean {
    return this.left.interpret() && this.right.interpret();
  }
}
 
// NonTerminalExpression — OR
class OrExpression implements BoolExpression {
  constructor(private left: BoolExpression, private right: BoolExpression) {}
  interpret(): boolean {
    return this.left.interpret() || this.right.interpret();
  }
}
 
// Client builds the tree for: "true AND false"
const expression: BoolExpression = new AndExpression(
  new Literal(true),
  new Literal(false)
);
console.log(expression.interpret()); // false

Java Example

Same boolean expression evaluator in Java (GoF p.243).

// AbstractExpression
interface BoolExpression {
    boolean interpret();
}
 
// TerminalExpression
class Literal implements BoolExpression {
    private final boolean value;
    Literal(boolean value) { this.value = value; }
    public boolean interpret() { return value; }
}
 
// NonTerminalExpression — AND
class AndExpression implements BoolExpression {
    private final BoolExpression left, right;
    AndExpression(BoolExpression left, BoolExpression right) {
        this.left = left; this.right = right;
    }
    public boolean interpret() { return left.interpret() && right.interpret(); }
}
 
// NonTerminalExpression — OR
class OrExpression implements BoolExpression {
    private final BoolExpression left, right;
    OrExpression(BoolExpression left, BoolExpression right) {
        this.left = left; this.right = right;
    }
    public boolean interpret() { return left.interpret() || right.interpret(); }
}
 
// Client — builds tree for "true AND false"
BoolExpression expression = new AndExpression(
    new Literal(true),
    new Literal(false)
);
System.out.println(expression.interpret()); // false

PatternRelationship
Composite-PatternNonTerminalExpression IS a Composite — Interpreter trees are Composite trees with interpret() as the operation
Visitor-PatternVisitor can traverse and operate on Interpreter expression trees without modifying the expression classes
Iterator-PatternIterator can traverse the expression tree nodes sequentially

Sources

  • Gamma, Helm, Johnson, Vlissides — Design Patterns: Elements of Reusable Object-Oriented Software, Addison-Wesley, 1994, p.243