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

4

7

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?

Galer answered 29/3, 2013 at 13:3 Comment(0)
I
18

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.

Insouciant answered 29/3, 2013 at 13:6 Comment(5)
Except that this method does not support polymorphism, and so does not satisfy the need outlined in the question, which is to pass either a CoordinatePoint or a CartesianPoint to a function which computes a PolarPoint from either. You could not, given this solution, write ConvertXYToPolar as a func(Point) PolarPoint and accept a CartesianPoint, for example.Pattipattie
@Pattipattie The answer wasn't complete. I think your concern is addressed now.Lasky
Nice additions. I really like this approach. I would have used an interface with getX(), setX(), getY() and setY() but that is less elegant than your solution, I think. +1Pattipattie
I'm glad I refreshed before commenting on your answer @dystroy. Much more helpful after the edit. I added a 6th question based on a discussion on the golang google group. The idea of being able to add the ConvertXYToPolar method without modifying existing code is appealing to me, and seems like a somewhat common scenario in the industry. Do you see a compelling reason not to support fields in interface definitions?Galer
Semantically an interface gives an isolation from implementation, that's a very good reason. In my personal opinion, too much exposure reduces the flexibility. Of course too much isolation has drawbacks too but the balance looks fine.Lasky
F
3

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.

Fraze answered 7/12, 2022 at 8:39 Comment(0)
O
2

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
}
Orchestral answered 29/3, 2013 at 13:17 Comment(0)
P
1
  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.
Pattipattie answered 29/3, 2013 at 13:18 Comment(2)
In response to (2), how would you do this without modifying CoordinatePoint and CartesianPoint? I understand duck typing and how it would be done if the structs are already defined in terms of getter/setter methods, but my point here was to use the two structs as is.Galer
My apologies, I misinterpreted that, then. I took (2) to mean "without doing something to explicitly implement the interface". So the answer is "no" then; you would have to add a method of some sort. Look at @dystroy's solution. It's very clean.Pattipattie

© 2022 - 2024 — McMap. All rights reserved.