Skip to content

proposal: spec: support for struct members in interface/constraint syntax #51259

@davidmdm

Description

@davidmdm

Author background

  • Would you consider yourself a novice, intermediate, or experienced Go programmer?
    I consider myself an intermediate to experienced go programmer writing Go for the last 4 years and professionally for the last 2 years.

  • What other languages do you have experience with?
    I have experience in JS/TS, and a tiny bit of rust but nothing to bat an eye at.

Related proposals

  • Is this about generics?

yes

Proposal

  • What is the proposed change?

The proposal is to extend the interface/constraint syntax to support struct members.

  • Who does this proposal help, and why?

This proposal would allow for generic functions to be structurally generic. It would reduce a lot
of boilerplate around getters and setters for simple struct members, and would/might/could increase
performance for generic code by avoiding dynamic dispatch of interface methods for simple property accesses.

  • Please describe as precisely as possible the change to the language.

As a little bit of background, at the time of this proposal there is currently an issue being tracked for go1.19 about being able to use common struct members for generic types that are the union of structs . For example:

type Point2D struct { X, Y float64 }
type Point3D struct { X, Y, Z float64 }

func SomeOperation2D[T Point2D | Point3D](point T) {
   return point.X * point.Y
}

This fails like so:

point.X undefined (type T has no field or method X)
point.Y undefined (type T has no field or method Y)

Now the issue still stands as to whether or not Go should support the intersection of struct members for a union of struct types, but we can let that issue decide that use case.

The interesting thing that came out of the discussion, is that really what we want to express, is to be generic structurally hence:

func DoSomethingWithX[T struct { X float64 }](value T) float64 {
  return value.X
}

p2 := Point2D{}
p3 := Point3D{}

DoSomethingWIthX(p2) // would work becuase Point2D has member X
DoSomethingWithX(p3) // would work because Point3D has member X

However this does not seem to be compatible with what is being released in go1.18.
Consider:

type A struct { X, Y int }
type B struct { X, C int }

func Foo[T A | B](value T) { ... }

We would no longer be able to express that we want exactly struct A or B, as this would express any superset struct of A or any superset struct of B.

Adding the ~ operator to signify we want the approximate shape of the struct seems like a likely candidate:

// Foo accepts any value that has struct member X of type int
func Foo[T ~struct { X int }](value T) { ... }

However this breaks the ~ operator as defined in go1.18 as to mean any type who's underlying type is what follows the tilde.
I do not believe that making a special case for the tilde with a struct to be a good idea for orthogonality in the language.

Therefore, my proposal to is to extends our interface/constraint syntax to include struct members:

type Xer interface {
  X int
}

This works nicely with the idea of type sets and is fully backward compatible semantically with go1.18.

In the same way that type XGetter interface { GetX() int } represents the set of types that implement the method GetX() int, Xer would be the set of types that have a member X.

This way we don't need to touch what the tilde operator means, or how to interpret a type set of structs as constraint.

Slightly more illustrative example of what this might look like in real code:

type Point1D struct { X int }
type Point2D struct { X, Y int }
type Point3D struct { X, Y, Z int }

// type constraint
type TwoDimensional interface { X, Y int }

// Works for any struct with X and Y of type int, including Point2D and Point3D, etc, and excluding Point1D
func TwoDimensionOperation[T TwoDimensional](value T) { 
  return value.X * value.Y  // legal member accesses as described by the TwoDImensional constraint
 }
  • What would change in the language spec?

interace/constraint syntax allowing for struct members.

  • Is this change backward compatible?

yes

  • Orthogonality: how does this change interact or overlap with existing features?

I believe it to be orthagonal

  • Is the goal of this change a performance improvement?

Not necessarily but it may have performance benefits. Especially since currently, if we want to be able to pass any value with struct member X, we would need to create an interface with Getter and Setter methods; Generic struct member access is likely to prove to be faster than the current dynamic dispatch approach.

If so, what quantifiable improvement should we expect?

Readability and expressivity. Might make some generic code more performant.

  • How would we measure it?

Can't say.

Costs

  • Would this change make Go easier or harder to learn, and why?

I do not think it would make generics any more complicated than they already are.
Indeed it might solve headaches for people running into this problem and make generics
slightly easier to grasp or express overall.

  • What is the cost of this proposal? (Every language change has a cost).

Might cause confusion when reading struct{ X int } vs interface{ X int }, but I believe this to be minimal.

  • How many tools (such as vet, gopls, gofmt, goimports, etc.) would be affected?
  • What is the compile time cost?
  • What is the run time cost?
  • Can you describe a possible implementation?
  • Do you have a prototype? (This is not required.)

For all the prior questions: can't say. Defer to smarter people.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions