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
typescript1// Good: Simple template language2"{street}, {city} {zip}"34// Good: Simple filter expressions5"priority = high AND weight > 50"67// Good: Simple math expressions8"(price * quantity) - discount"
2. You need full control over interpretation
typescript1// Custom behavior during interpretation2class LoggingVariableExpression implements Expression {3 constructor(private name: string, private logger: Logger) {}45 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
typescript1// Too complex for Interpreter pattern2// Use ANTLR, PEG.js, or similar3const sqlQuery = `4 SELECT orders.id, customers.name, SUM(items.price) as total5 FROM orders6 JOIN customers ON orders.customer_id = customers.id7 JOIN order_items items ON items.order_id = orders.id8 WHERE orders.status = 'shipped'9 GROUP BY orders.id, customers.name10 HAVING total > 100011 ORDER BY total DESC12`;
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
typescript1// Use existing parsers for standard languages2import { parse } from 'json5'; // JSON with comments3import { parse as parseYaml } from 'yaml'; // YAML4import * 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
| Feature | Interpreter Pattern | Parser Library |
|---|---|---|
| Complexity | Simple DSLs | Any complexity |
| Performance | Slower (tree traversal) | Faster (optimized) |
| Setup | Minimal | Library dependency |
| Control | Full control | Library limitations |
| Maintainability | Class per rule | Grammar file |
| Error Messages | Manual | Often automatic |
| Learning Curve | Low | Medium to High |
Simple DSLs Perfect for Interpreter
1. Template Languages
typescript1// Format strings with simple variable substitution2const 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
typescript1// Simple boolean logic for filtering2class FilterInterpreter {3 parse(expression: string): Expression {4 // "status = 'active' AND priority = 'high'"5 // Becomes: AndExpression(EqualsExpression(...), EqualsExpression(...))6 }7}89// Usage in logistics10const activeHighPriority = filter.parse("status = active AND priority = high");11const shipments = allShipments.filter(s =>12 activeHighPriority.interpret(new ShipmentContext(s)) === true13);
3. Validation Rules
typescript1// Simple validation DSL2const 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
typescript1// Simple config expressions2const 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.
typescript1// Composite structure2interface Expression {3 interpret(context: Context): string;4}56// Leaf (Terminal)7class LiteralExpression implements Expression {8 // No children9}1011// Composite (Nonterminal)12class ConcatExpression implements Expression {13 constructor(private children: Expression[]) {}14 // Contains children, delegates to them15}
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:
typescript1class ExpressionFactory {2 private cache = new Map<string, Expression>();34 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 }1011 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}1920// Reuse common literals21const factory = new ExpressionFactory();22const comma = factory.getLiteral(', '); // Cached23const space = factory.getLiteral(' '); // Cached
3. Visitor Pattern
Use Visitor to add new operations without modifying expression classes:
typescript1interface ExpressionVisitor {2 visitLiteral(expr: LiteralExpression): void;3 visitVariable(expr: VariableExpression): void;4 visitConcat(expr: ConcatExpression): void;5}67// Expression classes accept visitors8interface Expression {9 interpret(context: Context): string;10 accept(visitor: ExpressionVisitor): void;11}1213// Example: Pretty print expression tree14class PrettyPrintVisitor implements ExpressionVisitor {15 private indent = 0;1617 visitLiteral(expr: LiteralExpression): void {18 console.log(' '.repeat(this.indent) + `Literal: "${expr.getValue()}"`);19 }2021 visitVariable(expr: VariableExpression): void {22 console.log(' '.repeat(this.indent) + `Variable: {${expr.getName()}}`);23 }2425 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}3233// Usage34const visitor = new PrettyPrintVisitor();35expression.accept(visitor);
4. Builder Pattern
Use Builder to construct complex expressions fluently:
typescript1class ExpressionBuilder {2 private expressions: Expression[] = [];34 literal(value: string): this {5 this.expressions.push(new LiteralExpression(value));6 return this;7 }89 variable(name: string): this {10 this.expressions.push(new VariableExpression(name));11 return this;12 }1314 build(): Expression {15 return this.expressions.length === 116 ? this.expressions[0]17 : new ConcatExpression(this.expressions);18 }19}2021// Usage22const 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:
typescript1// Inefficient: Parse on every format call2class 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}89// Better: Cache parsed expressions10class FastFormatter {11 private cache = new Map<string, Expression>();1213 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:
typescript1// Inefficient: Create new context for each field2addresses.map(addr => {3 return formatter.format(template, addr); // New context each time4});56// Better: Batch process with context pooling7class ContextPool {8 private pool: Context[] = [];910 acquire(data: Record<string, string>): Context {11 const context = this.pool.pop() || new Context({});12 context.reset(data);13 return context;14 }1516 release(context: Context): void {17 this.pool.push(context);18 }19}
3. Deep Expression Trees
Limit nesting to avoid stack overflow:
typescript1class SafeConcatExpression implements Expression {2 constructor(3 private expressions: Expression[],4 private maxDepth: number = 1005 ) {}67 interpret(context: Context, depth: number = 0): string {8 if (depth > this.maxDepth) {9 throw new Error('Maximum expression depth exceeded');10 }11 return this.expressions12 .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
typescript1describe('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 });78 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
typescript1describe('FormatParser', () => {2 const parser = new FormatParser();34 it('should parse literals', () => {5 const expr = parser.parse('Hello World');6 expect(expr).toBeInstanceOf(LiteralExpression);7 });89 it('should parse variables', () => {10 const expr = parser.parse('{name}');11 expect(expr).toBeInstanceOf(VariableExpression);12 });1314 it('should parse mixed content', () => {15 const expr = parser.parse('Hello {name}!');16 expect(expr).toBeInstanceOf(ConcatExpression);17 });1819 it('should handle malformed input', () => {20 expect(() => parser.parse('{unclosed')).toThrow();21 });22});
3. Test End-to-End Formatting
typescript1describe('AddressFormatter', () => {2 const formatter = new AddressFormatter();3 const address = {4 street: '123 Main St',5 city: 'Boston',6 state: 'MA',7 zip: '02101'8 };910 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 });1415 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:
typescript1class ParseError extends Error {2 constructor(3 message: string,4 public position: number,5 public input: string6 ) {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:
typescript1class Validator {2 validateExpression(expr: Expression, context: Context): string[] {3 const errors: string[] = [];4 // Collect all required variables5 const required = this.getRequiredVariables(expr);6 // Check context has them all7 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.