尝试使用泛型在Go语言中实现访问者模式。

huangapple go评论85阅读模式
英文:

Attempting to Implement the Visitor Pattern in Go using Generics

问题

我有一个基于泛型的简单go包,实现了GoF访问者模式:

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)
	}
}

编译器给出了以下错误:

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

这对我来说很奇怪,因为泛型类型T被限制为Social。

我尝试了几个方法:

  • 用接口定义替换了Visitor抽象类型。这导致Social和Visitor接口之间出现循环依赖。
  • 从声明中删除了泛型,这解决了问题,但我们几乎需要泛型来定义Collection类型。

看起来go应该能够处理这段代码中的泛型。

可能的解决方案:
在与@blackgreen进行了非常有帮助的讨论后,我们发现问题出现在以下几个方面:

  1. Go是(真正)严格类型的,不允许将传递给函数的参数“缩小”为原始类型的子集,即使编译器仍然可以证明它是安全的。关于Go是否应该允许缩小的问题还有待讨论。
  2. Go不允许在方法上使用泛型约束,因为这些约束可能与与方法关联的结构的泛型约束冲突。
  3. Go不允许循环依赖,这是正确的。我们可以将访问者模式的所有依赖项抽象为接口,但这将导致模式的“双重分派”方面所需的循环依赖。

为了解决这些问题,并仍然获得访问者模式的好处,我们可以将Visitor结构中的VisitXYZ()方法更改为(可能是泛型的)函数,每个函数将*Visitor作为函数的第一个参数,被访问的对象作为第二个参数。

我在Go Playground上发布了这个解决方案:https://go.dev/play/p/vV7v61teFbj

注意:尽管这个可能的解决方案似乎解决了问题,但实际上并没有。如果你考虑编写几种不同类型的访问者(一个用于漂亮打印,一个用于复制,一个用于排序等),你很快就会意识到,由于VisitXYZ()函数不是方法,你无法为每种访问者类型的每个函数创建多个版本。最终,访问者模式确实需要Social接口和Visitor接口之间的循环依赖,这对于Go来说是不可行的。我将关闭这篇文章,但会保留分析结果,以免其他人需要重复进行分析。

英文:

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:

  • Replaced the Visitor abstract type with an interface definition. This
    resulted in circular dependencies between the Social and Visitor
    interfaces.
  • Removed the generics from the declarations which fixes the problem
    but we pretty much need generics for the Collection type.

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.

答案1

得分: 1

我得出结论,泛型使这种模式变得更糟。通过将Collection结构参数化,您强制items []T具有相同的元素。而使用普通接口,您可以进行动态调度,从而允许items包含不同的实现。这一点本身就足够理由。

这是一个没有泛型的最小实现,改编自一些Java示例(可运行代码):

主要接口:

type Visitor func(Element)

type Element interface {
	Accept(Visitor)
}

一个实现者:

type Foo struct{}

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

容器:

type List struct {
	elements []Element
}

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

访问者本身,作为一个常规函数。在这里,您可以自由定义任何函数。由于是无类型的,您可以直接将其作为Visitor参数传递:

func doVisitor(v Element) {
	switch v.(type) {
	case *List:
		fmt.Println("访问列表")
	case Foo:
		fmt.Println("访问foo")
	case Bar:
		fmt.Println("访问bar")
	}
}
英文:

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")
	}
}

huangapple
  • 本文由 发表于 2022年6月13日 04:36:11
  • 转载请务必保留本文链接:https://go.coder-hub.com/72595700.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定