golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
122.04k stars 17.44k forks source link

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

Open davidmdm opened 2 years ago

davidmdm commented 2 years ago

Author background

Related proposals

yes

Proposal

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

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.

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
 }

interace/constraint syntax allowing for struct members.

yes

I believe it to be orthagonal

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.

Can't say.

Costs

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.

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

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

ianlancetaylor commented 2 years ago

CC @griesemer

Thanks. Putting on hold until we have more understanding of how generics play out in 1.18.

jub0bs commented 2 years ago

I'm concerned about this proposal... It seems to shift the emphasis of interfaces from behavior to data. I'd be curious to see actual good use cases for this.

DeedleFake commented 2 years ago

It seems to shift the emphasis of interfaces from behavior to data.

Indeed. And while I don't inherently dislike the idea of accessing struct members, I don't find the examples to be particularly convincing. Wouldn't a better approach be the way that the image/color package works with interfaces that provide a common form of the data? For example,

type Point interface {
  XYZ() (x, y, z int)
}

type Point1D struct { X int }
func (p Point1D) XYZ() (x, y, z int) { return p.X, 0, 0 }

type Point2D struct { X, Y int }
func (p Point2D) XYZ() (x, y, z int) { return p.X, p.Y, 0 }

type Point3D struct { X, Y int }
func (p Point3D) XYZ() (x, y, z int) { return p.X, p.Y, p.Z }

func TwoDimensionalOperation(point Point) {
  x, y, _ := point.XYZ()
  return x * y
}

A better example might be trying to do the operation in-place, though that runs into issues with pointers and generics if you try to combine it with struct fields:

// Should p1 be a pointer or not? Neither works nicely.
func TwoDimensionalOperation[T TwoDimensional](p1 *T, p2 T)
davidmdm commented 2 years ago

@jub0bs @DeedleFake,

interfaces are great at describing behavior, and that should be the primary focus of interfaces.

I think what I would like to achieve with the proposal is the constraint aspect. I think the best remark I made in the proposal was the following:

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.

The solutions you proposed above do satisfy the problem, but at the cost of more code/methods/design-patterns, and at a slight performance cost. Which in my contrived example probably does not matter at all. However this could matter a great deal for scientific computing, graphics, and even for image/color as you mentioned (although with a different intent).

When Go accepted to take on Generics, it accepted that it would need a mechanism to constrain types. In the go1.18 release the mechanism is either by behaviour (standard interfaces), or by type sets (new interface/constraint syntax). This proposal makes the case that constraining types by data is going to be a common ask, and can help code readability and maybe performance. Not to mention that constraining by data is completely analogous to constraining by behavior from a type set perspective.

I do agree that work arounds exist, and have served the community quite well as in the case of image/color.

As a final note, It is also out of scope of this proposal to make a data interface anything other than a type constraint, and hopefully that shouldn't shift the purpose of interfaces too far away from describing behavior.

jub0bs commented 2 years ago

@davidmdm

This proposal makes the case that constraining types by data is going to be a common ask, and can help code readability and maybe performance. Not to mention that constraining by data is completely analogous to constraining by behavior from a type set perspective.

I find this statement contentious. AFAIU, the need for type sets only arose because of the need to describe types that have a common behavior (e.g. support for comparison operators <, <=, etc.) other than methods. So I would claim that type sets too are focused on behavior and not on data.

zigo101 commented 2 years ago

Why methods belong to behaviors, but fields don't?

jub0bs commented 2 years ago

@go101 Methods are what a type, regardless of its nature, can do (behavior). Fields are what a struct type has (data).

I don't perceive "having a field named Foo of type int" as a behavior.

zigo101 commented 2 years ago

In my opinion, it is just a matter of definitions. A field is just a simplification of a getter and a setter methods.

changkun commented 2 years ago

A field is just a simplification of a getter and a setter methods.

