munificent / craftinginterpreters

Repository for the book "Crafting Interpreters"
http://www.craftinginterpreters.com/
Other
8.99k stars 1.05k forks source link

Visitor Pattern vs. Overloaded Methods #930

Closed hadjian closed 3 years ago

hadjian commented 3 years ago

Hi @munificent! Thanks for the great book!

I still didn't get the usefulness of the visitor pattern in the chapter "Representing Code". If I understand correctly, the text justifies the usage of this pattern, because it makes adding types and operations on types equally easy. You argue that without the visitor pattern

But adding a new type/class to the visitor pattern means adding a new method to all visitors, doesn't it? So it is like the second case, where a new case was added to all functions.

edit: I understand that a direct dispatched call would be a benefit over a dispatch at runtime, so there is an advantage over a long switch/case. My question below however still remains.

Also why wouldn't one just dispatch on overloaded static methods, like so:

class AstPrinter {
  public static String print(Expr.Binary bin) {
    ...
  }

  public static String print(Expr.Unary un) {
    ...
  }
}

Usage would be like

Expr bin = New Expr.Binary(...);
Expr un = New Expr.Unary(...);
AstPrinter.print(bin);
AstPrinter.print(un);

I tried to answer this myself, so I read up on Wikipedia. The article there says something about "double dispatch", which sounds to me like the visitor pattern is useful in cases where the calling code needs to dispatch on type and operation. So e.g. if I have a list of different operations and a list of types and put both lists into a method, which will apply all the operations to all types. It would just call:

currentType.accept(currentVisitor);

I am sure I will be embarrassed for asking this question, once I understand ;-)

mcfriend99 commented 3 years ago

Well, Github isn't SO and no one... I repeat... no one... embarrass anyone for opening issues. It may be easy to understand for one person, but might be the same question another person wants to ask. It's the core of contributing and the open in the source!

munificent commented 3 years ago

No need to feel embarrassed. This particular issue confuses many even very experienced programmers.

Also why wouldn't one just dispatch on overloaded static methods, like so:

class AstPrinter {
  public static String print(Expr.Binary bin) {
    ...
  }

  public static String print(Expr.Unary un) {
    ...
  }
}

Usage would be like

Expr bin = New Expr.Binary(...);
Expr un = New Expr.Unary(...);
AstPrinter.print(bin);
AstPrinter.print(un);

It's worth trying that out for yourself and seeing what happens. I can explain it and spoil the surprise for you, but it might be a better learning experience to see it first-hand. :)

hadjian commented 3 years ago

@munificent Thanks for not spoiling it. After my crunch at work, I could come back to this dangling pointer in my head.

Answer: My suggestion of overloaded methods would not work, because we want to call our operations polymorphically. So it would not be: AstPrinter.print(binary) but AstPrinter.print(expression) So it could not dispatch on the argument as well.

Also I understood a paragraph of the book wrong:

But, conversely, adding a new type is hard. You have to go back and add a new case to all of the pattern matches in all of the existing functions.

This doesn't mean, we want to avoid adding a function for each type (we have to, as the function does different things for different types), we want to avoid adding another dispatch to a long switch/case. With the visitor pattern we get rid of the glue code, which is now generated for us.

A follow-up question: I do the implementation in golang and there you don't have to keep the methods with the structs. Without dispatch magic, I can have a astprinter.go and define the print() methods for all structs at one place like so:

package ast

func (b Binary) print() string {
\\ code
}

func (l Literal) print() string {
\\ code
}

\\ etc.

Is there anything wrong with this approach? Let me guess: "Try and see" :-)

munificent commented 3 years ago

Is there anything wrong with this approach?

No, there's nothing wrong with it. :) Go uses structural typing, which is a pretty interesting approach.