Explore Visitor in Scala, when pattern matching replaces it, and when visitor-style separation still helps for open hierarchies, Java interop, or operation-heavy structures.
Visitor pattern: A behavioral pattern that separates operations from the object structure they run over, so new operations can be added without changing the element types.
Visitor matters less in Scala than in classic object-oriented languages because sealed traits and pattern matching already give you a very direct way to define operations over a fixed hierarchy. The real design question is whether the element hierarchy or the set of operations is the more stable axis.
If the hierarchy is closed and stable, pattern matching is usually the clearest answer:
1sealed trait Expr
2final case class Number(value: Int) extends Expr
3final case class Add(left: Expr, right: Expr) extends Expr
4
5def eval(expr: Expr): Int =
6 expr match
7 case Number(value) => value
8 case Add(left, right) => eval(left) + eval(right)
This is often better than simulating the classic visitor ceremony because the operation is explicit and the set of element types is right in front of you.
Visitor earns its place when:
Java interop is the most common reason. If the hierarchy is already visitor-shaped, fighting it with Scala wrappers can be more awkward than embracing the existing boundary.
A Scala-friendly visitor often looks closer to a fold than to textbook accept/visit boilerplate:
1trait ExprVisitor[A]:
2 def number(value: Int): A
3 def add(left: A, right: A): A
4
5sealed trait Expr:
6 def visit[A](visitor: ExprVisitor[A]): A
7
8final case class Number(value: Int) extends Expr:
9 def visit[A](visitor: ExprVisitor[A]): A =
10 visitor.number(value)
11
12final case class Add(left: Expr, right: Expr) extends Expr:
13 def visit[A](visitor: ExprVisitor[A]): A =
14 visitor.add(left.visit(visitor), right.visit(visitor))
This is essentially a structured fold over the AST. The benefit is that new operations become new visitors. The cost is that new element types force visitor updates everywhere.
The trade-off is simple:
That means Visitor is better when operations vary more often than the underlying structure.
Teams sometimes implement full visitor boilerplate on top of a sealed trait hierarchy where a direct pattern match would be shorter, clearer, and easier to teach.
One small calculation becomes a visitor object, an accept method, and a pile of indirection with no real gain.
If new element types are added often, Visitor becomes painful because every visitor has to be extended in lockstep.
In Scala, prefer sealed traits plus pattern matching for closed hierarchies. Use Visitor when operations truly outnumber structure changes, when you need a named operation boundary, or when Java interop makes visitor-style separation the honest design.