Only if we can override the field access behavior. Getter and setter methods can be implemented in a concurrent safe way but field access does not guarantee thread-safe.

zigo101 commented 2 years ago

Getter and setter methods can be implemented in a concurrent safe way but field access does not guarantee thread-safe.

That might be true. But it is generics unrelated.

changkun commented 2 years ago

Well, I'd say that's is very true and quite related when runtime component might be involved here. A field can be accessed if and only if a pointer type value is not nil, otherwise it results in a runtime panic. In contrast, a method has no such restriction and will not result in a runtime panic. If that is the case, consider the following example:

type F[T any] interface {
    Fn func() T
}

type S[T F[T]] struct {}

func (s *S[T]) Fn() (t T) { return } // Is it allowed?

Or should we say using F in S is not allowed?

zigo101 commented 2 years ago

In contrast, a method has no such restriction and will not result in a runtime panic.

This is NOT absolutely true:

A field can be accessed if and only if a pointer type value is not nil, otherwise it results in a runtime panic

And I don't think this is a valid reason to deny this proposal. Generics never promise to prevent code panicking.

[Edit]; BTW, I don't understand the intention of the code you shows. I just made a bit modification to make it compile.

type F[T any] interface {
    Fn() T
}

type S[T F[T]] struct {}

func (s *S[T]) Fn() T {
    var t T
    return t
}
changkun commented 2 years ago

The modification is not helpful for discussing the proposal. Thanks.

changkun commented 2 years ago

Generics never promise to prevent code panicking.

I think it is not our decision regarding promises, and also not sure whether the argument aligns with the mentioned confusing code. Just listing observed examples here, that any was decided to not implement comparable because of runtime panic confusion (one of the reasons).

Allow me to elaborate a bit more. According to this proposal, we could write an interface like this to have an Fn field:

type F[T any] interface {
    Fn func() T
}

Now, combining with the previously mentioned existing language behavior, we have two design choices for the code as follows:

type S[T F[T]] struct {}

func (s *S[T]) Fn() (t T) { return }
  1. Disallowing writing method Fn() in S because S is constrained by F.
  2. Disallowing using F as a type parameter in the struct S.

Either way, I would argue they are both confusing. Deciding each behavior will create the issue of either cannot access the Fn field or cannot call the Fn method.

zigo101 commented 2 years ago

Maybe you should elaborate even more. I couldn't understand the following sentences

docmerlin commented 2 years ago

I like this proposal, it really aids in readability by strongly decreasing boilerplate, and reducing need to make getters and setters. If the field access could be inline, that would be even more fantastic.

jub0bs commented 2 years ago

I don't like this proposal. Regardless of boilerplate reduction, it takes Go interfaces far away from what they're intended to be: a mechanism for focusing on what a type can do, rather that what a type is composed of. If this proposal gets accepted, this may be the point where Go jumps the shark for me.

bcmills commented 2 years ago

See previously #19524 (rejected because it was before the Go 2 process started) and #23796 (retracted due to lack of motivating examples‌ — see https://github.com/golang/go/issues/23796#issuecomment-365042943).

bcmills commented 2 years ago

As I noted in https://github.com/golang/go/issues/23796#issuecomment-368643766:

