What is the idiomatic way in Go to create a complex hierarchy of structs?

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

What is the idiomatic way in Go to create a complex hierarchy of structs?

问题

我正在使用Go编写一个解释器,并且正在寻找存储AST的惯用方法。我阅读了Go编译器的源代码,似乎他们使用了带有空方法的接口来表示AST。例如,我们有以下层次结构:

Object
--Immovable
----Building
----Mountain
--Movable
----Car
----Bike

以下是上述层次结构以"空方法"方式实现的代码:

type Object interface {
  object()
}

type Immovable interface {
  Object
  immovable()
}

type Building struct {
  ... 
}

type Mountain struct {
  ... 
}

type Movable interface {
  Object
  movable()
}

type Car struct {
  ...
} 

type Mountain struct {
  ...
} 

func (*Building) object() {}
func (*Mountain) object() {}
func (*Car) object() {}
func (*Bike) object() {}
func (*Building) immovable() {}
func (*Mountain) immovable() {}
func (*Car) movable() {}
func (*Bike) movable() {}    

上述代码是一个人为的示例,这就是Go编译器使用数十个空方法来实现AST的方式。但是为什么要这样做呢?请注意定义了多少个空方法。随着层次结构深度的增加,这可能会变得非常复杂。

注释中指出,空方法可以防止不兼容类型的赋值。在我们的示例中,*Car不能被赋值给*Immovable

这在其他支持继承的语言(如C++)中非常简单。我无法想到其他表示AST的方式。

Go编译器实现AST的方式可能是惯用的,但是否不够直观呢?

英文:

I am writing an interpreter in Go and I am looking for the idiomatic way to store the AST. I read the Go compiler source code and it seems they used interfaces with an empty method to represent the AST. For example, we have the following hierarchy,

Object
--Immovable
----Building
----Mountain
--Movable
----Car
----Bike

This is how the above hierarchy is implemented in the "empty method" way.

type Object interface {
  object()
}

type Immovable interface {
  Object
  immovable()
}

type Building struct {
  ... 
}

type Mountain struct {
  ... 
}

type Movable interface {
  Object
  movable()
}

type Car struct {
  ...
} 

type Mountain struct {
  ...
} 

func (*Building) object() {}
func (*Mountain) object() {}
func (*Car) object() {}
func (*Bike) object() {}
func (*Building) immovable() {}
func (*Mountain) immovable() {}
func (*Car) movable() {}
func (*Bike) movable() {}    

The above code is a contrived example and this is how the Go compiler implemented the AST with dozens of empty methods. But WHY? Note how many empty methods are defined. It may get very complicated with the increase of the depth of the hierarchy.

It is stated in the comments that the empty methods disallow the assignment of incompatible types. In our example, a *Car can't be assigned to a *Immovable for instance.

This is so easy in other languages like C++ that supports inheritance. I can't think of any other way of representing the AST.

The way how the Go compiler AST is implemented may be idiomatic but isn't it less straight forward?

答案1

得分: 10

Go语言并不是完全面向对象的语言:它没有类,也没有类型继承;但是它支持一种类似的构造,称为“嵌入”,可以在struct级别和interface级别上使用,并且它确实有方法。

Go中的接口只是固定的方法集合。如果一个类型的方法集是接口的超集(没有明确声明意图),则该类型会_隐式_实现该接口。

如果你想要在类型层次结构中区分(例如,你不希望一个对象既能是Movable又能是Immovable),它们必须具有不同的方法集(每个MovableImmovable的方法集中必须至少有一个方法不在另一个方法集中),因为如果方法集包含相同的方法,对其中一个的实现将自动实现另一个,因此你可以将一个Movable对象赋给类型为Immovable的变量。

在接口中添加一个同名的空方法将为你提供这种区分,假设你不会将这样的方法添加到其他类型中。

减少空方法的数量

个人而言,我对空方法没有任何问题。不过,有一种方法可以减少它们。

如果你为层次结构中的每个类型创建一个struct实现,并且每个实现都嵌入了上一级的struct实现,那么上一级的方法集将自动出现,无需其他操作:

Object

Object接口和ObjectImpl实现:

type Object interface {
  object()
}
type ObjectImpl struct {}
func (o *ObjectImpl) object() {}

Immovable

Immovable接口和ImmovableImpl实现:

type Immovable interface {
    Object
    immovable()
}
type ImmovableImpl struct {
    ObjectImpl // 嵌入ObjectImpl
}
func (o *Immovable) immovable() {}

注意ImmovableImpl只添加了immovable()方法,object()方法是“继承”的。

Building

Building实现:

type Building struct {
    ImmovableImpl // 嵌入ImmovableImpl结构体

    // 可能还有特定于Building的其他字段
}

注意Building没有添加任何新方法,但它自动成为一个Immovable对象。

如果“子类型”的数量增加或接口类型不仅有一个“标记”方法(因为所有方法都是“继承”的),这种技巧的优势将大大增加。

英文:

Go is not (quite) an object oriented language: it does not have classes and it does not have type inheritance; but it supports a similar construct called embedding both on struct level and on interface level, and it does have methods.

Interfaces in Go are just fixed method sets. A type implicitly implements an interface if its method set is a superset of the interface (there is no declaration of the intent).

Empty methods are great if you want to document or state explicitly that your type does implement an interface (because it is not stated explicitly). Official Go FAQ: How can I guarantee my type satisfies an interface?

type Fooer interface {
    Foo()
    ImplementsFooer()
}

If you want a distinction in your type hierarchy (e.g. you don't want to allow an object to be both Movable and Immovable), they must have different method sets (there must be at least 1 method in each of the method sets of Movable and Immovable that is not present in the other's), because if the method sets would contain the same methods, an implementation of one would automatically implement the other too therefore you could assign a Movable object to a variable of type Immovable.

Adding an empty method to the interface with the same name will provide you this distinction, assuming that you will not add such methods to other types.

Reducing the number of empty methods

Personally I have no problem with empty methods whatsoever. There is a way to reduce them though.

If you also create a struct implementation for each type in the hierarchy and each implementation embeds the struct implementation one level higher, the method set of one level higher will automatically come without further ado:

Object

Object interface and ObjectImpl implementation:

type Object interface {
  object()
}
type ObjectImpl struct {}
func (o *ObjectImpl) object() {}

Immovable

Immovable interface and ImmovableImpl implementation:

type Immovable interface {
    Object
    immovable()
}
type ImmovableImpl struct {
    ObjectImpl // Embed ObjectImpl
}
func (o *Immovable) immovable() {}

Note ImmovableImpl only adds immovable() method, object() is "inherited".

Building

Building implementation:

type Building struct {
    ImmovableImpl // Embed ImmovableImpl struct

    // Building-specific other fields may come here
}

Note Building does not add any new methods yet it is automatically an Immovable object.

The advantage of this technic grows greatly if the number of "subtypes" increases or if the interface types have more than just 1 "marker" method (because all methods are "inherited").

huangapple
  • 本文由 发表于 2015年3月19日 20:29:21
  • 转载请务必保留本文链接:https://go.coder-hub.com/29144622.html
匿名

发表评论

匿名网友

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

确定