Card 03/ 08

CodeDifficulty: Intermediate4 min

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.

py
class Expression(ABC):
    @abstractmethod
    def interpret(self, context: Context) -> int: ...

Terminal Expression

A leaf, evaluated by reading the context.

py
def interpret(self, context: Context) -> int:
    return context.variables[self.name]

Non-terminal Expression

Interprets its parts, then combines them.

py
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.

py
sentence = Add(Number(1), Multiply(Number(2), Variable("x")))
sentence.interpret(Context({"x": 3}))

The complete template

py
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})))
OutputRan with python:3.12-slim (Python 3.12.13)
1 + 2 * x with x = 3: 7
1 + 2 * x with x = 10: 21
`Variable` reads the context, and `Add` and `Multiply` interpret their parts.
java
import 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);
    }
}
OutputRan with eclipse-temurin:21-jdk (Java 21.0.12)
1 + 2 * x with x = 3: 7
1 + 2 * x with x = 10: 21
Each expression is a record holding its parts, and `interpret` combines them.
ts
class 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]]))));
OutputRan with node:22-slim (Node 22.23.1), TypeScript 5.9.3
1 + 2 * x with x = 3: 7
1 + 2 * x with x = 10: 21
The number class is `NumberLiteral`, since `Number` is already the name of a JavaScript built-in.

All 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.