Structural typing and polymorphism in Go – Writing a method that can operate on two types having the same fields

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

Structural typing and polymorphism in Go - Writing a method that can operate on two types having the same fields

问题

我在玩过Scala和OCaml等其他语言的结构类型后开始研究Go,并且我正在尝试在这些语言之间映射一些惯用的技术。考虑以下类型:

type CoordinatePoint struct {
    x int
    y int
    // 其他不相关的方法和字段
}

type CartesianPoint struct {
    x int
    y int
    // 其他不相关的方法和字段
}

假设我们想编写一个方法,该方法可以操作这两种类型来计算它们的极坐标表示,func ConvertXYToPolar(point XYPoint) PolarPoint。如果CartesianPointCoordinatePoint类型为xy字段定义了getter和setter方法,我们可以将XYPoint定义为具有这些方法的公共接口,从而可以操作这两种类型。但是,目前接口不能声明字段,只能声明方法。

基于此,我有几个问题:

  1. 在Go中,处理这种情况的惯用方法是什么?
  2. 是否可以在不修改现有类型的情况下完成?
  3. 我们是否可以保留类型安全性,即避免使用空接口类型作为参数并手动转换来定义ConvertXYToPolar
  4. 如果接口和隐式接口满足是Go中多态的主要工具,那么禁止在接口定义中使用字段是否有限制?
  5. 在结构体上是否常常定义getter/setter方法以规避这个限制?
  6. 在设计决策中,不支持在接口定义中支持字段背后是否有令人信服的原因?

我发现嵌入类型、隐式接口满足和基于接口的多态性的简单性非常简单且吸引人,可以促进代码的可重用性和可维护性,但是禁止在接口定义中使用字段使得Go的结构化类型能力在我看来有些有限。我是否错过了一个简单的解决方案?

英文:

I started looking into Go after playing around with structural typing in other languages like Scala and OCaml, and I'm trying map some of the idiomatic techniques between the languages. Consider the following types

type CoordinatePoint struct {
    x int
    y int
    // Other methods and fields that aren't relevant
}

type CartesianPoint struct {
    x int
    y int
    // Other methods and fields that aren't relevant
}

Let's say we'd like to write a method which operates on both of these types to compute their polar coordinate representations, func ConvertXYToPolar(point XYPoint) PolarPoint. If the CartesianPoint and CoordinatePoint types defined getter and setter methods for the x and y fields we could define XYPoint as a common interface with those methods, allowing us to operate on both types, but as it stands, interfaces cannot declare fields, only methods.

Based on this, I have a few questions:

  1. What is the idiomatic way of handling this in Go?
  2. Can it be done without modifying the existing types?
  3. Can we retain type safety, i.e. avoid defining ConvertXYToPolar without using the empty interface type as the parameter and manually converting?
  4. If interfaces and implicit interface satisfaction are the primary tools for polymorphism in Go, is the forbiddence of fields in interface definitions limiting?
  5. Are getter/setter methods commonly defined on structs to circumvent this limitation?
  6. Is there a compelling reason behind the design decision not to support fields in interface definitions?

I find the simplicity of embedded types, implicit interface satisfaction, and interface-based polymorphism to be a very simple and appealing combination of techniques to promote code reusability and maintainability, but forbidding fields in interface definitions makes Go's structural typing capabilities somewhat limited from my perspective. Am I missing a simple solution?

答案1

得分: 14

通常的方法是使用组合:

type Point struct {
    x int
    y int
}

type CoordinatePoint struct {
    Point
    其他的东西
}

type CartesianPoint struct {
    Point
    其他不相关的方法和字段
}

Go语法使得这种组合在其他语言中大部分感觉像继承。例如,你可以这样做:

cp := CoordinatePoint{} 
cp.x = 3
log.Println(cp.x)

并且你可以使用带有Point参数的函数调用

doAThingWithAPoint(cp.Point)

为了让你的点可以互换地传递,你需要定义一个接口

type Pointer interface {
    GetPoint() *Point
}
func (cp CoordinatePoint) GetPoint() *Point {
    return &cp.Point
}

然后你就可以定义接受Pointer的函数:

func doSomethingWith(p Pointer) {
    log.Println(p.GetPoint())
}

另一种解决方案是基于一个定义了GetXSetXGetYSetY的接口,但我个人认为这种方法比必要的更重和更冗长。

英文:

The usual way is to use composition :

type Point struct {
    x int
    y int
}

type CoordinatePoint struct {
    Point
    other stuff
}

type CartesianPoint struct {
    Point
    Other methods and fields that aren't relevant
}

Go syntax makes this composition mostly feel like inheritance in other languages. You can for example do this :

cp := CoordinatePoint{} 
cp.x = 3
log.Println(cp.x)

And you can call functions taking a Point as parameter with

doAThingWithAPoint(cp.Point)

To let your points be passed interchangeably, you would have to define an interface

type Pointer interface {
    GetPoint() *Point
}
func (cp CoordinatePoint) GetPoint() *Point {
    return &cp.Point
}

Then you would be able to define functions taking a Pointer :

func doSomethingWith(p Pointer) {
    log.Println(p.GetPoint())
}

