gogenericsvisitor-pattern

Attempting to Implement the Visitor Pattern in Go using Generics


I have the following simple generics based go package that implements the GoF Visitor pattern:

package patterns

type Social interface {
    AcceptVisitor(visitor *Visitor)
}

type Component struct {
}

func (c *Component) AcceptVisitor(visitor *Visitor) {
    visitor.VisitComponent(c)
}

type Collection[T Social] struct {
    Component
    items[]T
}

func (c *Collection[T]) AcceptVisitor(visitor *Visitor) {
    visitor.VisitCollection(c) // <- Error Here
}

type Visitor struct {
}

func (v *Visitor) VisitComponent(component *Component) {
}

func (v *Visitor) VisitCollection(collection *Collection[Social]) {
    for _, item := range collection.items {
        item.AcceptVisitor(v)
    }
}

The compiler gives the following error:

./patterns.go:20:26: cannot use c (variable of type *Collection[T]) as
  type *Collection[Social] in argument to visitor.VisitCollection

This seems strange to me since the generic type T is constrained as Social.

I tried a couple things:

It seems like go should be able to handle the generics in this code.

POSSIBLE SOLUTION: After a really helpful discussion with @blackgreen we decided that the problem shows up due to a few things:

  1. Go, being (truly) strictly typed, does not allow an argument that is being passed into a function to be "narrowed" to a subset of the original type even though the compiler could still prove it to be safe. Whether or not Go should allow narrowing is up for debate.
  2. Go does not allow generic constraints on a method since the constraints might conflict with generic constraints on the structure associated with the method.
  3. Go, rightly so, does not allow circular dependencies. We could abstract all the dependencies for the Visitor pattern into interfaces but would then have the circular dependencies required by the "double dispatch" aspect of the pattern.

To work-around these items, and still get the benefits of the Visitor pattern, we can change the VisitXYZ() methods in the Visitor structure to be (potentially generic) functions that each take a *Visitor argument as the first parameter of the function and the object being visited as the second parameter.

I posted this solution in the Go Playground: https://go.dev/play/p/vV7v61teFbj

NOTE: Even though this possible solution does appear to solve the problem, it really doesn't. If you think about writing several different types of Visitors (one for pretty printing, one for copying, one for sorting, etc.) you quickly realize that since the VisitXYZ() functions are not methods, you cannot have multiple versions of each function for each Visitor type. In the end, the fact that the Visitor pattern really does require a circular dependency between the Social interface and the Visitor interface dooms it for Go. I am closing this post but will leave the analysis so that others won't need to repeat it.


Solution

  • I came to the conclusion that generics make this pattern worse. By parametrizing the Collection struct, you force items []T to have the same elements. With plain interfaces instead you can have dynamic dispatch, hence allow items to contain different implementations. This alone should be sufficient reason.

    This is a minimal implementation without generics, adapted from some Java examples (runnable code):

    Main interfaces:

    type Visitor func(Element)
    
    type Element interface {
        Accept(Visitor)
    }
    

    An implementor:

    type Foo struct{}
    
    func (f Foo) Accept(visitor Visitor) {
        visitor(f)
    }
    

    The container:

    type List struct {
        elements []Element
    }
    
    func (l *List) Accept(visitor Visitor) {
        for _, e := range l.elements {
            e.Accept(visitor)
        }
        visitor(l)
    }
    

    The visitor itself, as a regular function. And here you can define any function at all freely. Being untyped, you can pass it directly as a Visitor argument:

    func doVisitor(v Element) {
        switch v.(type) {
        case *List:
            fmt.Println("visiting list")
        case Foo:
            fmt.Println("visiting foo")
        case Bar:
            fmt.Println("visiting bar")
        }
    }