[Requiring that an interface value be implemented by a specific underlying type] is already possible today: if a [value of a] particular type is inspected using reflection, it may be required to be a pointer-to-struct or a channel or obey any number of other invariants. For example, proto.Message is required to be a pointer to a struct type with appropriate field tags. (That's not fundamentally different from the invariants of the standard encoding/json, encoding/gob, or text/template packages: they just happen to use interface{} instead of naming some more specific type.)

To enforce the level of behavioral abstraction you'd like today, you'd have to strike reflection from the language. Given that that's not at all likely to happen (much as I might wish for it), I don't see that it's all that harmful to codify a particularly common type constraint in the interface definition.

That is: we already have those sorts of constraints, and to me it seems strictly positive to promote at the least the common ones from ad-hoc comments into the language proper.

bcmills commented 2 years ago

@changkun

A field can be accessed if and only if a pointer type value is not nil, otherwise it results in a runtime panic. In contrast, a method has no such restriction and will not result in a runtime panic.

That does not hold in general. Compare, say, the case in #18617, in which invoking a pointer method defined by an embedded type unavoidably panics.

It is true that if a method is not promoted by embedding it is possible to implement that method such that it doesn't panic for a nil pointer, but in practice most methods are not implemented that way anyway — to the extent that the fmt package already has special-case code to detect panics due to nil-dereferences.

zigo101 commented 2 years ago

I just realized that a problem (or a difficulty) of this proposal that non-basic interface types might be allowed to be used as value types, then when a value of such non-basic interface type is used as a value argument and passed to a value parameter of a type parameter which constraint is described by this proposal, then modifying a field of the interface argument is illegal.

[Edit]: maybe this is not a big difficulty, but the runtime really needs to change some.

The following code should print 0.0, which means when an interface value is passed to a value parameter of a type parameter, the dynamic value of the interface value should be duplicated. This is already true in theory now, but not true in implementation.

package main

type Point2D struct { X, Y float64 }
type Point3D struct { X, Y, Z float64 }
type C interface{Point2D | Point3D | *Point2D | *Point3D}

func SomeOperation2D[T C](point T) {
   point.X = 1.0 // If point is an interface, then this is illegal.
}

func main() {
    var v C = Point2D{}
    SomeOperation2D(xv)
    println(v.x) // 0.0 or 1.0?
}
davidmdm commented 2 years ago

@go101

Your example goes beyond what I am asking for in the proposal. I am mainly interested in extending the interface syntax for use as a type constraint.

The following in your example is not legal under my proposal.

var v C

It might be somebody else's battle to make that valid, should my proposal be accepted.

My main idea is to generically constrain types by their underlying structure. I think this makes the proposal much simpler, and doesn't involve interface copying.

In fact your example doesn't use any of the syntax I proposed, but simply type sets with legal common field access.

My proposal would look something like this:

To follow your example:

package main

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

type TwoDimensional interface{
  X float64
  Y float64
}

func SomeOperation2D[T TwoDimensional](point T) {
   point.X = 1.0
}

func main() {
    point := Point2D{}

        SomeOperation2D(point)  
        println(point.X) // 0.0

        SomeOperation2D(&point)
        println(point.X) // 1.0

        // Showing this block to highlight that Point3D satisfies the TwoDimensional Constraint.
        point3D := Point3D{}
        SomeOperation2D(point3D)    
        println(point3D.X) // 0.0
}
davidmdm commented 2 years ago

@jub0bs

You have to stop thinking of this as changing what interfaces are for. This proposal is about extending generic constraints.

Perhaps you would have been much more open to this proposal had Go gone with the contract keyword for constraints as in the original generics proposal.

Unfortunately since interfaces and contracts/constraints have been merged into one concept, which is neither a good or bad thing, just the direction that go went with, updating and extending our ability to constrain generic types will most likely impact interface syntax.

I am not proposing in this proposal that struct members be used for instantiable interfaces. Soley for constraints. In the same way that type sets can only be used in the context of a constraint.

I do recognize that people will propose this down the road, but it's not because this brings us closer to that, that we should never improve how we constrain our generic types.

zigo101 commented 2 years ago

@davidmdm Yes, that example is more relevant to https://github.com/golang/go/issues/48522 But the point of that example also applies to the current proposal. The current proposal must wait the point is resolved to be accepted.

In short, in the custom generics age, the dynamic values of some interface values may be partially modified.

davidmdm commented 2 years ago

@go101

Please help me understand your point. What issue around interfaces / dynamic values needs to be solved for the proposal to be viable?

I am not sure I get it. Thanks!

zigo101 commented 2 years ago

if non-basic interfaces could be used as value types, an interface argument might be passed to the point parameter.

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

type TwoDimensional interface{
  X float64
  Y float64
}

func SomeOperation2D[T TwoDimensional](point T) {
   point.X = 1.0
}
davidmdm commented 2 years ago

@go101

I see what you mean. This proposal assumes that non-basic interfaces (interfaces that currently are only valid constraints and non-instantiable) cannot be used as values.

However if they could, I would expect:

var point TwoDimensional = Point2D{}

SomeOperation2D(point)
println(point.X) // 0.0

var pointPtr TwoDimensional = &Point2D{}

SomeOperation2D(pointPtr)
println(pointPtr.X) // 1.0

There might be some ambiguity to if the interface captures the reference and it might Print 1.0 in both cases, but I believe this to be the same ambiguity as to how interfaces work today, and not a blocker to this proposal.

Regardless, it is important to understand that this proposal does not aim to answer this use case, and is only interested in structural constraints

zigo101 commented 2 years ago

Yes, it is not a blocker. But it must be resolved firstly, at it is a more fundamental problem. The runtime needs some adjustments.

Simplified code:

var point TwoDimensional = Point2D{}
var point2 = point
point.x = 1.0

fmt.Println(point2.x) // this should print 0.0
fmt.Println(point.x) // but this? Maybe it should also print 0.0.
ianlancetaylor commented 2 years ago

@go101 I don't understand why point.x = 1.0; fmt.Println(point.x) would not print 1.0. That doesn't seem OK.

@davidmdm While this proposal is only about using interface fields as constraints, we should still plan to remove the distinction between constraints and ordinary interface types if we can. If this proposal can't permit that, that would be a reason to not adopt this proposal.

zigo101 commented 2 years ago

I don't understand why point.x = 1.0; fmt.Println(point.x) would not print 1.0. That doesn't seem OK.

Nowadays, the dynamic value of an interface value is immutable. Maybe the line var point TwoDimensional = Point2D{} or the line point.x = 1.0 should not compile. The reason is a field corresponds two methods, a getter and a setter. The setter is declared for *Point2D instead of Point2D.

So maybe the current proposal should be extended to annotate whether a field is only for getter or both. For example:

type TwoDimensional interface {
    X int  // for getter only
    *Y int // for both setter and getter
}
davidmdm commented 2 years ago

@ianlancetaylor

My goal is to limit the scope of this proposal and hope it is feasible to accept.

I don't think that changing interfaces to be able to describe data is incompatible with the language. An interface is after all, just a description of how to use something; ie how one interfaces.

The common method interface is just the list of methods a type must satisfy so that we can call that method on the underlying value.

An interface with struct members is exactly the same, but lists the fields the type must have so that we can access them from the underlying value given the type information.

I think the exact same rules that apply to interfaces of methods would apply to structural interface.

Indeed there would be no reason an interface couldn't contain both methods and structure.

If there is a technical reason, I would love to know, and if this is poor design I would also to love to know.

However I think it has the potential to remain simple yet give a lot more expressiveness to Go's type system and generics, without introducing unfamiliar syntax and complexity on the user.

At least that is my hope and understanding.

ianlancetaylor commented 2 years ago

My goal is to limit the scope of this proposal and hope it is feasible to accept.

Understood. To be clear, at least for now we won't accept any change to type parameter constraints that would not be feasible to accept for ordinary interface types.

And for the general idea of accepting fields in ordinary interface types, it may be interesting to read #23796.

For this specific issue, I think it's worth asking how often people want to write a generic function that can handle "any struct with a field x int", and how often people want to write a generic function that can handle "any of these already known types, each of which happen to have a field x int". My understanding is that this issue is about the former case. The latter case is covered by #48522.

davidmdm commented 2 years ago

@ianlancetaylor You are absolutely correct that this issue is about the former case about handling any structure with a given field.

I do believe that people do want to express that in Go, and the current work around is to write getters and setters. We have seen examples in this thread of such patterns, for example image/color.

I think #23796 will help resolve many cases for this features, but on the long term maintaining a type set of values that have the data you want doesn't cut it, especially as a library author who may want to provide functionality for their users, and won't be able to add their user's type to their library's type sets.

I think it is likely that this issue should remain open for a very long time as pain points around generics are found, and as the limitations of the current type sets as interfaces are discovered.

I think this proposal is the sensible next step forward as we are improving generics and extending type expressiveness in Go.

mpx commented 2 years ago

I think @griesemer's earlier comment sums up the concern well (avoid requiring a specific implementation for an interface) and recommends a path forward (syntactic sugar for getters/setters).

Finding a way to allow struct fields to match interface methods would be a more general concept. It would allow other types and implementations without forcing a requirement of using a struct. In practice, the implementation could be just as performant as using struct fields.

For example, interfaces could allow struct { Field T } to match either:

type Fielder interface {
    Field() *T
}

type FieldValuer interface {
    Field() T
}

That would handle the motivating use case in this issue:

type Point2D interface {
    X() *float64
    Y() *float64
}

//func Scale[P Point2D](p P, mult float64) {
func Scale(p Point2D, mult float64) {
    *p.X() *= mult
    *p.Y() *= mult
}

It would also solve another issue I hit semi-regularly -- allowing static structs to implement simple interfaces for in-memory implementations. Most recently, this was providing a static solution to fs.FileInfo, often needed when implementing io/fs.File. Currently, it is painful since the obvious field names conflict with the required interface method names. Hence field names need to be renamed to something less obvious/more awkward and many getters need to be implemented.

Ideally something like this would work:

type StaticFileInfo struct {
    Name    string    
    Size    int64
    Mode    fs.FileMode  
    ModTime time.Time
    Sys     any
}

func (s StaticFileInfo) IsDir() bool {
    return s.Mode.IsDir()
}

var _ = fs.FileInfo(StaticFileInfo{})
var _ = fs.FileInfo(&StaticFileInfo{})
bcmills commented 2 years ago

@go101

I just realized that a problem (or a difficulty) of this proposal that non-basic interface types might be allowed to be used as value types, then when a value of such non-basic interface type is used as a value argument and passed to a value parameter of a type parameter which constraint is described by this proposal, then modifying a field of the interface argument is illegal.

I agree that that is a difficulty.

One way to address it might be to disallow writes to interface fields unless the type set of the interface contains only pointer types. But that's a bit unsatisfying, because even if the concrete value is a struct value (not a pointer), it could be a struct whose fields are embedded pointers, and the interface field access could write through those embedded pointers. 🤔

And even then, a value of a concrete pointer type could be a nil pointer, or it could itself contain embedded nil pointers. So even the presence of a pointer type doesn't prevent the possible panic on field access.

On the other hand, if one were to apply today's workaround and provide a setter method, that setter method would also panic in case of nils, but would (presumably) only be defined on pointer receivers.


It seems to me that the crux of the problem is addressability: we ideally want to distinguish between “has a field X” and “has an addressable field X”, in much the same way that reflect.Value.CanAddr reports whether a field reached through reflection is addressable.

And that is exactly the syntax you suggested in https://github.com/golang/go/issues/51259#issuecomment-1098620376! 😃

zigo101 commented 2 years ago

I (suddenly) wonder why the dynamic value of an interface value is immutable. If it is addressable, then things will be simpler. The drawback is the compatibility of some reflect APIs will be broken.

[edit] another drawback is copying an interface needs to duplicate its dynamic value. Currently the dynamic value is not duplicated, which might be the reason why dynamic values of interface values are immutable.

jub0bs commented 2 years ago

I'm still against this proposal, but here is a question: would the order of the fields in the declaration impact whether a concrete type satisfies the interface? Here is an example:

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

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

Would both Point2D and Point3D satisfy TwoDimensional? Or would only Point2D satisfy the interface?

What about the following struct type?

type Point2DPlusMetadata struct {
  meta string
  X, Y int
}
davidmdm commented 2 years ago

@jub0bs I don't believe that order would matter, at the least order would not matter in userspace. When used as a constraint order should not matter anymore than method order matters which is to say none, and this should hold for value interfaces as well.

pcgeek86 commented 1 year ago

100% support this proposal. In fact, it's incredibly frustrating that accessing struct fields from generic types isn't possible.

Since I have to implement separate methods on each of my structs, even if certain fields are common across the structs, requires significant duplication of code, and increased maintenance of the software.

For example:

type Airplane struct {
  age uint8
}

type Car struct {
  age uint8
}

func increaseAge[A Airplane | Car](amount uint8, input A) {
  input.age += amount
}

This simple program results in a compiler error:

./generics.go:12:8: input.age undefined (type A has no field or method age)

Why should I have to duplicate my code by implementing a separate increaseAge(..) function for both structs, when I don't need to?

The same issue occurs if you use an interface as a type set, as in the below example.

package main

type Airplane struct {
    age uint8
}

type Car struct {
    age uint8
}

type HasAge interface {
    Airplane | Car
}

func increaseAge[A HasAge](amount uint8, input A) {
    input.age += amount
}

Many other languages have solved this problem for years, but Golang is lagging behind in design.

On top of that, even if you try using a type assertion to solve the issue, that is disallowed by Golang as well.

func increaseAge[A HasAge](amount uint8, input A) {
    plane, success := input.(Airplane)
}

This causes the error:

invalid operation: cannot use type assertion on type parameter value input (variable of type A constrained by HasAge)

fzipp commented 1 year ago

The question that arises with this proposal: Why should fields be considered in generics but not in interfaces? What makes one fundamentally different from the other, so that this further difference between the two should be allowed? In the past proposals for fields as part of interfaces were rejected. However, if we were to support fields we should do so on both sides of the coin. And just like with methods it should be explicit not implicitly deducted.

pcgeek86 commented 1 year ago

However, if we were to support fields we should do so on both sides of the coin.

Agreed, and generics are built on top of interfaces as type sets, so it's pretty much implied that fields would be supported for generic types and interfaces (without generics).

And just like with methods it should be explicit not implicitly deducted.

I also agree with this completely. The current implementation of interfaces doesn't require a struct to explicitly declare its implementation of an interface. This makes it hard to know if a struct does or does not properly implement an interface. The magical incantation of the reflect package to determine if a struct implements an interface is also challenging to memorize.

If you look at the implementation of interfaces in C#, for example, it includes both properties (fields) and methods. And C# classes have to explicitly declare that they are implementing an interface. This allows the compiler to detect if the interface has not been properly implemented, and a compiler error can be thrown to the developer.

fzipp commented 1 year ago

The current implementation of interfaces doesn't require a struct to explicitly declare its implementation of an interface.

I wasn't talking about that. This (structural implementation instead of nominal implementation of interfaces) is actually fine and in my opinion a great feature of Go. I meant that fields should be listed in the interface like methods if they were supported.

pcgeek86 commented 1 year ago

I meant that fields should be listed in the interface like methods if they were supported.

Gotcha, well that is sensible as well. Although I do think that interface implementations should be explicitly declared on structs as well, for the reasons I previously shared. I'm not sure I would consider it a "feature" that Go doesn't require this. 🤷🏻‍♂️

ianlancetaylor commented 1 year ago

@fzipp

Why should fields be considered in generics but not in interfaces? What makes one fundamentally different from the other, so that this further difference between the two should be allowed?

it depends on your view of fields. If you view field access as an operation on a type, in the same sense that + is an operation on a type, then it does make sense. Generics permit using + if it is available for all members of the constraint's type set, and we could decide that generics permit using .f if it is available for all members of the constraint's type set.

To be clear, this proposal is not just about that. It's also about defining a syntax for making it possible to define the type set "all struct types with a field f of type int" (or whatever).

davidmdm commented 1 year ago

What @ianlancetaylor just wrote is absolute correct.

It's also about defining a syntax for making it possible to define the type set "all struct types with a field f of type int" (or whatever)

For example:

type Named interface { Name string }

Would be the set of types T such that it structurally contains a field that can be identified By the fieldName Name of type string.

This is analogous to today's method based interfaces:

type Named interface {
  Name() string
}

That create a type set of the types that have a method called Name that returns a string.

If we accepted the prior as valid type set, then we could write code that is generic for types over some structural data.

I think the biggest obstacle is consolidating this extended structural interface syntax into regular go programs. How could these interfaces be used as values?

I think that in the same way that regular interfaces have vtables to method pointers for dynamic dispatch, then structural interfaces can be implemented as just vtables to normal pointers for data access. I am doing my best as a layman to sound correct, so please forgive me if the terminology is slightly incorrect.

Let's take some examples:

type Named interface { Name string }

type Person struct { Name string } 

func main() {
  me := Person{"David"}

  // myself is a completely new variable that copies the value and stores the address to the Name property.  
  myself := Named(me)
  myself = "Dave"

  fmt.Println(me.Name) // prints: "David"
  fmt.Println(myself.Name) // prints: "Dave"

  _, ok := myself.(Person) // ok is true
}

Using a pointer instead changes the behavior but in expected ways:

func main() {
  me := &Person{"David"}

  // myself is a completely new variable that copies the value (the pointer), stores the address to the Name property which is the same address as the original  
  myself := Named(me)
  myself = "Dave"

  fmt.Println(me.Name) // prints: "Dave"
  fmt.Println(myself.Name) // prints: "Dave"

  _, ok := myself.(*Person) // ok is true
}

Are there any obvious problems implementing structural interfaces as proposed above?

ianlancetaylor commented 10 months ago

Just a note that this proposal requires a new syntax to describe a constraint for "any struct type that contains a field with and ". The proposal suggests using ~ for that syntax, but that does not match the current meaning of ~. So if we want to proceed with this we need to design a new syntax to use here.

(I don't know whether we want to proceed with this. it's not clear to me how often this comes up. I can see cases for #48522 but I don't yet see many cases for this proposal.)

rubenv commented 10 months ago

@ianlancetaylor I ran into this while making a generic database mapper which may have any number of fields in the model structs, but always expects an ID field to be available. #48522 feels, at first glance, like it'll require explicitly listing all different types. Ideally there's a way to make this open-ended.

davidmdm commented 10 months ago

@ianlancetaylor The proposal actually doesn't suggest using the tilde ~. But was merely using it narratively, illustrating some of my considerations. Indeed the tilde is rejected by the original proposal.

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

The proposal simply put is to extend the definition of an interface to include struct members / fields.

When introducing interfaces as type constraints, we redefined interfaces as type sets, ie the set of types that have method X.

This proposal keeps the type set framing but where we could express the set of types that have field X of some type.

Also as for use-cases, I do not expect it to be a common every day thing but will help the use case where folks need to express Getter/Setters for their constraints and implement them, instead of expressing the fields they expect. It may also have internal performance benefits if it reduces the amount of dynamic dispatch say in like an image processing lib or other. However that is purely speculation and I can't back that up.

davidmdm commented 10 months ago

Also to be fair, it's a really big change and I sometimes also wonder if I want this in Go. However I cannot refute why we shouldn't have the ability to express the set of types that contain a certain Named Field with given Type, or to use that to constrain our generic functions. So I hope it stays open, and that it gets considered carefully at some point in the future!

ianlancetaylor commented 10 months ago

Ah, I see, sorry for misreading the proposal. You are suggesting that we can write interface { F T } to match any struct type with a field F of type T.