But I really don't see the point of that accept call. If you've got the visitor and the objects to be visited, why not just pass these objects directly to the visitor and avoid the indirection?
Christophe's answer is on point, I just want to expand on that. Not knowing the runtime type of the object is actually an assumption of the Visitor pattern.
You can understand the pattern in two ways. The first one is that it's a trick to do multiple dispatch in a single-dispatch language. The other is that it's a way to do abstract data types in OOP languages. Let me explain.
You see, there are two major approaches to data abstraction 1. OOP achieves it by abstracting away procedure calls. As in, you are actually specifying an abstract operation when you're making the call (you're specifying "the message"), and the actual function you're calling is being resolved by some underlying mechanism. This underlying mechanism allows objects to respond to a certain interface (a set of public methods/messages), which makes it easy to add new representations (by subclassing), but harder to add new operations. Note that, when utilizing this sort of polymorphism, while the code that creates the objects knows concrete types, other client code is written in terms of the abstract type (and in case of OOP, that specifically means in terms of the interface defined by that abstract type).
The other approach is abstract data types (ADTs), where a finite set of representations (concrete data types) is abstracted away and treated as a single data type. In contrast to OOP, you're now calling concrete functions, but you're passing in a data abstraction. I.e., the parameter type is never concrete, and client code never works with or has knowledge of concrete representations (except at construction sites, but the same is true for OOP). There's an underlying mechanism that allows functions to identify (or match to) a concrete type, and each operation must support all representations (or, in terms of the Visitor pattern, each concrete Visitor must handle all Element types). At the simplest form it's something like a switch statement, in functional languages it manifests as pattern matching, and in the Visitor pattern it's encoded in the abstract Visitor interface (an abstract visit method for each possible element type) that each derivative must support in a meaningful way. The tradeoffs for this kind of data abstraction are the other way around - it's easy to add new operations, but it's hard to add new representations (new element types).
So, with that in mind, the Visitor pattern is good for scenarios where you can expect the operations to change more frequently compared to representations, i.e., scenarios where the number of different element types is expected to be finite and relatively stable.
I've noticed that you've linked to a page called "Crafting Interpreters: The visitor pattern". The use case there demonstrates this idea - the underlying data structure is an expression tree, which consists of nodes that can be represented in different ways (have different data types). There's a finite number of representations (defined by the rules of the language), but they are all rolled into an abstract data type representing an expression tree (Expr
). You can then define a number of concrete visitors representing different generalized operations that can be applied to that tree. The external (client-facing) interface of each visitor only uses the abstract type, Expr
, which then lets you write client code only in terms of this abstraction (i.e., client code doesn't have to know the concrete types of each node, just that it's an expression tree, and that there's a number of operations that can be applied to it). I know that the examples there construct the tree right before it's used, but a more realistic scenario is reading some code from a file and returning an abstract syntax tree.
Interestingly, in that article, the Visitor pattern is kind of implemented backwards; their example of client code is:
new AstPrinter().print(expression)
whereas it should be:
expression.accept(new AstPrinter())
since AstPrinter is the "visiting" operation (but then the method of extracting the result from the AstPrinter would be different). This sort of reversal is more suitable when you're implementing an internal iterator (where you pass the operation to the iterator, but then the iterator internally passes the elements to the operation).
If you find the accept/visit naming confusing, you can mentally rename these methods:
element.accept(visitor)
// can be seen as:
abstractType.do(operation)
and
visitor.visit(this)
// can be seen as:
operation.applyTo(concreteType)
An important thing to realize is that the Visitor interface (the various visit
overloads) are meant to be treated as internal to the type abstraction (in other words, they are there to (1) be called internally by concrete elements, and (2) to be implemented by Visitor-derivatives; they are not meant to be used by client code).
1 The two approaches involve different tradeoffs; this is known in the CS community as the "expression problem".