Interpreter
Given a language, define a representation for its grammar along with an interpreter that uses the representation to interpret sentences in the language.
Participants
| Role | In this example | Responsibility |
|---|---|---|
| Abstract Expression | Expression, #evaluate(context) | The interface every node in the grammar shares, plus the + and * operators. |
| Terminal Expression | Number, Variable | Leaves of the tree. They interpret themselves directly. |
| Nonterminal Expression | Add, Multiply | One class per grammar rule. They interpret themselves in terms of their parts. |
| Context | Hash, e.g. { wave: 3 } | The values the variables refer to. |
Ruby implementation
# A tiny language for wave formulas, such as "5 + 2 × wave" goblins.
# Operators are shared by every expression, so formulas read like arithmetic.
module Expression
def +(other) = Add.new(left: self, right: other)
def *(other) = Multiply.new(left: self, right: other)
end
# Terminal expressions.
Number = Data.define(:value) do
include Expression
def evaluate(_context) = value
def to_s = value.to_s
end
Variable = Data.define(:name) do
include Expression
def evaluate(context) = context.fetch(name)
def to_s = name.to_s
end
# Nonterminal expressions: each one interprets itself in terms of its parts.
Add = Data.define(:left, :right) do
include Expression
def evaluate(context) = left.evaluate(context) + right.evaluate(context)
def to_s = "(#{left} + #{right})"
end
Multiply = Data.define(:left, :right) do
include Expression
def evaluate(context) = left.evaluate(context) * right.evaluate(context)
def to_s = "(#{left} * #{right})"
endIn use
wave = Variable.new(:wave)
# Ruby operators build the syntax tree for us:
goblins = Number.new(5) + Number.new(2) * wave
goblins.to_s # => "(5 + (2 * wave))"
goblins.evaluate(wave: 1) # => 7
goblins.evaluate(wave: 3) # => 11
The game’s wave sizes are formulas in code. As expressions, they become data: a level designer could write them, and the game can print, store or check them.
Notes
Ruby idioms
- Operators are methods. Defining
+and*in a shared module lets ordinary Ruby syntax build the tree. Ruby’s precedence rules give the right grouping for free. Data.definemakes each grammar rule a tiny immutable value with structural equality, so two identical sub-expressions compare equal.- A Hash is a fine context.
fetchraisesKeyErroron an unbound variable instead of quietly givingnil. - Arel, the SQL builder inside Active Record, is a large interpreter of this kind.
Reach for it when
- The language is small and stable: wave formulas, map layouts, search filters, pricing rules.
- Designers or configuration need to describe behaviour as data.
Watch for
- The pattern says nothing about parsing text into the tree. For a real grammar, use a parser such as Parslet or Racc and keep the interpreter for evaluation.
- With one class per rule, large grammars get unwieldy. Visitor can move operations out of the nodes.
Represent each grammar rule as a small object, and every sentence becomes a tree you can evaluate, print or check.