Design Patterns: Interpreter
8 min
Interpreter defines a grammar for a language and maps its sentences to an object tree — each node knows how to interpret itself.
Intent
When a simple language needs to be evaluated or executed, you can represent its grammar as a hierarchy of objects (an AST), where each node type knows how to interpret itself.
Interpreter is one of the less commonly used GoF patterns. It's only worth reaching for when you're defining a small, controlled language: a config DSL, a simple rule engine, or a command set.
Structure
- AbstractExpression: the
interpret(context)interface - TerminalExpression: leaf symbols with no sub-expressions (numbers, variables)
- NonTerminalExpression: composite expressions containing other expressions (add, multiply)
- Context: external state available during interpretation
Practical Example: Arithmetic Expression Evaluator
TypeScript
type Context = Map;
interface Expression {
interpret(context: Context): number;
}
// TerminalExpression: number literal
class NumberExpression implements Expression {
constructor(private value: number) {}
interpret(_context: Context): number {
return this.value;
}
}
// TerminalExpression: variable
class VariableExpression implements Expression {
constructor(private name: string) {}
interpret(context: Context): number {
const value = context.get(this.name);
if (value === undefined) throw new Error(`Undefined variable: ${this.name}`);
return value;
}
}
// NonTerminalExpression: addition
class AddExpression implements Expression {
constructor(
private left: Expression,
private right: Expression,
) {}
interpret(context: Context): number {
return this.left.interpret(context) + this.right.interpret(context);
}
}
// NonTerminalExpression: multiplication
class MultiplyExpression implements Expression {
constructor(
private left: Expression,
private right: Expression,
) {}
interpret(context: Context): number {
return this.left.interpret(context) * this.right.interpret(context);
}
}
// NonTerminalExpression: negation
class NegateExpression implements Expression {
constructor(private expression: Expression) {}
interpret(context: Context): number {
return -this.expression.interpret(context);
}
}
// Build AST for: (x + 5) * y
const context: Context = new Map([
['x', 3],
['y', 4],
]);
const expression = new MultiplyExpression(
new AddExpression(
new VariableExpression('x'),
new NumberExpression(5),
),
new VariableExpression('y'),
);
console.log(expression.interpret(context)); // (3 + 5) * 4 = 32
// Update context without touching the expression tree
context.set('x', 10);
console.log(expression.interpret(context)); // (10 + 5) * 4 = 60 When to Use
Good fits
- You need to evaluate a simple language where sentences can be represented as an abstract syntax tree
- Config DSLs, simple rule engines, small command sets
Important caveat
- As grammar complexity grows, Interpreter leads to a class explosion. For a real language, use a dedicated parser generator like ANTLR instead.
- Understanding the pattern's design intent is more valuable than using it directly in most cases.
Summary
Interpreter is the classic technique for mapping language grammar onto an object tree.
Understanding it unlocks one of the conceptual layers behind compilers and template engines — even if you never implement it from scratch.