lesson

Interpreter Pattern Deep Dive

Interpreter Pattern Deep Dive

Let's explore when to use the Interpreter pattern, its relationship with other patterns, and important considerations for production use.

When to Use Interpreter vs Parser Libraries

Use Interpreter Pattern When:

1. The language is simple and domain-specific

typescript
1// Good: Simple template language
2"{street}, {city} {zip}"
3
4// Good: Simple filter expressions
5"priority = high AND weight > 50"
6
7// Good: Simple math expressions
8"(price * quantity) - discount"

2. You need full control over interpretation

typescript
1// Custom behavior during interpretation
2class LoggingVariableExpression implements Expression {
3 constructor(private name: string, private logger: Logger) {}
4
5 interpret(context: Context): string {
6 const value = context.get(this.name);
7 this.logger.log(`Accessed variable: ${this.name} = ${value}`);
8 return value;
9 }
10}

3. Grammar changes infrequently

  • Adding new expression types requires new classes
  • Stable grammar keeps class count manageable
  • Extension via new expression classes is straightforward

4. You want to learn or teach language processing concepts

  • Great educational tool for understanding parsers
  • Clear demonstration of recursive descent parsing
  • Shows abstract syntax tree (AST) construction

Use Parser Libraries When:

1. The language is complex

typescript
1// Too complex for Interpreter pattern
2// Use ANTLR, PEG.js, or similar
3const sqlQuery = `
4 SELECT orders.id, customers.name, SUM(items.price) as total
5 FROM orders
6 JOIN customers ON orders.customer_id = customers.id
7 JOIN order_items items ON items.order_id = orders.id
8 WHERE orders.status = 'shipped'
9 GROUP BY orders.id, customers.name
10 HAVING total > 1000
11 ORDER BY total DESC
12`;

2. You need high performance

  • Parser libraries use optimized parsing algorithms (LALR, LL(k))
  • Compiled parsers are much faster than interpreted expression trees
  • Better for languages processed frequently

3. Grammar is well-established

typescript
1// Use existing parsers for standard languages
2import { parse } from 'json5'; // JSON with comments
3import { parse as parseYaml } from 'yaml'; // YAML
4import * as math from 'mathjs'; // Math expressions

4. You need advanced features

  • Error recovery and helpful error messages
  • Left recursion handling
  • Operator precedence parsing
  • Lexer/parser separation

Comparison Table

FeatureInterpreter PatternParser Library
ComplexitySimple DSLsAny complexity
PerformanceSlower (tree traversal)Faster (optimized)
SetupMinimalLibrary dependency
ControlFull controlLibrary limitations
MaintainabilityClass per ruleGrammar file
Error MessagesManualOften automatic
Learning CurveLowMedium to High

Simple DSLs Perfect for Interpreter

1. Template Languages

typescript
1// Format strings with simple variable substitution
2const templates = {
3 email: "Dear {name}, your order #{orderId} has shipped.",
4 sms: "Order #{orderId} shipped to {city}, {state}",
5 label: "{name}\n{street}\n{city}, {state} {zip}"
6};

2. Filter Expressions

typescript
1// Simple boolean logic for filtering
2class FilterInterpreter {
3 parse(expression: string): Expression {
4 // "status = 'active' AND priority = 'high'"
5 // Becomes: AndExpression(EqualsExpression(...), EqualsExpression(...))
6 }
7}
8
9// Usage in logistics
10const activeHighPriority = filter.parse("status = active AND priority = high");
11const shipments = allShipments.filter(s =>
12 activeHighPriority.interpret(new ShipmentContext(s)) === true
13);

3. Validation Rules

typescript
1// Simple validation DSL
2const rules = {
3 zip: "length = 5 OR length = 9",
4 weight: "value > 0 AND value < 1000",
5 priority: "value IN ['low', 'medium', 'high']"
6};

4. Configuration Languages

typescript
1// Simple config expressions
2const route = "WAREHOUSE -> HUB(region=northeast) -> STORE";
3// Interprets to route configuration object

Relationship with Other Patterns

1. Composite Pattern

The Interpreter pattern is essentially an application of the Composite pattern.

typescript
1// Composite structure
2interface Expression {
3 interpret(context: Context): string;
4}
5
6// Leaf (Terminal)
7class LiteralExpression implements Expression {
8 // No children
9}
10
11// Composite (Nonterminal)
12class ConcatExpression implements Expression {
13 constructor(private children: Expression[]) {}
14 // Contains children, delegates to them
15}

