Interpreter Design Pattern in Go
1. Definition
The Interpreter Design Pattern is a behavioral design pattern used to define a grammatical representation for a language and an interpreter to interpret the grammar. This pattern is particularly useful for designing simple languages or parsing expressions.
The Interpreter Design Pattern provides a way to evaluate language grammar or expressions. It is used to interpret expressions or sentences in a language. This pattern is commonly used in the development of SQL parsers, symbol processing engines, and other language interpreters.
Key Points:
- Grammar Representation: Defines a way to represent the grammar.
- Interpreter: Implements the interpretation of the grammar.
- Terminal and Non-Terminal Expressions: Composes the grammar rules.
2. Problem Statement
Consider needing to evaluate expressions written in a simple custom language or DSL (Domain Specific Language). Hand-coding the parsing and evaluation logic for every new expression or language construct can become tedious, error-prone, and hard to maintain.
3. Solution
Define a grammar for the language, represent each grammar rule with a class, and use a parser that composes these classes based on the input expression. The resulting object structure (often an abstract syntax tree) can then be traversed to evaluate the expression.
4. Real-World Use Cases
1. Evaluating mathematical expressions.
2. SQL parsers that translate SQL queries into executable commands.
3. Configuration or rule engines that interpret custom DSLs.
5. Implementation Steps
1. Define an abstract expression that declares an interpret() method.
2. For each grammar rule, create a concrete class that implements the interpret() method.
3. Implement the parser that constructs the abstract syntax tree of the expression using the grammar classes.
4. To evaluate an expression, traverse the tree and call the interpret() method.
6. Implementation in Go
// Abstract Expression
type Expression interface {
Interpret(context string) bool
}
// TerminalExpression
type TerminalExpression struct {
data string
}
func (t *TerminalExpression) Interpret(context string) bool {
if strings.Contains(context, t.data) {
return true
}
return false
}
// OrExpression
type OrExpression struct {
expr1 Expression
expr2 Expression
}
func (o *OrExpression) Interpret(context string) bool {
return o.expr1.Interpret(context) || o.expr2.Interpret(context)
}
// AndExpression
type AndExpression struct {
expr1 Expression
expr2 Expression
}
func (a *AndExpression) Interpret(context string) bool {
return a.expr1.Interpret(context) && a.expr2.Interpret(context)
}
// Client code
func main() {
john := &TerminalExpression{data: "John"}
married := &TerminalExpression{data: "Married"}
isMarriedMan := &AndExpression{expr1: john, expr2: married}
fmt.Println(isMarriedMan.Interpret("John is a Married man"))
}
Output:
true
Explanation:
1. The Expression interface is the abstract representation of any expression and declares the Interpret() method.
2. TerminalExpression checks if the data is contained within the context.
3. OrExpression and AndExpression are compound expressions. They evaluate based on the result of their individual expressions.
4. In the client code, an AndExpression is constructed to check if “John” and “Married” are both present in the string. The interpretation of “John is a Married man” returns true.
7. When to use?
Use the Interpreter Pattern when:
1. There’s a grammar/syntax to interpret, and this grammar is well-defined.
2. Efficiency isn’t a top concern — the pattern can be slow and complex.
3. You want to provide an easy way to extend and add new ways of interpreting the language.