Yes, you should probably start off with pattern matching instead of the visitor pattern. See this interview with Martin Odersky (my emphasis):
So the right tool for the job really depends on which direction you
want to extend. If you want to extend with new data, you pick the
classical object-oriented approach with virtual methods. If you want
to keep the data fixed and extend with new operations, then patterns
are a much better fit. There’s actually a design pattern—not to be
confused with pattern matching—in object-oriented programming called
the visitor pattern, which can represent some of the things we do with
pattern matching in an object-oriented way, based on virtual method
dispatch. But in practical use the visitor pattern is very bulky. You
can’t do many of the things that are very easy with pattern matching.
You end up with very heavy visitors. And it also turns out that with
modern VM technology it’s way more innefficient than pattern matching.
For both of these reasons, I think there’s a definite role for pattern
matching.
EDIT: I think this requires a bit of a better explanation, and an example. The visitor pattern is often used to visit every node in a tree or similar, for instance an Abstract Syntax Tree (AST). Using an example from the excellent Scalariform. Scalariform formats scala code by parsing Scala and then traversing the AST, writing it out. One of the provided methods takes the AST and creates a simple list of all of the tokens in order. The method used for this is:
private def immediateAstNodes(n: Any): List[AstNode] = n match {
case a: AstNode ⇒ List(a)
case t: Token ⇒ Nil
case Some(x) ⇒ immediateAstNodes(x)
case xs @ (_ :: _) ⇒ xs flatMap { immediateAstNodes(_) }
case Left(x) ⇒ immediateAstNodes(x)
case Right(x) ⇒ immediateAstNodes(x)
case (l, r) ⇒ immediateAstNodes(l) ++ immediateAstNodes(r)
case (x, y, z) ⇒ immediateAstNodes(x) ++ immediateAstNodes(y) ++ immediateAstNodes(z)
case true | false | Nil | None ⇒ Nil
}
def immediateChildren: List[AstNode] = productIterator.toList flatten immediateAstNodes
This is a job which could well be done by a visitor pattern in Java, but much more concisely done by pattern matching in Scala. In Scalastyle (Checkstyle for Scala), we use a modified form of this method, but with a subtle change. We need to traverse the tree, but each check only cares about certain nodes. For instance, for the EqualsHashCodeChecker, it only cares about equals and hashCode methods defined. We use the following method:
protected[scalariform] def visit[T](ast: Any, visitfn: (Any) => List[T]): List[T] = ast match {
case a: AstNode => visitfn(a.immediateChildren)
case t: Token => List()
case Some(x) => visitfn(x)
case xs @ (_ :: _) => xs flatMap { visitfn(_) }
case Left(x) => visitfn(x)
case Right(x) => visitfn(x)
case (l, r) => visitfn(l) ::: visitfn(r)
case (x, y, z) => visitfn(x) ::: visitfn(y) ::: visitfn(z)
case true | false | Nil | None => List()
}
Notice we’re recursively calling visitfn()
, not visit()
. This allows us to reuse this method to traverse the tree without duplicating code. In our EqualsHashCodeChecker
, we have:
private def localvisit(ast: Any): ListType = ast match {
case t: TmplDef => List(TmplClazz(Some(t.name.getText), Some(t.name.startIndex), localvisit(t.templateBodyOption)))
case t: FunDefOrDcl => List(FunDefOrDclClazz(method(t), Some(t.nameToken.startIndex), localvisit(t.localDef)))
case t: Any => visit(t, localvisit)
}
So the only boilerplate here is the last line in the pattern match. In Java, the above code could well be implemented as a visitor pattern, but in Scala it makes sense to use pattern matching. Note also that the above code does not require a modification to the data structure being traversed, apart from defining unapply()
, which happens automatically if you’re using case classes.