Card 03/ 08
Interpreter Template in Python, Java and TypeScript
Numbers, a variable, addition and multiplication, and the tree for 1 + 2 * x built by hand.
The Interpreter template is the pattern in its pure form, with the participants from the class diagram. Two terminal expressions, a number and a variable, and two non-terminal ones, addition and multiplication, evaluate the tree for 1 + 2 * x, built by hand, against two contexts. The steps build the Python version, and the tabs give the complete program in three languages, each with its output.
Building the template one part at a time
Abstract Expression
The one method every rule implements.
class Expression(ABC):
@abstractmethod
def interpret(self, context: Context) -> int: ...Terminal Expression
A leaf, evaluated by reading the context.
def interpret(self, context: Context) -> int:
return context.variables[self.name]Non-terminal Expression
Interprets its parts, then combines them.
def interpret(self, context: Context) -> int:
return self.left.interpret(context) + self.right.interpret(context)Client
Builds the tree and evaluates it against a context.
sentence = Add(Number(1), Multiply(Number(2), Variable("x")))
sentence.interpret(Context({"x": 3}))The complete template
from abc import ABC, abstractmethod
class Context:
def __init__(self, variables: dict[str, int]) -> None:
self.variables = variables
class Expression(ABC):
@abstractmethod
def interpret(self, context: Context) -> int: ...
class Number(Expression):
def __init__(self, value: int) -> None:
self.value = value
def interpret(self, context: Context) -> int:
return self.value
class Variable(Expression):
def __init__(self, name: str) -> None:
self.name = name
def interpret(self, context: Context) -> int:
return context.variables[self.name]
class Add(Expression):
def __init__(self, left: Expression, right: Expression) -> None:
self.left = left
self.right = right
def interpret(self, context: Context) -> int:
return self.left.interpret(context) + self.right.interpret(context)
class Multiply(Expression):
def __init__(self, left: Expression, right: Expression) -> None:
self.left = left
self.right = right
def interpret(self, context: Context) -> int:
return self.left.interpret(context) * self.right.interpret(context)
sentence = Add(Number(1), Multiply(Number(2), Variable("x")))
print("1 + 2 * x with x = 3:", sentence.interpret(Context({"x": 3})))
print("1 + 2 * x with x = 10:", sentence.interpret(Context({"x": 10})))1 + 2 * x with x = 3: 7
1 + 2 * x with x = 10: 21import java.util.Map;
public class InTemplate {
public static void main(String[] args) {
Expression sentence = new Add(new Number(1), new Multiply(new Number(2), new Variable("x")));
System.out.println("1 + 2 * x with x = 3: " + sentence.interpret(new Context(Map.of("x", 3))));
System.out.println("1 + 2 * x with x = 10: " + sentence.interpret(new Context(Map.of("x", 10))));
}
}
record Context(Map<String, Integer> variables) {}
interface Expression {
int interpret(Context context);
}
record Number(int value) implements Expression {
public int interpret(Context context) {
return value;
}
}
record Variable(String name) implements Expression {
public int interpret(Context context) {
return context.variables().get(name);
}
}
record Add(Expression left, Expression right) implements Expression {
public int interpret(Context context) {
return left.interpret(context) + right.interpret(context);
}
}
record Multiply(Expression left, Expression right) implements Expression {
public int interpret(Context context) {
return left.interpret(context) * right.interpret(context);
}
}1 + 2 * x with x = 3: 7
1 + 2 * x with x = 10: 21class Context {
constructor(readonly variables: ReadonlyMap<string, number>) {}
}
interface Expression {
interpret(context: Context): number;
}
class NumberLiteral implements Expression {
constructor(readonly value: number) {}
interpret(): number {
return this.value;
}
}
class Variable implements Expression {
constructor(readonly name: string) {}
interpret(context: Context): number {
const value = context.variables.get(this.name);
if (value === undefined) throw new Error(`unknown variable ${this.name}`);
return value;
}
}
class Add implements Expression {
constructor(readonly left: Expression, readonly right: Expression) {}
interpret(context: Context): number {
return this.left.interpret(context) + this.right.interpret(context);
}
}
class Multiply implements Expression {
constructor(readonly left: Expression, readonly right: Expression) {}
interpret(context: Context): number {
return this.left.interpret(context) * this.right.interpret(context);
}
}
const sentence = new Add(new NumberLiteral(1), new Multiply(new NumberLiteral(2), new Variable("x")));
console.log("1 + 2 * x with x = 3:", sentence.interpret(new Context(new Map([["x", 3]]))));
console.log("1 + 2 * x with x = 10:", sentence.interpret(new Context(new Map([["x", 10]]))));1 + 2 * x with x = 3: 7
1 + 2 * x with x = 10: 21All three print 7 for x set to 3, and 21 for x set to 10. The tree is built once and evaluated against each context; the sentence and the values it is evaluated with are kept apart, which is why the context is a separate participant.
A new rule, such as subtraction, is one new class with its own interpret, and the existing classes do not change. A new operation over every rule, such as printing the sentence back as text, is the opposite: a method on every class. A grammar that gains many operations is where Interpreter and Visitor are combined, with the tree from Interpreter and each operation as a visitor.
← → move through sections and cards