Merging visit methods

Hello,

I have a question regarding the expressions project from the tutorials. I am referring to the advanced one:

which features a visitor.

Why exactly are there as many visit methods in this class? Obviously the Visitor interface demands them, but let’s say it didn’t. If I removed all visit methods and replaced them with methods:

String visit(BinaryExpression)
String visit(UnaryExpression)

which cover only the cases we care about for this visitor, then what would happen?

To find out I didn’t change the tutor code but quickly made my own example:

// Acceptor.java
public class Acceptor {

    public void accept(BabyVisitor v) {
        System.out.println(v.visit(this));
    }

    public void accept(Visitor v) {
        System.out.println(v.visit(this));
    }
}

// A.java
public class A extends Acceptor {

    @Override
    public void accept(Visitor v) {
        System.out.println(v.visit(this));
    }
}

// B.java
public class B extends Acceptor {

    @Override
    public void accept(Visitor v) {
        System.out.println(v.visit(this));
    }
}

// Visitor.java
public class Visitor {

    public String visit(Acceptor a) {
        return "Acceptor visited by Visitor";
    }

    public String visit(A a) {
        return "A visited by Visitor";
    }

    public String visit(B a) {
        return "B visited by Visitor";
    }
}

// BabyVisitor.java
public class BabyVisitor extends Visitor {

    @Override
    public String visit(Acceptor a) {
        return "Acceptor visited by BabyVisitor";
    }
}

If I now execute this code:

Acceptor acc = new Acceptor();
A a = new A();
B b = new B();

Visitor v = new Visitor();
BabyVisitor baby = new BabyVisitor();

acc.accept(v);
a.accept(v);
b.accept(v);

acc.accept(baby);
a.accept(baby);
b.accept(baby);

I get this output:

Acceptor visited by Visitor
A visited by Visitor
B visited by Visitor
Acceptor visited by BabyVisitor
Acceptor visited by BabyVisitor
Acceptor visited by BabyVisitor

If I initialize baby as a Visitor I get:

Acceptor visited by BabyVisitor
A visited by Visitor
B visited by Visitor

So if I make a mistake and declare my visitors poorly my example will break and I will get unintended behavior. (but as far as I can tell this is consistent with method resolution as discussed in the tutorials)

The selection of methods is coarser for BabyVisitor, i.e. where Visitor sees:

      Acceptor
     /        \
    A          B

the BabyVisitor only sees Acceptor.

In the jargon of pattern matching for switch as presented in the kNobel Tutorial this would be handling multiple cases at once. So if I had:

               A
             /   \
            B     C
                /   \
               D     E

where A is an interface, I could do one of the following:

switch (a) {
    case B b -> 0;
    case D d -> 1;
    case E e -> 1;
    case C c -> 1;
}

which corresponds to visit methods for each implementation or I could merge behaviors:

switch (a) {
    case B b -> 0;
    case C c -> 1;

which corresponds to my “simplification” above.

Is there anything wrong with my approach? (I already acknowledged above that I have to take care of giving my visitors the correct static type)

I don’t know. What I would do is have one visitor, and have another sub-interface of that interface where some methods are already implemented by delegating to a more common handler.

interface Visitor<T> {

T visit(Acceptor a);
T visit(A a);
T visit(B b);
}

interface BabyVisitor<T> implements Visitor<T>{
 default T visit(A a) {return visitAcceptor(a);}
 default T visit(B a) {return visitAcceptor(a);}
}

Whether you really want to use visitors is another matter. I would softly recommend against this, but that might just be my personal preference.

Hope this helps,
Johannes

1 Like