lesson

Introduction to the Interpreter Pattern

Introduction to the Interpreter Pattern

The Interpreter pattern is a behavioral design pattern that defines a grammatical representation for a language and provides an interpreter to process sentences in that language. It's particularly useful when you need to evaluate or parse simple domain-specific languages (DSLs) or expressions.

The Problem

Imagine you're building a logistics system that needs to handle various address formatting requirements. Different regions and use cases require addresses to be formatted differently:

  • US Standard: {street}, {city}, {state} {zip}
  • Compact: {street}, {city} {zip}
  • Full: {street}, {city}, {state} {zip}, {country}
  • Label: {name}\n{street}\n{city}, {state} {zip}

You could hard-code each format variation, but what happens when:

  • You need to add new formats frequently
  • Users want to define custom formats
  • You need to validate format strings before use
  • You need to parse and extract format components

Hard-coding becomes unmaintainable. You need a way to define a grammar for these format strings and interpret them to produce the desired output.

The Solution

The Interpreter pattern solves this by:

  1. Defining a grammar - Establishing the syntax rules for your language (in our case, format strings with placeholders)
  2. Representing grammar as classes - Each grammar rule becomes a class
  3. Building an expression tree - Parsing creates a tree of expression objects
  4. Interpreting the tree - Walking the tree evaluates the expression

Here's a simple example of how an address format expression is interpreted:

typescript
1// Format string: "{street}, {city} {zip}"
2// Becomes an expression tree:
3
4ConcatExpression([
5 VariableExpression("street"),
6 LiteralExpression(", "),
7 VariableExpression("city"),
8 LiteralExpression(" "),
9 VariableExpression("zip")
10])
11
12// Interpretation with context { street: "123 Main St", city: "Boston", zip: "02101" }:
13// → "123 Main St, Boston 02101"

Pattern Structure

The Interpreter pattern consists of four main components:

1. AbstractExpression

The interface that declares an interpret() method. All expression classes implement this interface.

typescript
1interface Expression {
2 interpret(context: Context): string;
3}

2. TerminalExpression

Implements interpret for terminal symbols in the grammar - the "leaves" of the expression tree that don't contain other expressions.

typescript
1// Terminal: a literal string
2class LiteralExpression implements Expression {
3 constructor(private value: string) {}
4
5 interpret(context: Context): string {
6 return this.value;
7 }
8}
9
10// Terminal: a variable lookup
11class VariableExpression implements Expression {
12 constructor(private name: string) {}
13
14 interpret(context: Context): string {
15 return context.get(this.name);
16 }
17}

3. NonterminalExpression

Implements interpret for nonterminal symbols - expressions that contain other expressions (branches of the tree).

typescript
1// Nonterminal: concatenate multiple expressions
2class ConcatExpression implements Expression {
3 constructor(private expressions: Expression[]) {}
4
5 interpret(context: Context): string {
6 return this.expressions
7 .map(expr => expr.interpret(context))
8 .join('');
9 }
10}

4. Context

Holds the global state needed for interpretation - typically variable values or configuration.

typescript
1class Context {
2 constructor(private variables: Map<string, string>) {}
3
4 get(name: string): string {
5 return this.variables.get(name) || '';
6 }
7}

How It Works: Address Format Example

Let's see how these components work together to parse and interpret an address format:

typescript
1// Step 1: Parse format string into expression tree
2const formatString = "{street}, {city} {zip}";
3const expression = parseFormat(formatString);
4// Creates: ConcatExpression([
5// VariableExpression("street"),
6// LiteralExpression(", "),
7// VariableExpression("city"),
8// LiteralExpression(" "),
9// VariableExpression("zip")
10// ])
11
12// Step 2: Create context with address data
13const context = new Context(new Map([
14 ['street', '123 Main St'],
15 ['city', 'Boston'],
16 ['state', 'MA'],
17 ['zip', '02101']
18]));
19
20// Step 3: Interpret the expression
21const result = expression.interpret(context);
22console.log(result); // "123 Main St, Boston 02101"

Logistics Domain Context

In logistics systems, the Interpreter pattern is valuable for:

Address Formatting

Parse and evaluate format templates for different output requirements:

typescript
1const formats = {
2 shipping: "{name}\n{street}\n{city}, {state} {zip}",
3 billing: "{street}, {city}, {state} {zip}",
4 compact: "{city}, {state}"
5};

Route Expression Language

Define simple DSLs for route specifications:

typescript
1// Route: "WAREHOUSE -> STORE(priority=high) -> CUSTOMER"
2// Interpreted to create route objects with constraints

Condition Evaluation

Parse and evaluate business rules:

typescript
1// Rule: "weight > 50 AND destination = 'international'"
2// Interpreted to filter shipments

Label Template System

Allow users to define custom shipping label layouts:

typescript
1// Template: "{tracking}\n{from.city} → {to.city}\n{service}"
2// Interpreted to generate label content

Benefits

  1. Easy to change and extend grammar - Add new expression types without modifying existing code
  2. Grammar is explicit - The class structure directly represents the grammar rules
  3. Flexible interpretation - Different contexts can produce different results from the same expression
  4. Testable - Each expression type can be tested independently

Drawbacks

  1. Complex grammars are hard to maintain - Each grammar rule requires a new class
  2. Not efficient for complex languages - Better to use parser generators (ANTLR, PEG.js)
  3. Expression tree overhead - Creating and traversing trees has performance cost
  4. Limited to simple languages - Works best for simple DSLs, not full programming languages

When to Use Interpreter Pattern

Use the Interpreter pattern when:

  • You have a simple language or DSL to interpret
  • Grammar is stable but you need to evaluate many different sentences
  • Efficiency is not critical - interpretation is slower than compiled approaches
  • You want grammar rules as classes for clarity and extensibility

Don't use it when:

  • The language is complex - use a proper parser library instead
  • Performance is critical - compiled or cached approaches are faster
  • The grammar changes frequently - class explosion becomes unmaintainable

Real-World Examples

  • Regular expressions - Each regex component is an expression
  • SQL interpreters - Simple query languages
  • Template engines - Mustache, Handlebars for simple templates
  • Mathematical expression evaluators - Calculator parsers
  • Configuration DSLs - Simple configuration languages

Summary

The Interpreter pattern provides a way to define and evaluate sentences in a simple language by:

  1. Representing grammar rules as classes (AbstractExpression)
  2. Implementing terminal expressions (literals, variables)
  3. Implementing nonterminal expressions (compositions, operations)
  4. Using context to provide interpretation state
  5. Building and traversing expression trees

In our logistics context, it's perfect for address format parsing, allowing flexible template-based formatting without hard-coding every variation.

Next, we'll implement the Interpreter pattern to build a complete address format parser.

Introduction to the Interpreter Pattern - Anko Academy