Zero-Overhead Tree Processing with the Visitor Pattern
lihaoyi.com
lihaoyi.com
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" :)
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
P.S. Thanks for the real perfomance data!
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.
As it turns out GHC is usually pretty good at optimizing this kind of thing due to the extremely aggressive inlining which is possible when you know that there are no side effects. It'll often handle compositions of traversals with no sweat. (I think it's sort of all lumped in as "fusion".)
EDIT: See tel's comment in this thread. He always understands and explains it much better than I can.
Here is Appel's Java-based compiler book: https://eden.dei.uc.pt/~amilcar/pdf/CompilerInJava.pdf. It's full of visitors. If the same book's ML version uses pattern matching instead, then that says a lot about the different implementation languages and not much about what you wanted to claim.
"Comment by Vladimir N. Makarov: Another good book to start to study compilers from parser to code generation and basic optimizations. I especially like the version in ML (Modern compiler implementation in ML).
Comment by Steven Bosscher: The version in ML is the best of the three. The other two look too much like "had to do this"-books where algorithms are translated from ML, which makes them look very unnatural in C/Java." [1]
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)) pairsIs that the answer to "how do recursion schemes handle heterogenous layers"? I'd love to learn more.
http://homepages.inf.ed.ac.uk/wadler/papers/deforest/defores...
So that seems like quite a stretch.
[0] https://mechanical-sympathy.blogspot.com/2012/04/invoke-inte...
>This is exactly what the JVM does. See [0].
The jump table from Martin's blog is the virtual call one.
On top of that JVM has an inline budget and just stops at a point. So the switch stuff ain't happening for megamorph.
[0]:https://blog.h2o.ai/2010/04/inline-caches-and-call-site-opti...
It's a trade off and the article shows no proof the of an improved performance by reducing the 'overhead'. For example object allocation in JVM is really cheap (pointer bump in the TLAB + check&perfectly predicted jump), reusing objects in a cycling list/buffer is similar, etc. In the end the post is about performance and I pointed that the multiple mentions of 'zero overhead' are incorrect.
When I talk about how the OO community has anti-intellectual toxicity can try to buzzword bingo architecture away, Visitor is my go to example. Because that's sure what it seems like to me.
You will 100% end up with pipeline stalls with a sufficiently large, random input anyways. This complaint about vtables is not substantial in this case, and I welcome you to prove they do.
The fact that you can fuse multiple passes into one alteration (or reconstruction) of the tree is what's a removal of overhead.
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.
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).
Inevitably, any method (visitor, pattern matching, if elsif) of customizable walking of a tree datastructure separates behavior from the tree object. That's the point, though, again.
Now, you can say wanting to do that (customizable tree walking) in itself is an antipattern and shouldn't be done. That just doesn't match well with reality.
I've seen many cases where the visitor pattern is about the best one can do. Yes, it becomes ugly but that's usually due to the complexity of the attempted tree walking, e.g. for transformation.
A lot of the alternative approaches in the functional world become ugly too once there's a need to offer maximum customizability, e.g. in terms of order of evaluation.
If you are able to sufficiently constrain the domain you can get away with some simplied templates, but where I see visitors most often is in API's wanting to offer any kind of tree walking.
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.
The kind of principle that most realise is a mistake of enormous proportions.
I was surprised how similar it looks in Java. I hadn't considered how it would look.
I'm not convinced that folks here saying, "We just us pattern matching like functional programming does" actually know how modern functional programming does this. From the perspective of runtime overhead and code size, this approach is very similar to recursion-schemes.
Where RS excels over this approach is not in the use of pattern matching, but in the fact that it can reuse the functor structure to not even have to write the descending algorithm; it comes for free as part of your fixpoint functor.
https://medium.com/@swortelbernux/libraryless-reflection-in-...
I found this blog post discussing the differences between visitors and zippers: http://www.ibm.com/developerworks/library/j-treevisit/index....
(Written in 2011. In the article, multimethods are used rather than protocols)
Composability without intermediate data structures seems to be the major use case.
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.
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.
But you're right, once upon a time, training and appropriate architectural decisions were a lot more valued by the OO community.
I would say it's not totally trivial in most languages, and that it's slightly more extra work in functional languages.
He's used the visitor pattern in a few other areas as well. I'm not a big fan.
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.
Calling it overengineered is pretty ridiculous IMO. "Over-engineered" doesn't mean, "Because software training is often inadequate someone might encounter this for the first time in industry rather than in school."
Not even knowing the language involved, there's really not much to say.
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.