LESSON 7.7
Interpreter
Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language.
15 min read
Real-world analogy
Sheet music notation is an everyday interpreter. A composer writes a symphony using a standardized musical grammar: quarter notes, half rests, treble clefs, and key signatures. When a pianist sits down with the score, they interpret each symbol in the context of the instrument's keys, tempo, and dynamics, translating paper symbols into acoustic sound waves.
The problem
You are building a dynamic feature-flagging and authorization engine. Product managers and security compliance officers need to write custom access rules for new features without deploying new backend code:
- Rule A:
user.age >= 18 AND user.country == "US" - Rule B:
user.isBetaTester == true OR (user.department == "Engineering" AND user.level >= 4)
If you attempt to evaluate these rules using ad-hoc string splitting or regex matching directly inside your business logic, the code becomes fragile, insecure, and vulnerable to syntax errors:
// Naive approach: Brittle string manipulation and unsafe eval()
class RuleEvaluator {
evaluate(ruleString, context) {
// Unsafe: eval() exposes application to remote code execution (RCE) vulnerabilities!
// String parsing without a formal grammar fails on nested parentheses and precedence.
try {
const fn = new Function("user", `return ${ruleString};`);
return Boolean(fn(context));
} catch (err) {
console.error("Syntax error in rule:", err);
return false;
}
}
}public class Example {
static class RuleEvaluator {
public boolean evaluate(String rule, java.util.Map<String, Object> ctx) {
// Brittle: Ad-hoc substring and regex checks fail on parentheses,
// operator precedence, and type casting.
if (rule.contains("AND")) {
String[] parts = rule.split("AND"
Using eval() introduces critical security vulnerabilities, while string splitting fails to handle operator precedence, nested parentheses, and type conversions.
The solution
The Interpreter pattern defines a grammatical representation for a domain-specific language (DSL) along with an interpreter that evaluates sentences in that language. Each rule in the grammar is mapped to a class, forming an Abstract Syntax Tree (AST):
-
Terminal Expressions: Leaves that return values directly (e.g. constant numbers, string literals, context variable lookups).
-
Nonterminal Expressions: Nodes that combine child expressions according to grammar operations (e.g.
AndExpression,OrExpression,GreaterThanExpression). -
Abstract Expression (
Expression): Interface declaring theinterpret(Context c)method. -
Terminal Expression (
VariableExpression,ConstantExpression): Evaluates leaf values against the context. -
Nonterminal Expression (
AndExpression,OrExpression): Recursively interprets child expressions. -
Context (
EvaluationContext): Holds global data, variable values, and environment state needed during evaluation.
Step-by-step implementation
Step 1: Define the abstract expression interface
Declare the interpret contract that accepts an evaluation context.
class Expression {
interpret(context) {
throw new Error("interpret() must be implemented");
}
}public class Example {
interface Expression {
boolean interpret(java.util.Map<String, Object> context);
Step 2: Implement terminal expressions (Leaves)
Terminal expressions resolve variable values from the context or return constants.
Step 3: Implement nonterminal expressions (Branches)
Nonterminal expressions combine child expressions using boolean operators.
The complete example
Here is the complete, runnable implementation demonstrating a boolean rule expression tree evaluated against user profile contexts.
Alice (US, age 25, USER): Qualified = true
Bob (CA, age 30, USER): Qualified = false
Charlie (FR, age 19, ADMIN): Qualified = trueVariations
When to use a parser generator instead
The Interpreter pattern maps one class per grammar production rule. This works wonderfully for simple grammars with 5 to 10 rules (such as arithmetic expressions, filter conditions, or cron schedules).
However, for complex languages (such as full SQL, ECMAScript, or Markdown) containing 50+ grammar rules:
- Creating a separate class for every grammar rule causes severe class explosion.
- Evaluating deep trees recursively via method dispatch is significantly slower than compiled byte-code or iterative table-driven parsing.
- Solution: For complex grammars, use dedicated Parser Generators (like ANTLR, Tree-sitter, or Lex/Yacc) to generate optimized parsing tables and AST walkers.
Where you've already used it
java.util.regex.Pattern: Parses regex strings (e.g.[a-z]+@[a-z]+\.[a-z]{2,}) into a hierarchy of regex matcher nodes (Start,Slice,Branch,CClass).- Spring Expression Language (SpEL): Evaluates dynamic expressions in annotations (
@Value("#{systemProperties['user.home']}")). - Cron Expression Evaluators: Evaluates 5-field cron strings (
*/5 * * * *) against a current timestamp context to schedule tasks. - SQL Query Optimization ASTs: Database engines parse SQL strings into ASTs of relational algebra operators (Scan, Filter, Project, Join) before execution.
Pros and cons
Advantages
- Easy to Change Grammar: Changing or extending grammar rules requires only adding or modifying an expression class.
- Open/Closed Principle: Add new operators without modifying existing expressions.
- Clear Separation: Decouples grammar evaluation logic from application business controllers.
Disadvantages
- Class Explosion: Complex grammars require hundreds of distinct classes, becoming difficult to maintain.
- Performance Overhead: Recursive evaluation of deep AST trees is slower than state-machine table parsers.
When not to use it
Do not use Interpreter when the grammar is complex or performance-critical. If you are implementing a programming language or full SQL parser, use ANTLR or a Pratt parser instead of manually writing GoF Interpreter classes.
Related patterns
| Pattern | Relationship | Primary Difference |
|---|---|---|
| Interpreter | Represents grammar AST | Evaluates domain language sentences |
| Composite | Structural foundation of AST | Composite manages tree hierarchy; Interpreter adds evaluation semantics |
| Visitor | Traverses AST nodes | Can be used to evaluate or type-check the Interpreter AST without mutating node classes |
| Flyweight | Shares terminal symbols | Can share leaf nodes (e.g. numbers, variables) across large AST trees |
Interpreter: boolean rule evaluator
Build a domain-specific rule evaluator using the Interpreter pattern with terminal variables and non-terminal AND/OR/NOT expressions.
Click Run tests to evaluate your solution against the behavior and structural test suite.
Interview questions
How is the Interpreter pattern structurally related to the Composite pattern? The Abstract Syntax Tree in an Interpreter is a Composite tree. Terminal expressions are Leaves (holding constants or variables), and Nonterminal expressions are Composites (holding child expressions and combining their results).
Why is the Interpreter pattern rarely used for full programming languages? Because a complete programming language has hundreds of grammar rules. Creating a dedicated class for every rule causes massive class proliferation, and recursively evaluating deep trees via virtual method calls is too slow compared to bytecode VMs or table-driven parsers.
How does the Interpreter pattern evaluate expressions with variables?
The client passes a Context object (often a Map<String, Object>) into interpret(context). Terminal expressions look up their variable names in the context map to retrieve current runtime values.
How can the Visitor pattern be combined with the Interpreter pattern?
Instead of having an interpret() method inside each expression class (which tangles evaluation code with AST structure), an external EvaluationVisitor or TypeCheckVisitor can traverse the AST nodes using Double Dispatch, keeping the AST classes clean.
Try it
Arithmetic Expression Evaluator LLD
Tokenize and parse arithmetic with precedence, unary operators and bounded evaluation.