
Zero-Overhead Tree Processing with the Visitor Pattern - lihaoyi
http://www.lihaoyi.com/post/ZeroOverheadTreeProcessingwiththeVisitorPattern.html?a=1
======
richard_shelton
In fact, it's a rather old question: what is better 1) a declarative approach
with pattern matching or 2) Visitor pattern from OOP world.

Ok, better for what? Let's take a practical example: AST transformations
inside a compiler. You may find Visitor pattern use only in a few Java-related
compiler textbooks. For some reason almost every good compiler/compiler
textbook uses pattern matching: Appel, nanopass, CompCert, DMS etc.

Ok, but the real point of the article is not that "visitors" are more readable
and easy to use, but they are more perfomant, right? Do we have any profiling
here? Maybe with a good GC it will cost almost nothing for us to build another
tree in terms of speed? We remember the words about "premature optimization",
and any "manual optimization" which makes the algorithm structure foggy is
evil so (necessary evil -- in some rare cases). In some declarative languages
pattern matching constructs are converted to a fast decision trees by
compiler. There are some "lazy" approaches to pattern matching.

Or you can just provide a list of transforming (maybe some composition of
transformations) functions to your generic pattern matching function. It's
almost the same as using "visitors", but you don't need to call "The Team
Architect™, for supervising the object oriented quality of your software" :)

~~~
lihaoyi
> You may find Visitor pattern use only in a few Java-related compiler
> textbook

The Java standard library uses ASM heavily for it’s method handle JIT compiler
implementation, which is all visitor based. As does the Scala compiler. ASM is
extremely widespread for anyone doing jvm bytecode work and isn’t exactly
something you find only in textbooks

> Maybe with a good GC it will cost almost nothing for us to build another
> tree in terms of speed

Not sure what environment you work in, but converting uPickle to a visitor-
based architecture from recursive transformation sped it up 3x on the JVM and
10x on Node.js. I’d love to have a GC that could elide the intermediate trees
but the GCs I have aren’t up to snuff

~~~
richard_shelton
I guess things you've mentioned are widespread inside the Java community. But
let's consider the things that have more global impact to compiler writers.
For example, Scala LMS is a very interesting and promising recent project.
Take a look at its source code -- no Visitor pattern inside. Visitor pattern
may be necessary part of "Java culture", but for Scala I see no real reason to
keep using it.

P.S. Thanks for the real perfomance data!

~~~
v-yadli
Not quite a part of "Java culture". See Roslyn compiler, .NET Expression tree,
C++ `std::visit` or tons of DOM tree rewriting js stuff.

Actually `visitor` and `pattern matching` are not mutually exclusive. imo the
only difference is that in the visitor pattern, if you don't have anything to
update, you just return the original node.

~~~
richard_shelton
Thank you! I agree that just using of pattern matching may be not enough. But,
instead of Visitor Pattern, I would use generic traversal "strategies"
(combinators). My ideal is a DSL with first-class term rewriting rules + user
definable strategies which take these rules as arguments. In fact, I already
use such DSL for compiler writing :)

------
tel
This is pretty common in Haskell using the "recursion schemes" technique. The
idea is to note that any recursive data type can be constructed by laying many
"layers" down. For instance

    
    
        data Json
          = Str String
          | Int Int
          | Dict [(String, Json)]
    

has the "layer" type

    
    
        data JsonLayer x
          = StrL String
          | IntL Int
          | DictL [(String, x)]
    

You could also "layerize" the list in here which is what the author did with
the DictVisitor, but I'll leave it as is for simplicity now.

We can recover the original type using the layers and the "type level fixed
point operator"

    
    
        newtype Fix f = Fix { unwrap :: f (Fix f) }
    