Another solution would be based on an interface defining GetX, SetX, GetY and SetY but I personally find this approach much heavier and more verbose than necessary.

答案2

得分: 2

我的第一个草稿看起来是这样的,

package points

type XYPoint struct {
    X, Y int64
}

type CoordinatePoint struct {
    XYPoint
}

type CartesianPoint struct {
    XYPoint
}

type PolarPoint struct {
    R, T float64
}

type XYToPolarConverter interface {
    ConvertXYToPolar(point XYPoint) PolarPoint
}

func (cp *CoordinatePoint) ConvertXYToPolar(point XYPoint) PolarPoint {
    pp := PolarPoint{}
    // ...
    return pp
}

func (cp *CartesianPoint) ConvertXYToPolar(point XYPoint) PolarPoint {
    pp := PolarPoint{}
    // ...
    return pp
}
英文:

My first draft would look like this,

package points

type XYPoint struct {
    X, Y int64
}

type CoordinatePoint struct {
    XYPoint
}

type CartesianPoint struct {
    XYPoint
}

type PolarPoint struct {
    R, T float64
}

type XYToPolarConverter interface {
    ConvertXYToPolar(point XYPoint) PolarPoint
}

func (cp *CoordinatePoint) ConvertXYToPolar(point XYPoint) PolarPoint {
    pp := PolarPoint{}
    // ...
    return pp
}

func (cp *CartesianPoint) ConvertXYToPolar(point XYPoint) PolarPoint {
    pp := PolarPoint{}
    // ...
    return pp
}

答案3

得分: 1

  1. 通常,惯用的方法是使用getter和setter。不太方便?也许是。但至少目前是这样做的。
  2. 是的。这就是鸭子类型的本质。任何匹配接口的类型都会被接受,无需显式实现。*编辑:根据对这个答案的评论,我误解了这个问题。答案是否定的,你需要添加方法来使这些结构体匹配除了interface{}之外的接口。
  3. 是的,使用getter和setter。
  4. 可能。我可以理解为什么getter和setter可能被认为不太方便。但据我所知,它们并不严格限制你能做什么。
  5. 是的。这是我在别人的代码和标准库中看到的方式。
英文:
  1. Typically, the idiomatic way is to use getters and setters. Less convenient? Perhaps. But that's the way it's done for now, at least.
  2. Yes. That's the essence of duck typing. Any type matching the interface will be accepted, without the need to explicitly implement. EDIT: Per the comments on this answer, I misinterpreted this question. The answer is no, you will need to add methods for these structs to match an interface other than interface{}.
  3. Yes, using getters and setters.
  4. Maybe. I can see why getters and setters might be perceived as less convenient. But they do not strictly limit what you can do, so far as I can tell.
  5. Yes. This is the way I've seen it done in others' code and in the standard libraries.

答案4

得分: 0

我不确定在你提问的时候是否可能,但这是我会做的。这种方法即使你不拥有结构体也可以工作,而其他解决方案则不行。

// 在包1中
type Coordinate struct {
	X, Y float64
}

// 在包2中
type Cartesian struct {
	X, Y float64
}

// 回到main.go

type Polar struct {
	r, θ float64
}

type Point = struct{ X, Y float64 } // 重用的类型别名

func XYToPolar(p Point) Polar {
	// 对于任何具有相同底层结构的结构体都适用
	return Polar{
		r: math.Hypot(p.X, p.Y),
		θ: math.Atan(p.Y/p.X) * 180 / math.Pi,
	}
}

func main() {
	// 编译成功
	fmt.Println(
		XYToPolar(pack1.Coordinate{X: 5.0, Y: 12.0}),
		XYToPolar(pack2.Cartesian{X: 12.0, Y: 5.0}),
	)
}

Go对结构化类型的支持还不完善。根据Go 1.18发布说明

即使类型参数的类型集中的所有类型都有字段f,Go编译器也不支持访问类型参数类型的结构字段x.f。我们可能会在未来的版本中删除此限制。

一旦这个特性发布,将会有更好和更通用的方法来实现这一点。

英文:

I'm not sure if this was possible when you asked the question but this is what I would do. This works even when you don't own the structs unlike the other solutions.

// in package 1
type Coordinate struct {
	X, Y float64
}

// in package 2
type Cartesian struct {
	X, Y float64
}

// back in main.go

type Polar struct {
	r, θ float64
}

type Point = struct{ X, Y float64 } // type alias for reuse

func XYToPolar(p Point) Polar {
	// works for any struct with the same underlying structure
	return Polar{
		r: math.Hypot(p.X, p.Y),
		θ: math.Atan(p.Y/p.X) * 180 / math.Pi,
	}
}

func main() {
	// compiles successfully
	fmt.Println(
		XYToPolar(pack1.Coordinate{X: 5.0, Y: 12.0}),
		XYToPolar(pack2.Cartesian{X: 12.0, Y: 5.0}),
	)
}

Go's support for structural typing isn't comprehensive yet. From Go 1.18 release notes,

> The Go compiler does not support accessing a struct field x.f where x is of type parameter type even if all types in the type parameter's type set have a field f. We may remove this restriction in a future release.

Once this feature lands, there'll be better and more general ways to achieve this.

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

发表评论

匿名网友

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

确定