1. 18
  1.  

    1. 11

      Pattern matching is (IMO) orthogonal to the visitor pattern, which is still useful, even with rich pattern matching syntax - notably, for complex object trees/graphs, such as abstract syntax trees, where the details of traversing the tree/graph is not something you want to be repeating every time you need to visit it. Pattern matching reduces the boilerplate of writing the traversal significantly, but doesn’t remove it completely. The visitor pattern approach lets you reuse that traversal in every visitor, while letting you override it for specific types as-needed (e.g. to short-circuit, ignore certain subtrees, etc.).

      There is no reason to use the visitor pattern when you don’t need to abstract over the traversal though - the example given in the post here seems to be using it for basic pattern matching uses, which, yeah, would be redundant with actual pattern matching syntax.

      1. 9

        Often, a better way to abstract away just the traversal is through the iterator interface:

        for node in tree.iter_pre_order() {
            match node {
                ...
            }
        }
        

        A typical syntax tree visitor is actually two patterns — a visitor/double dispatch per-se, plus an internal iterator.

        1. 3

          For sure! At least, so long as your language supports them. For example, Go didn’t support them for most of its existence, and I think that has only recently started to change (pretty sure I caught wind of discussion around that anyway). Of course it doesn’t have pattern matching either, so maybe it isn’t a great example here. I suppose the takeaway here is that, if your language provides better tools for the sort of use cases a visitor makes sense for, definitely use them!

        2. 3

          Pattern matching is (IMO) orthogonal to the visitor pattern, which is still useful, even with rich pattern matching syntax

          I don’t claim to understand how it’s implemented, but the widely used Rust serialization library serde uses visitor patterns, in a language where pattern matching is common and OOP patterns are rare.

          1. 2

            Could you give a simple example of abstracting over traversals, i.e. an example where you’d consider the visitor pattern is useful?

            (I’d like to understand if there are other ways than mere pattern matching to achieve the same thing in a functional programming setting.)

            1. 6

              Here is an example from the Python-to-C++ translator in Oils – mycpp

              I never liked visitors myself – the main reason this uses visitors is because MyPy does. (We reached into MyPy’s internals and borrowed its structure.)

              One thing that bugs me about visitors is that you lose the stack. You end up having to put more stuff in class members instead of params on the stack.

              It is kind of like a sync vs. state machine issue – the state machines are harder to write. async/await gives you back the stack.


              As some others have pointed out, there is one class SimpleVisitor that has the traversal logic

              And then we have an inherited const pass that does a “generic” traversal over all the string literal nodes, at arbitrary depths, wherever they appear

              class SimpleVisitor:
              
                  def visit_while_stmt(self, o: 'mypy.nodes.WhileStmt') -> None:
                      self.accept(o.expr)
                      self.accept(o.body)
              
                  def visit_return_stmt(self, o: 'mypy.nodes.ReturnStmt') -> None:
                      if o.expr:
                          self.accept(o.expr)
              
                 # ... 30 more methods, for every part of the AST
              

              https://github.com/oils-for-unix/oils/blob/master/mycpp/visitor.py

              and then

              class Collect(visitor.TypedVisitor):  # subclass of SimpleVisitor
              
                  # no traversal logic in this class
              
                  def visit_str_expr(self, o: StrExpr) -> None:
                      raw_string = format_strings.DecodeMyPyString(o.value)
                      self.global_strings.Add(o, raw_string)
              
              

              https://github.com/oils-for-unix/oils/blob/master/mycpp/const_pass.py#L107


              So you can have many passes, without repeating the traversal logic on the typed AST.

              Using the a preorder iterator like matklad suggested would be very possible here, although I stuck with the structure inherited from MyPy

              For other passes, where you need to keep track of some state per subtree, I am not sure if it would be as convenient


              Actually I noticed in Siek’s Essentials of Compilation - https://mitpress.mit.edu/9780262048248/essentials-of-compilation/

              He says you just need “open recursion”. You just use inheritance without visitors I think. I think that would work for this visit_str_expr() thing:

              https://journal.stuffwithstuff.com/2013/08/26/what-is-open-recursion/

              Visitors are controversial, and I have seen the exact argument in this thread before:

              • “you don’t need visitors if you use pattern matching!”
              • “yes visitors are still useful!”

              I would like someone to write up some definitive comparison of this, preferably in multiple languages. I think the issue is there are many types of tree traversals, and some are slightly more convenient in one style vs. another.

              Mycpp has 6 passes now, and over time the visitors have grown on me, although I would still like to try another style. Again I think it will probably be the case that certain passes are easier in one style vs. another.

              It’s certainly possible to use visitors for everything, and that’s what MyPy does!!! It seems very verbose and un-Python-like, but it does work … and it is much more complex than mycpp, being a gradual type checker

              1. 5

                Also I think the fact that Haskell has code generators (and research papers!) for tree traversals, and idiomatic Rust can make use of visitors (as mentioned in this thread), proves that it’s not a simple argument

                https://stackoverflow.com/questions/28244136/what-is-scrap-your-boilerplate

                Often when making transformations on complex data types, we only need to affect small pieces of the structure—

                The classic example is double-negation elimination over a type of integer expressions:

                https://hackage.haskell.org/package/TYB

                So Haskell has boilerplate too – just not visitor boilerplate. It’s not just “Java was a weak language so it needed visitors!”

                The issue is what kind of tree traversals you’re doing, and how many


                I do think that visitors can be thought of as a “hole” in the host language – but it’s also a hole in Haskell/Rust

                For the “find all string literals / cast expressions” use cases, you can actually do a little better with dynamic typing! That is, dynamically typed language are able to reflect on the types of nodes and the types of the children.

                So this actually made me wonder if you can write some kind of “visitor” language construct in a statically-typed language that uses RTTI – very similar to a garbage collector

                i.e. a garbage collector is a homogeneous traversal of a heterogeneous/typed graph – and for certain kinds of tree traversal use cases, you want the same thing. You can also think of a GC as “generic” or “dynamically typed”

                But no language exposes such a primitive AFAIK

                1. 3

                  The classic example is double-negation elimination over a type of integer expressions:

                  Finally, a small example. Ok, here we go:

                  {-# LANGUAGE DeriveDataTypeable #-}
                  module MyLib where
                  import Data.Data
                  import Data.Generics.Uniplate.Data
                  
                  data Exp = Plus Exp Exp | Mult Exp Exp | Negate Exp | Pure Int
                    deriving (Show, Eq, Data, Typeable)
                  
                  doubleNegSimpl :: Exp -> Exp
                  doubleNegSimpl = transform f where
                    f (Negate (Negate e)) = e
                    f x = x
                  
                  test :: Bool
                  test = doubleNegSimpl
                    (Plus
                      (Mult (Negate (Negate (Pure 1))) (Pure 2))
                      (Pure 3))
                    ==
                    (Plus
                      (Mult (Pure 1) (Pure 2))
                      (Pure 3))
                  

                  As you can see, no boilerplate. The key feature here is that algebraic datatypes, such as Exp, can be represented as sums of products, i.e. Exp * Exp + Exp * Exp + Exp + Int and this allows us to define functions that operate generically on all algebraic datatypes!

                  1. 2

                    I think you have to add ternary operator, logical operators, etc. to see the boilerplate. (I don’t know Haskell, but I know a bit of OCaml)

                    The argument is that people wrote these for a reason …

                    https://hackage.haskell.org/package/syb

                    https://hackage.haskell.org/package/TYB

                    Pattern matching is good for certain type of traversals. You can do that in Java too now.

                    But there are also cases where you would want visitors / “boilerplate generators”

                    1. 3

                      The argument is that people wrote these for a reason …

                      No, that’s not an argument. The library that I used, uniplate, is a variant of SYB and I explained how they work generically for all datatypes. There’s no boilerplate being generated, the functions (e.g. transform) are defined to work on all algebraic datatypes via the sums-of-products encoding. It’s a language feature that lets you program over these encodings – abstracting over the shape of the data!

                      1. 2

                        Your original query was

                        (I’d like to understand if there are other ways than mere pattern matching to achieve the same thing in a functional programming setting.)

                        Glad you understand now :)

                        I already mentioned SYB, I didn’t know what Uniplate was, so now I know it is some variant of that

                        1. 1

                          I’m still not sure if the double negation elimiation example is an example of what the parent commenter meant with:

                          “There is no reason to use the visitor pattern when you don’t need to abstract over the traversal though”.

                    2. 2

                      I don’t know Haskell well enough to say this with confidence, but that looks almost exactly like what I’d write with visitors. If I understand this correctly, the transform function is doing exactly what the visitor pattern does: abstract over the traversal of the tree, such that you only have to deal with a node at a time. But exactly as @bitwalker said earlier, debating over an example doesn’t really do either pattern justice ¯_(ツ)_/¯

                      FWIW I can’t immediately think of a reason why, with reflection, you couldn’t write this example in Java either.

                      1. 2

                        My understanding of the difference is: you need to write the visitor pattern for each datatype before you can use it to do the traversal. In Haskell, which support generic programming, one can write transform once and for all datatypes.

                        FWIW I can’t immediately think of a reason why, with reflection, you couldn’t write this example in Java either.

                        I believe reflection happens at run-time, while generic programming is at compile-time.

                        1. 1

                          I believe reflection happens at run-time, while generic programming is at compile-time.

                          That’s not an important distinction here, just an implementation detail.

                          But there are annotation processors that can generate any code at compile time, so I doubt it would be impossible to implement in java the same-ish way.

                          1. 1

                            In one case you pay a run-time performance penalty or have to generate code (increasing compilation time), in the other case you don’t. I wouldn’t call that an implementation detail, because I don’t see how you could avoid the negative aspects with an alternative implementation in the Java case.

                            Generic programming is an example of an abstraction where you use the types to get programs for free.

                            1. 2

                              (Asking from a Rust perspective) Doesn’t generic programming also entail either a run-time cost (if implemented with dynamic dispatch) or a compile-time cost (if implemented with monomorphization)?

                              1. 2

                                Yeah, I also don’t get the difference. He seems to be using a language extension in Haskell’s case, so it’s not even completely “fair”.

                                1. 1

                                  Perhaps I should have tried to avoid using “generic”, as it is an overloaded word and means different things in different programming language contexts.

                                  Java’s generics gives us polymorphism, i.e. we can define the size function for a List<A> where A can be any type (ints, bools, etc).

                                  Haskell’s generic programming allows us to define the size function for arbitrary algebraic datatypes (e.g. the same function works for List<A>, BinaryTree<A>, etc).

                                  1. 2

                                    But that by necessity has to be done by either monomorphization (e.g. at call time it will generate a new instance for the concrete type for better optimizations) or by boxing (there will be only a single implementation, but that can only operate on uniform objects, so everything will be boxed and done with virtual calls. That is either a compile time or a runtime cost either way.

                                    I’m no expert in Haskell, but I believe it does the latter (almost everything is a thunk, is it not?)

                                    E.g. see https://lobste.rs/s/jdgjjt/visitor_pattern_considered_pointless#c_qpffms

                                    1. 1

                                      For polymorphism (Java Generics), yes. For Generic programming, I’m not sure.

                                      (For the sake of this discussion, I believe polymorphism isn’t relevant.)

                                      1. 1

                                        I have edited my comment, and you may not have seen the new additions.

                                        I think it necessarily has to boil down to either code gen or uniform structure at one point though, otherwise I don’t see how it could execute differently for different types.

                                        Of course my low-level view may not be relevant here, as this is more about a higher-level feature. But then I just don’t see why annotation processors or reflection would be dismissed. Sure, Haskell’s solution is more elegant, but is not fundamentally superior.

                                        Nonetheless , thanks for showing me this Haskell feature!

                                        1. 2

                                          uniform structure at one point though

                                          In some sense it’s a uniform structure: it’s the normal form of algebraic datatypes: sums-of-products, i.e. same as the normal form of polynomials in algebra. But I think it’s different from boxing types via pointers to achieve polymorphism.

                                          Here’s a sketch of a language with generic programming capabilities:

                                          1. Instead of exposing datatype declaration a la Haskell, i.e. data D = C1 A1 ... A_m | ... | C_n one defines datatypes using the building blocks of sums-and-products;
                                          2. Allow the user to define functions over these building blocks (i.e. abstract over the shape of the data).

                                          So for example, instead of data List a = Nil | Cons a (List a) one would write something like: data List a = Const Unit + Const a * Rec or data BinTree a = Const a + Rec * Rec.

                                          Generic programs would then be defined using the building blocks, something like e.g.:

                                          size : (S : Shape) -> Data S -> Int
                                          size (f + g) (Inl l) = size f l
                                          size (f + g) (Inr r) = size g r
                                          size (f * g) (l, r) = max (size f l) (size g r)
                                          size Rec ih = ih + 1
                                          size (Const x) _ = 0
                                          

                                          And this size function would work for both List and BinTree, similarly one could define transform from above using the same way and thus avoid the visitor boilerplate without compile-time code generation or run-time overhead, I think.

                      2. 1

                        For the “find all string literals / cast expressions” use cases, you can actually do a little better with dynamic typing! That is, dynamically typed language are able to reflect on the types of nodes and the types of the children.

                        To me this sounds like (in Rust):

                        enum Node {
                            StringLiteral { content: String },
                            ...
                        }
                        
                        match node {
                            Node::StringLiteral { content } => ...,
                            ...
                        }
                        

                        which in, say, JavaScript would be like

                        const nodeTypes = {
                            stringLiteral = {},
                            ...
                        };
                        
                        if (node.nodeType === nodeTypes.stringLiteral) {
                            ...
                        }
                        

                        which doesn’t seem better to me.

                        What am I missing?

                    3. 5

                      Almost every linting tool that supports custom lints implements those through the visitor pattern. Consider Rubocop and ESLint. I’m pretty sure that’s the exact scenario GP is talking about - writing custom rules with pattern matching and without visitors would require each rule to navigate the entire tree.

                      At the same time, I don’t know if asking for a “simple” example makes the case for whether or not the visitor pattern is useful. Maybe there’s no simple cases, but a lot of really complex ones?

                      1. 6

                        ’m pretty sure that’s the exact scenario GP is talking about - writing custom rules with pattern matching and without visitors would require each rule to navigate the entire tree.

                        Yeah exactly, both @andyc’s and @david_chisnall’s comments are other great examples.

                        Maybe there’s no simple cases, but a lot of really complex ones?

                        100%. The visitor pattern (and IMO, design patterns in general) exist to manage complexity that only emerges at larger scales. Any simple example will look dumb in comparison to simpler, better suited, language constructs - e.g. pattern matching.

                        I think you can try to strip out a lot of the irrelevant details of a real-world use case, and rely on the reader to extrapolate why a pattern is useful in that case, and have that be helpful sometimes. But in my experience, if the reader doesn’t believe it is useful to begin with, such attempts usually devolve into debating the example code, and not the pattern being demonstrated.

                        1. 2

                          At the same time, I don’t know if asking for a “simple” example makes the case for whether or not the visitor pattern is useful. Maybe there’s no simple cases, but a lot of really complex ones?

                          I believe one can always extract the essense of a problem into a small example, from which it’s clear that the technique will continue to work even if the complexity of the example increases. For example, the double elimination example from else where in the thread, it’s clear that this can work even if the abstract syntax tree gets more complex.

                          1. 4

                            There’s two points to talk about here:

                            First, TFA (and OP) specifically are talking about visitors vs pattern matching. You might think there’s other language features which replace visitors, but that’s not the point under contention here, so I’m not going to talk about your example which uses an entirely different language with far more powerful features than “just” pattern matching.

                            Then,

                            I believe one can always extract the essense of a problem into a small example,

                            You’re specifically talking about your example, but I believe you missed my point. All I said was that not having a simple example to prove that a pattern is useful isn’t directly proof that the pattern is entirely pointless, per TFA.

                            1. 1

                              Regarding your first point, in a later comment I said:

                              More generally, I believe there’s the belief in the functional programming community that all GOF OOP patterns are subsumed by the relatively stronger notions of abstraction available in FP.

                              Basically I’m trying to find out if this is true or not.

                              Regarding your second point: That’s not what I meant and I agree that not having a simple example doesn’t prove that the pattern isn’t useful. To be clear: I also don’t doubt that the visitor pattern is useful. I’m merely trying to understand if the reply to your first point is true or not.

                        2. 5

                          LLVM has a bunch of visitor classes using CRTP. The useful thing versus raw pattern matching is the ability to do more generic things. The clang AST visitors make it trivial to find specific nodes at arbitrary depths, ignoring intermediates. For example, if I want to write a clang-tidy rule about C-style casts (I don’t, it’s there already) I can write a visitor that matches cast expressions. These will typically be ten or more levels deep in the tree, but I don’t need to care about any of the nodes that I need to travers to get there.

                          For a shallower tree, LLVM’s instruction visitor visits every instruction in a function. The traversal is trivial here (two-deep nested iterators) but the subtyping can be complex. For a simple example, in some cases I care about call instructions (which call non-throwing things and so have no intraprocedural control flow) and may want to handle invokes (which branch to one of two basic blocks to model exception flow) separately, in other cases I care about either and it’s the callee that matters not how it was called. In this case, I can implement the CallSite visitor method and get both via a wrapper type, rather than having to match call and invoke instructions and then dispatch to a common type myself. Similarly, it I care about binary integer arithmetic operations, I can match this in a single method. The visitor also handles priorities, so if I implement the method for binary operations and the method for add instructions, I will get the specific one called for adds and the general one called for everything else.

                          I could implement the same thing with iterators and pattern matching equivalent code (if (auto Foo = dyn_cast<Thing>()) as the closest approximation) but even with more concise pattern matching syntax it wouldn’t be as good as using the visitors.

                          1. 2

                            I ask for a simple example and I get LLVM… Classic C++ programmers ;-). Let me try again: can you show me a small self-contained example?

                            From what I can gather, typically in Haskell we’d use generic programming (unrelated to generics in Java) in these situations. I believe, for example, the uniplate library could achieve what you describe without boilerplate. (But I’m not sure, that’s why it would be nice to have a small example to try it out on.)

                            More generally, I believe there’s the belief in the functional programming community that all GOF OOP patterns are subsumed by the relatively stronger notions of abstraction available in FP.

                            1. 6

                              I doubt you’ll find a small example because it’s a pattern that is only provides a big benefit for complex data structures. Trees with a lot of different node kinds, with subtyping relationships (which may or may not be subtypes in the language) between them. A minimal example where that would show up is going to be thousands of lines of code.

                              In Haskell, assuming the nodes are syubtypes, the things that need to be per-data-structures in C++ could be implemented as generic functions (assuming that the logical subtyping is actually expressed as subtyping relationships in the code).

                          2. 4

                            Sure, or at least, I can give you a example based on a DSL compiler I worked on awhile back, in Rust:

                            Let’s say you have an AST, for a language with modules, functions, various global declarations (such as imports of items from other modules, constants, etc.), and expressions consisting of let-bindings, various arithmetic operations, function invocation, comprehensions.

                            Your AST consists of structs for each node type, something like (intentionally over-simplified here):

                            pub struct Module {
                                name: Symbol,
                                functions: BTreeMap<Symbol, Function>,
                            }
                            
                            pub struct FunctionIdent {
                                module: Symbol,
                                name: Symbol,
                            }
                            
                            pub struct Function {
                                id: FunctionIdent,
                                signature: Signature,
                                /// `None` indicates this is an imported declaration
                                body: Option<Box<Expr>>,
                            }
                            
                            pub enum Expr {
                                Var(Symbol),
                                Lit(Literal),
                                BinaryOp(BinaryOp),
                                UnaryOp(UnaryOp),
                                Let(Let),
                                Call(Call)
                                Lc(ListComprehension),
                            }
                            
                            /// <lhs> <opcode> <rhs>
                            pub struct BinaryOp {
                                opcode: BinaryOperator,
                                lhs: Box<Expr>,
                                rhs: Box<Expr>,
                            }
                            
                            pub enum BinaryOperator {
                                Add,
                                Sub,
                                ...
                            }
                            
                            pub struct Call {
                                callee: Callee,
                                args: Vec<Expr>,
                            }
                            
                            pub enum Callee {
                                Alias(Symbol),
                                Qualified(FunctionIdent),
                            }
                            
                            /// let <bound> = <init> in <body>
                            pub struct Let {
                                bound: Symbol,
                                init: Box<Expr>,
                                body: Box<Expr>,
                            }
                            
                            /// <body> for <binding> in <iterable>
                            /// <body> for (<binding0>, .., <bindingN>) in (<iterable0>, .., <iterableN>)
                            pub struct ListComprehension {
                                bindings: Vec<Symbol>,
                                iterables: Vec<Expr>,
                                body: Box<Expr>
                            }
                            
                            /// snip...
                            

                            We define the basis of our reusable visitor via a Visit trait, and a set of default functions that implement the traversal details:

                            use core::ops::ControlFlow;
                            
                            use crate::ast;
                            
                            /// Implement this trait to define a visitor.
                            ///
                            /// By default, all methods will simply traverse the AST.
                            ///
                            /// # Example
                            ///
                            /// The following demonstrates the basic usage patterns:
                            ///
                            /// ```rust
                            /// use crate::{ast, visit};
                            ///
                            /// struct PreOrderVisitor;
                            ///
                            /// impl Visit<()> for PreOrderVisitor {
                            ///     fn visit_expr(&mut self, expr: &ast::Expr) -> ControlFlow<()> {
                            ///         // do stuff here...
                            ///         visit::visit_expr(self, expr)
                            ///     }
                            /// }
                            ///
                            /// struct PostOrderVisitor;
                            ///
                            /// impl Visit<()> for PostOrderVisitor {
                            ///     fn visit_expr(&mut self, expr: &ast::Expr) -> ControlFlow<()> {
                            ///         visit::visit_expr(self, expr)?;
                            ///         // do stuff here...
                            ///         ControlFlow::Continue(())
                            ///     }
                            /// }
                            /// ```
                            pub trait Visit<T> {
                                fn visit_module(&mut self, module: &ast::Module) -> ControlFlow<T> {
                                    visit_module(self, module)
                                }
                                fn visit_expr(&mut self, expr: &ast::Expr) -> ControlFlow<T> {
                                    visit_expr(self, expr)
                                }
                                fn visit_let(&mut self, expr: &ast::Let) -> ControlFlow<T> {
                                    visit_let(self, expr)
                                }
                                /// snip...
                            }
                            
                            /// Automatically implement `Visit` for references to a `Visit` impl.
                            ///
                            /// Primarily useful for allowing the use of `dyn Visit` in place of a concrete type.
                            impl<'a, V, T> Visit<T> for &'a V
                            where
                                V: ?Sized + Visit<T>,
                            {
                                fn visit_module(&mut self, module: &ast::Module) -> ControlFlow<T> {
                                    (**self).visit_module(module)
                                }
                                fn visit_expr(&mut self, expr: &ast::Expr) -> ControlFlow<T> {
                                    (**self).visit_expr(expr)
                                }
                                fn visit_let(&mut self, expr: &ast::Let) -> ControlFlow<T> {
                                    (**self).visit_let(expr)
                                }
                                /// snip...
                            }
                            
                            /// These functions allow resuming traversal of a given node type from within `Visit`
                            /// when one has overridden the method for that node type. They are exported for
                            /// use by `Visit` implementations. 
                            
                            pub fn visit_module<V, T>(visitor: &mut V, module: &ast::Module) -> ControlFlow<T>
                            where
                                V: ?Sized + Visit<T>,
                            {
                                for function in module.functions.values() {
                                    visitor.visit_function(function)?;
                                }
                                ControlFlow::Continue(())
                            }
                            
                            pub fn visit_function<V, T>(visitor: &mut V, function: &ast::Function) -> ControlFlow<T>
                            where
                                V: ?Sized + Visit<T>,
                            {
                                match function.body.as_ref() {
                                    None => visitor.visit_function_declaration(function),
                                    Some(body) => visitor.visit_expr(body),
                                }
                                ControlFlow::Continue(())
                            }
                            
                            pub fn visit_expr<V, T>(visitor: &mut V, expr: &ast::Expr) -> ControlFlow<T>
                            where
                                V: ?Sized + Visit<T>,
                            {
                                match expr {
                                    ast::Expr::Var(var) => visitor.visit_var(var),
                                    ast::Expr::Lit(lit) => visitor.visit_literal(lit),
                                    ast::Expr::Let(r#let) => visitor.visit_let(r#let),
                                    // snip...
                                }
                            }
                            
                            // snip..
                            

                            So that’s the boilerplate, let’s look at what that actually amounts to in practice. As an example, let’s write a semantic analysis pass that verifies that all callees of a Call expression are valid symbols in scope:

                            #[derive(Default)]
                            pub struct VerifyCallTargets {
                                current_module: Symbol,
                                scope: BTreeSet<ast::FunctionIdent>,
                            }
                            
                            impl VerifyCallTargets {
                                pub fn verify(&mut self, module: &ast::Module) -> Result<(), SemanticAnalysisError> {
                                    match self.visit_module(module) {
                                        ControlFlow::Continue(_) => Ok(()),
                                        ControlFlow::Break(err) => Err(err),
                                    }
                                }
                            
                                fn verify_callee(&self, callee: FunctionIdent) -> ControlFlow<SemanticAnalysisError> {
                                    if self.scope.contains(&callee) {
                                        ControlFlow::Continue(())
                                    } else {
                                        ControlFlow::Break(SemanticAnalysisError::UndefinedFunction(callee))
                                    }
                                }
                            }
                            
                            impl Visit<SemanticAnalysisError> for VerifyCallTargets {
                                fn visit_module(&mut self, module: &ast::Module) -> ControlFlow< SemanticAnalysisError > {
                                    self.scope.clear();
                                    self.scope.extend(module.functions.values().map(|f| f.id));
                                    visit::visit_module(self, module)
                                }
                            
                                fn visit_call(&mut self, call: &ast::Call) -> ControlFlow<SemanticAnalysisError > {
                                    match call.callee {
                                        Callee::Alias(name) => self.verify_callee(FunctionIdent { module: self.current_module, name }),
                                        Callee::Qualified(id) => self.verify_callee(id),
                                    }
                                }
                            }
                            

                            I’d like to reiterate that this isn’t the actual implementation I was working with, just a vastly simplified example based on a real implementation, so I’m just trying to illustrate the gist of it without getting too deep in the weeds on ancillary stuff that isn’t important (i.e. you’d ideally do a bunch of related checks in a single traversal, you’d want to gather all errors you can rather than short-circuiting the traversal at the first error, and so on).

                            Happy to answer any questions about this, but hopefully this was helpful in illustrating how this can solve problems in practice, even when you have pattern matching in your toolbox.

                        3. 10

                          Mild counter-take: the visitor pattern is useful even in languages which have always put pattern-matching front and center, depending on what you prioritize. Specifically: do you want to make it easier to extend the number of variants in the system at low cost of change, or do you want to make it easier to extend the set of operations in the system at low cost of change? The (in)famous “expression problem”, in other words—on which Eli Bendersky’s post (lengthy) or even this set of Q&As on Stack Overflow are much better explanations than e.g. the Wikipedia article.

                          The novelty for Java (and, a few years ago, C♯) is that you now actually have both options on the table, instead of only the visitor pattern.

                          Edit to add: @david_chisnall mentions this in regard to C++ and LLVM in a sibling thread, but I’ll second it: the visitor pattern is fantastic for working with ASTs while skipping intermediate nodes. I have done a fair bit of that while using it for code mods or other such transformations with JavaScript/TypeScript codebases. You only implement the parts of the interface you need to for any given node and everything else just gets ignored.

                          1. 5

                            I haven’t written Java in a decade, so gf0’s comment about how much Java has improved made me curious. I found an article from 2020 about algebraic data types in Java and saw that the article promoted the visitor pattern.

                            Since I hated the visitor pattern’s boilerplate and confusing code flow the last time I implemented it, I wondered whether it was really still necessary in Java. I am happy to find it is not.

                            1. 1

                              That was new to me too, very handy. Thanks for the link.

                            2. 4

                              I hate design pattern overuse as much as the next guy, but calling a pattern pointless because you don’t need it for the first contrived usage you find on Wikipedia is not a good argument. The author should find actually concrete uses of visitors in the wild and talk about why they’re pointless. IMO they are not - there’s plenty of comments on this thread for scenarios where they apply.

                              1. 2

                                So many “patterns” people talk about in Java land have always baffled me because they all seem to be elaborate shenanigans to work around Java’s under-expressive rigid type system. I started programming in BASIC, Ruby, JavaScript, Go where you’d just start out by writing the switch statement or if-else stack without compiler safety, and then I jumped straight to languages with tagged union types like Typescript, Kotlin (sealed class…), Rust, Swift.

                                1. 0

                                  I mean this is a great way to confirm that all of those patterns mostly exist to work around the dysfuntional Java programming language.

                                  Imagine doing software engineering and then having to haggle with people over the minute details of this class or that and whether some interface is designed this way or that. I’ve done it in the distant past and it’s a great way to spend lots of time producing nothing.

                                  1. 6

                                    There’s plenty of examples in this thread showing how it’s not just Java - C++ came up earlier, the sibling comment mentions Haskell, and I’ve personally seen it be very useful in Rust, JavaScript, Python and Ruby. It’s just a useful model for a set of cases.

                                    1. 5

                                      exist to work around the dysfuntional Java …

                                      And Haskell!

                                      https://en.wikipedia.org/wiki/Catamorphism

                                      1. 1

                                        exist to work around the dysfuntional Java programming language

                                        Wouldn’t that be Smalltalk?