If you unwind this, it says

    
    
        Fix JsonLayer = JsonLayer (Fix JsonLayer) = JsonLayer (JsonLayer (Fix JsonLayer)
                      = JsonLayer (JsonLayer (JsonLayer (...))
    

On the other hand, we can recursively consume values of `Fix JsonLayer` (and,
equivalently, Json) with functions of the shape

    
    
        type Alg f a = f a -> a
    
        type JsonAlg a = JsonLayer a -> a
    

In other words, it tells us how to get a result from a single layer (where the
recursive parts have already had their results computed). This is the Json
visitor, though you can rewrite the type to be something more familiar

    
    
        -- isomorphic to Alg JsonLayer a
        data JsonVisitor a = 
          JsonVisitor
          { onStr :: String -> a
          , onInt :: Int -> a
          , onDict :: [(String, a)] -> a
          }
    

You can write a function which consumes Fix'd data types using an appropriate
Alg that is totally generic, or you can write a specific one for consuming
Json using `Alg JsonLayer`

    
    
        -- commonly seen as consume :: (JsonLayer a -> a) -> (Json -> a)
        consume :: JsonVisitor a -> (Json -> a)
        consume v (Str s) = onStr v s
        consume v (Int i) = onInt v i
        consume v pairs = onDict v $ map (\(n, json) -> (n, consume v json)) pairs

~~~
dllthomas
> You could also "layerize" the list in here

Is that the answer to "how do recursion schemes handle heterogenous layers"?
I'd love to learn more.

~~~
tel
The general solution for heterogenous layers requires indexing the layers. The
x type variable ends up having kind `Index -> *` for your index type Index.

------
peterkelly
Wadler, Philip (1990). "Deforestation: transforming programs to eliminate
trees". Theoretical Computer Science. 73 (2): 231–248.

[http://homepages.inf.ed.ac.uk/wadler/papers/deforest/defores...](http://homepages.inf.ed.ac.uk/wadler/papers/deforest/deforest.ps)

~~~
Yoric
Not 100% the same thing, but definitely related. Thanks for the reference :)

------
ginko
How is it zero-overhead? The visitor pattern at the very least requires
dynamic dispatch, which isn't free.

~~~
IloveHN84
In JVM languages maybe, in C++ with the CRTP there's zero overhead for sure

~~~
xxs
Polymorhic call sites can hardly be called 'zero overhead' \- they remove any
chance for inlining to begin with. So no, JVM is not amongst them

~~~
weberc2
I don’t think this is true. If you know the set of functions that can be
dispatched to, you could inline them in a jump table / switch statement. JVM
could definitely do this since it doesn’t even need to know the full set; it
could inline new functions as it encounters them. This is all theoretical, of
course.

~~~
dnomad
This is exactly what the JVM does. See [0].

[0] [https://mechanical-sympathy.blogspot.com/2012/04/invoke-
inte...](https://mechanical-sympathy.blogspot.com/2012/04/invoke-interface-
optimisations.html)

~~~
xxs
>>jump table / switch statement.

>This is exactly what the JVM does. See [0].

The jump table from Martin's blog is the virtual call one.

------
falcolas
Meta about the article: "Let's use a simplified JSON style tree consisting of
only a few types" followed immediately by "Here's the Scala code with a
discussion of Scala specific implementation details".

Not exactly all that friendly a method to discuss a topic. Someone at a point
in their career where they may have derived value from this blog post is
likely to be immediately and hopelessly lost.

Instead, introduce one thing at a time. If you're talking about a new concept,
don't introduce it using a complex language - use something the reader will be
familiar with. I personally dislike JavaScript, but it's still a much better
choice for this. Or Python. Or even plain old Java - lots of folks cut their
teeth in college on Java.

~~~
philipkglass
The author writes Scala libraries and tools and blogs about Scala. It's "not
friendly" only if you're expecting him to write for a different audience than
the one he's addressing.

------
lmz
The parsing example is very similar to "push" / event based parsers e.g. the
SAX XML parsing API.

~~~
zaphar
I was going to say the same thing. I've converted XML processing code that
took hours to run into code that took minutes just by switching to SAX
parsing. The lower overhead has a drastic impact on runtime.

------
louthy
Or, use pattern matching for significantly less boilerplate. Double dispatch
isn’t free either.

~~~
ScalaMilano
Or use type classes.

I do think visitor pattern, type classes and pattern matching have different
areas where responsibility lies and how it is distributed (with pattern
matching the most local, visitor pattern extensible without by classes and
type classes - which I prefer - extensible at the call site).

~~~
dnomad
I think the Visitor pattern is an antipattern. I've never seen a non-trivial
application that didn't become a huge mess. It fundamentally violates the
basic OO principle of encapsulating state and behavior. Anything else --
reifying the events into first-class types, factoring out the overlap into a
new type, generics, even Scala-style traits -- is better.

~~~
tom_mellior
> the basic OO principle of encapsulating state and behavior

Not everything you might want to program necessarily fits the principles of
your religion.

An abstract syntax tree in particular isn't something where information hiding
makes a lot of sense. You _need_ the details in the tree.

------
swort
Or, for an example of actual-zero-overhead tree processing using the visitor
pattern:

[https://medium.com/@swortelbernux/libraryless-reflection-
in-...](https://medium.com/@swortelbernux/libraryless-reflection-
in-c-288d7873e3a6)

------
ris
_Yeah_ , there _is_ a neatness to it. I manage to achieve a similar thing with
recursive generators in python quite often, which ends up being quite concise.

------
fnordsensei
For processing JSON in Clojure, I've used zippers, modifying the tree or leaf
nodes by protocol dispatch. More because it seems how the task is "supposed"
to be done in Clojure rather than based on any particular understanding of
whether there are alternative methods out there, and if so, which would be the
better method.

I found this blog post discussing the differences between visitors and
zippers:
[http://www.ibm.com/developerworks/library/j-treevisit/index....](http://www.ibm.com/developerworks/library/j-treevisit/index.html)

(Written in 2011. In the article, multimethods are used rather than protocols)

------
bakhy
I think in C# nowadays I'd implement Select and SelectMany for the data type I
want to visit, and then use LINQ. This is one pattern which is easier in a
functional approach. I presume it would be easy to do something similar in
Scala?

------
nebulous1
He should have led with the reasons for using it, and his definition of zero-
overhead.

Composability without intermediate data structures seems to be the major use
case.

------
mattbierner
From a functional perspective, aren’t visitors just a case of ‘map’ with
potential side effects? That handles composition, streaming, and so on

~~~
KirinDave
Yes, but from a functional perspective you need to do extra work to make
generalized recursive applications possible. If you're interested, the name is
"Recursion Schemes". Avoid the paper, its notation is too challenging. This
blog has a long running series which is much better:
[http://blog.sumtypeofway.com/an-introduction-to-recursion-
sc...](http://blog.sumtypeofway.com/an-introduction-to-recursion-schemes/)

------
kizer
Looks like the pattern effectively linearizes the hierarchical structure. So
you can stream or iterate over the ouput of a visitor run, as the author said.

Could one just implement an iterator on top of a tree structure? This would be
similar but be imperative right rather than declarative as you would not
provide a “callback”, but pull out nodes directly.

~~~
mark_edward
This is how all of Rust's iterators and iteration protocol work. Including
standard library and other tree structures, graphs, etc.

------
drfrank
This is the article that I share with my coworkers when I see them reaching
for the Visitor Pattern: [https://lostechies.com/derekgreer/2010/04/19/double-
dispatch...](https://lostechies.com/derekgreer/2010/04/19/double-dispatch-is-
a-code-smell/)

~~~
KirinDave
Why is this article even remotely applicable here, Dr. Frank? Not only does
this technique factor away the recursive part of the algorithm from the
transformative, but it improves performance at the same time. Any time you can
abstract to a simpler requirement (that is more testable) _and_ gain
performance, this is like finding a silver dollar at the beach. You should
take it and be thankful.

I think an awful lot of OO programmers see "visitor pattern" and immediately
their body clenches because they've been given warnings over and over about
how "multiple dispatch is dangerous and confusing." And when it's used in
specialized code instead of generic code, that's true. But when it's used to
separate business logic from algorithmic implementation, it's nearly always a
net win.

------
OJFord
It's advantageous for unit testing (smaller units) and debugging too, IMO.

------
nbevans
A nostalgic article into coding practices of 15 years ago.

~~~
KirinDave
This technique is still relevant today and even cutting edge papers often use
a more general variant of this technique to get work done.

But you're right, once upon a time, training and appropriate architectural
decisions were a lot more valued by the OO community.

------
jacobtracey
Free dispatch

------
rs86
this is completely trivial in functional programming.

~~~
phyzome
Fellow (?) functional programmer here. I would generally use a recursive
descent algorithm on a tree. That's the natural approach, yes? But the entire
point of the article was that if you use a streaming approach you can get some
performance benefits.

I would say it's _not_ totally trivial in most languages, and that it's
slightly more extra work in functional languages.

------
djsumdog
Our current principal engineer has implemented the visitor pattern for a bunch
of things in our current project. It's a quite ugly implementation we shot
down in code review and then we had an entire meeting where he defended it and
eventually it got let in.

He's used the visitor pattern in a few other areas as well. I'm not a big fan.

~~~
KirinDave
This technique improves readability, improves abstraction, improves
testability, and improves performance.

Please don't confuse your superstitions or local culture's distaste for
properly researched architecture for a more general sense of code quality.
Simply saying, "I saw the word visitor and got mad," not only is disrespectful
to the author, but it's disrespectful to your entire profession.

~~~
phyzome
Visitor pattern definitely does not automatically improve readability or
clarity. (In the general case! Summing values would be an exception.) In fact,
I'd see it as a performance/readability tradeoff in any language where
recursive tree processing is ergonomic.

Not even knowing the language involved, there's really not much to say.

~~~
KirinDave
I'm not referring to the Visitor pattern in Abstract, I'm referring to the
specific solution the author of the article we are discussing employed.

There is no such thing as a pattern that always increases clarity. That's not
what patterns are for, so that's unsurprising. It's the case though that this
specific application has pretty much only benefits.