Key similarity: Both define tree structures where leaf and composite nodes share a common interface.

Key difference: Interpreter adds the interpret() operation that processes the tree to produce a result.

2. Flyweight Pattern

Use Flyweight to share common expressions and reduce memory:

typescript
1class ExpressionFactory {
2 private cache = new Map<string, Expression>();
3
4 getLiteral(value: string): Expression {
5 if (!this.cache.has(value)) {
6 this.cache.set(value, new LiteralExpression(value));
7 }
8 return this.cache.get(value)!;
9 }
10
11 getVariable(name: string): Expression {
12 const key = `var:${name}`;
13 if (!this.cache.has(key)) {
14 this.cache.set(key, new VariableExpression(name));
15 }
16 return this.cache.get(key)!;
17 }
18}
19
20// Reuse common literals
21const factory = new ExpressionFactory();
22const comma = factory.getLiteral(', '); // Cached
23const space = factory.getLiteral(' '); // Cached

3. Visitor Pattern

Use Visitor to add new operations without modifying expression classes:

typescript
1interface ExpressionVisitor {
2 visitLiteral(expr: LiteralExpression): void;
3 visitVariable(expr: VariableExpression): void;
4 visitConcat(expr: ConcatExpression): void;
5}
6
7// Expression classes accept visitors
8interface Expression {
9 interpret(context: Context): string;
10 accept(visitor: ExpressionVisitor): void;
11}
12
13// Example: Pretty print expression tree
14class PrettyPrintVisitor implements ExpressionVisitor {
15 private indent = 0;
16
17 visitLiteral(expr: LiteralExpression): void {
18 console.log(' '.repeat(this.indent) + `Literal: "${expr.getValue()}"`);
19 }
20
21 visitVariable(expr: VariableExpression): void {
22 console.log(' '.repeat(this.indent) + `Variable: {${expr.getName()}}`);
23 }
24
25 visitConcat(expr: ConcatExpression): void {
26 console.log(' '.repeat(this.indent) + 'Concat:');
27 this.indent += 2;
28 expr.getExpressions().forEach(e => e.accept(this));
29 this.indent -= 2;
30 }
31}
32
33// Usage
34const visitor = new PrettyPrintVisitor();
35expression.accept(visitor);

4. Builder Pattern

Use Builder to construct complex expressions fluently:

typescript
1class ExpressionBuilder {
2 private expressions: Expression[] = [];
3
4 literal(value: string): this {
5 this.expressions.push(new LiteralExpression(value));
6 return this;
7 }
8
9 variable(name: string): this {
10 this.expressions.push(new VariableExpression(name));
11 return this;
12 }
13
14 build(): Expression {
15 return this.expressions.length === 1
16 ? this.expressions[0]
17 : new ConcatExpression(this.expressions);
18 }
19}
20
21// Usage
22const expr = new ExpressionBuilder()
23 .variable('street')
24 .literal(', ')
25 .variable('city')
26 .literal(' ')
27 .variable('zip')
28 .build();

Performance Considerations

1. Expression Tree Overhead

Each interpretation creates and traverses a tree:

typescript
1// Inefficient: Parse on every format call
2class SlowFormatter {
3 format(template: string, data: Record<string, string>): string {
4 const expr = this.parser.parse(template); // Parse every time!
5 return expr.interpret(new Context(data));
6 }
7}
8
9// Better: Cache parsed expressions
10class FastFormatter {
11 private cache = new Map<string, Expression>();
12
13 format(template: string, data: Record<string, string>): string {
14 if (!this.cache.has(template)) {
15 this.cache.set(template, this.parser.parse(template));
16 }
17 return this.cache.get(template)!.interpret(new Context(data));
18 }
19}

2. Context Creation Cost

Reuse contexts when possible:

typescript
1// Inefficient: Create new context for each field
2addresses.map(addr => {
3 return formatter.format(template, addr); // New context each time
4});
5
6// Better: Batch process with context pooling
7class ContextPool {
8 private pool: Context[] = [];
9
10 acquire(data: Record<string, string>): Context {
11 const context = this.pool.pop() || new Context({});
12 context.reset(data);
13 return context;
14 }
15
16 release(context: Context): void {
17 this.pool.push(context);
18 }
19}

