Visitor and the Double Dispatch Idea
What This Concept Is
Visitor separates an operation from the objects it operates on. You have a stable hierarchy of element classes (AST nodes, shapes, filesystem entries) and many operations to perform over them (print, type-check, optimize). Visitor puts each operation in its own class with a method per element type.
The mechanism is double dispatch. Single dispatch (virtual method call) selects behavior based on one object's type: element.accept(visitor). The accept method then calls visitor.visitConcreteElement(this), which selects again based on the visitor's type. Two dispatches -- one for each role -- give you the effect of operation(elementType, visitorType) dispatch that most OO languages do not support directly.
Roles:
- Element interface with
accept(visitor). - Concrete elements each implementing
acceptasvisitor.visitX(this). - Visitor interface with one
visitXmethod per concrete element. - Concrete visitors implementing each operation.
Why It Matters Here
Visitor is the right answer to "we have a fixed set of types and a growing set of operations". It sidesteps putting every operation on every element class. It also exposes the asymmetry that makes it expensive: adding a new element forces every visitor to change.
The double dispatch idea itself is worth knowing even when you do not adopt the full pattern -- language features like Clojure's multimethods or Rust's trait objects encode the same distinction.
Concrete Example
A tiny expression tree with two operations as visitors.
interface Expr { <R> R accept(Visitor<R> v); }
class Num implements Expr {
final double value;
Num(double v) { this.value = v; }
public <R> R accept(Visitor<R> v) { return v.visitNum(this); }
}
class Add implements Expr {
final Expr left, right;
Add(Expr l, Expr r) { this.left = l; this.right = r; }
public <R> R accept(Visitor<R> v) { return v.visitAdd(this); }
}
class Mul implements Expr {
final Expr left, right;
Mul(Expr l, Expr r) { this.left = l; this.right = r; }
public <R> R accept(Visitor<R> v) { return v.visitMul(this); }
}
interface Visitor<R> {
R visitNum(Num n);
R visitAdd(Add a);
R visitMul(Mul m);
}
class Evaluate implements Visitor<Double> {
public Double visitNum(Num n) { return n.value; }
public Double visitAdd(Add a) { return a.left.accept(this) + a.right.accept(this); }
public Double visitMul(Mul m) { return m.left.accept(this) * m.right.accept(this); }
}
class Print implements Visitor<String> {
public String visitNum(Num n) { return Double.toString(n.value); }
public String visitAdd(Add a) { return "(" + a.left.accept(this) + " + " + a.right.accept(this) + ")"; }
public String visitMul(Mul m) { return "(" + m.left.accept(this) + " * " + m.right.accept(this) + ")"; }
}
Expr e = new Add(new Num(1), new Mul(new Num(2), new Num(3)));
System.out.println(e.accept(new Evaluate())); // 7.0
System.out.println(e.accept(new Print())); // (1.0 + (2.0 * 3.0))
Expr has no evaluate or print method. Each operation is one class. Adding "Differentiate" tomorrow is one more class; Expr stays untouched.
Common Confusion / Misconception
- "Why not just put
evaluateon each node?" You can. Do that when the number of operations is tiny and stable, and the node set grows often. Visitor is for the opposite case: operations grow, nodes do not. - "Pattern matching solves this better." In languages with pattern matching (Scala, Kotlin
when, Pythonmatch, Rust), visitor-style code often reduces to amatch. The idea survives; the ceremony does not. - Visitor + mutable element state. A visitor that writes into elements during traversal is easy to get wrong. Prefer returning a value (
Visitor<R>) over mutating. - Adding a new element type. Every visitor must grow a new method. That is the trade. If element types grow faster than operations, Visitor is the wrong pattern.
- Confusing Visitor with Iterator. Iterator walks the structure and hands you elements; Visitor applies an operation uniformly, often in concert with an iterator for traversal.
How To Use It
- Confirm the trade: element set mostly fixed; operations likely to grow.
- Declare an element interface with
accept(visitor). - Each concrete element calls the visitor's type-specific method and passes itself.
- Declare the visitor interface with one
visitmethod per concrete element. - Implement the first operation as one visitor. Subsequent ones are additive.
- Consider generic return types so the same visitor shape serves evaluation and printing.
- If the hierarchy grows, update every visitor deliberately; do not silently add a default.
Check Yourself
- Why does
acceptneed to dispatch to a specificvisitXrather than takingElementas a parameter? - What trade-off does Visitor make -- what grows cheaply, what grows expensively?
- When is pattern matching a better tool than Visitor?
- What goes wrong if a visitor mutates elements during traversal?
Mini Drill or Application
Given a small AST for arithmetic (Num, Var, Add, Mul):
- Implement
Evaluate(env)andPrettyvisitors. - Add a
FreeVarsvisitor that collects the set of variable names. - Add a new node
Neg. Update all three visitors. - Write a one-paragraph note: why Visitor was worth the ceremony -- or why a
matchwould have been cleaner in your language of choice.