3. Deep Expression Trees

Limit nesting to avoid stack overflow:

typescript
1class SafeConcatExpression implements Expression {
2 constructor(
3 private expressions: Expression[],
4 private maxDepth: number = 100
5 ) {}
6
7 interpret(context: Context, depth: number = 0): string {
8 if (depth > this.maxDepth) {
9 throw new Error('Maximum expression depth exceeded');
10 }
11 return this.expressions
12 .map(expr => {
13 if (expr instanceof SafeConcatExpression) {
14 return expr.interpret(context, depth + 1);
15 }
16 return expr.interpret(context);
17 })
18 .join('');
19 }
20}

Testing Strategies

1. Test Expression Classes Independently

typescript
1describe('VariableExpression', () => {
2 it('should retrieve value from context', () => {
3 const expr = new VariableExpression('city');
4 const context = new Context({ city: 'Boston' });
5 expect(expr.interpret(context)).toBe('Boston');
6 });
7
8 it('should throw on missing variable', () => {
9 const expr = new VariableExpression('missing');
10 const context = new Context({});
11 expect(() => expr.interpret(context)).toThrow();
12 });
13});

2. Test Parser with Various Inputs

typescript
1describe('FormatParser', () => {
2 const parser = new FormatParser();
3
4 it('should parse literals', () => {
5 const expr = parser.parse('Hello World');
6 expect(expr).toBeInstanceOf(LiteralExpression);
7 });
8
9 it('should parse variables', () => {
10 const expr = parser.parse('{name}');
11 expect(expr).toBeInstanceOf(VariableExpression);
12 });
13
14 it('should parse mixed content', () => {
15 const expr = parser.parse('Hello {name}!');
16 expect(expr).toBeInstanceOf(ConcatExpression);
17 });
18
19 it('should handle malformed input', () => {
20 expect(() => parser.parse('{unclosed')).toThrow();
21 });
22});

3. Test End-to-End Formatting

typescript
1describe('AddressFormatter', () => {
2 const formatter = new AddressFormatter();
3 const address = {
4 street: '123 Main St',
5 city: 'Boston',
6 state: 'MA',
7 zip: '02101'
8 };
9
10 it('should format US address', () => {
11 const result = formatter.format('{street}, {city}, {state} {zip}', address);
12 expect(result).toBe('123 Main St, Boston, MA 02101');
13 });
14
15 it('should handle missing optional fields', () => {
16 const result = formatter.format('{street}, {city}', address);
17 expect(result).toBe('123 Main St, Boston');
18 });
19});

Common Pitfalls and Solutions

Pitfall 1: Grammar Explosion

Problem: Too many expression classes for complex grammars.

Solution: Keep grammar simple or use parser library for complex cases.

Pitfall 2: Poor Error Messages

Problem: Generic errors don't help users fix format strings.

Solution: Add position tracking and helpful messages:

typescript
1class ParseError extends Error {
2 constructor(
3 message: string,
4 public position: number,
5 public input: string
6 ) {
7 super(`${message} at position ${position}\n${input}\n${' '.repeat(position)}^`);
8 }
9}

Pitfall 3: No Validation

Problem: Runtime errors when required variables are missing.

Solution: Validate before interpretation:

typescript
1class Validator {
2 validateExpression(expr: Expression, context: Context): string[] {
3 const errors: string[] = [];
4 // Collect all required variables
5 const required = this.getRequiredVariables(expr);
6 // Check context has them all
7 for (const varName of required) {
8 if (!context.has(varName)) {
9 errors.push(`Missing required variable: ${varName}`);
10 }
11 }
12 return errors;
13 }
14}

Summary

The Interpreter pattern works best for:

  • Simple DSLs where grammar is straightforward
  • Stable grammars that don't change frequently
  • Custom interpretation needs beyond simple parsing
  • Educational purposes for learning language processing

Use parser libraries instead when dealing with complex languages, needing high performance, or working with established grammars.

The pattern combines well with:

  • Composite for tree structure
  • Flyweight for expression reuse
  • Visitor for new operations
  • Builder for fluent construction

Key to success: cache parsed expressions, handle errors gracefully, and keep grammars simple.

Next, you'll implement a complete address format parser using the Interpreter pattern in the workshop.

Interpreter Pattern Deep Dive - Anko Academy