-
Notifications
You must be signed in to change notification settings - Fork 17.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: spec: support for struct members in interface/constraint syntax #51259
Comments
CC @griesemer Thanks. Putting on hold until we have more understanding of how generics play out in 1.18. |
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. |
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 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) |
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
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 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 As a final note, It is also out of scope of this proposal to make a |
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 |
Why methods belong to behaviors, but fields don't? |
@go101 I don't perceive "having a field named |
In my opinion, it is just a matter of definitions. 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. |
That might be true. But it is generics unrelated. |
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:
Or should we say using F in S is not allowed? |
This is NOT absolutely true:
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. type F[T any] interface {
Fn() T
}
type S[T F[T]] struct {}
func (s *S[T]) Fn() T {
var t T
return t
} |
The modification is not helpful for discussing the proposal. Thanks. |
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 Allow me to elaborate a bit more. According to this proposal, we could write an interface like this to have an
Now, combining with the previously mentioned existing language behavior, we have two design choices for the code as follows:
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. |
Maybe you should elaborate even more. I couldn't understand the following sentences
|
I like this proposal, it really aids in readability by strongly decreasing boilerplate, and reducing need to make getters and setters. |
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. |
See previously #19524 (rejected because it was before the Go 2 process started) and #23796 (retracted due to lack of motivating examples — see #23796 (comment)). |
As I noted in #23796 (comment):
|
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 |
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 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?
} |
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. 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
} |
You have to stop thinking of this as changing what interfaces are for. Perhaps you would have been much more open to this proposal had Go gone with the 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. |
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 (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.) |
@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. |
@ianlancetaylor The proposal actually doesn't suggest using the tilde
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. |
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! |
Ah, I see, sorry for misreading the proposal. You are suggesting that we can write |
Just want to put on record here that if we do this, we need to put in place the same restrictions we currently put on interfaces containing methods. That is, we should not allow interfaces containing field-constraints in union elements. The proposal doesn't mention if it would be allowed for such fields to be type-parameter typed. i.e. is this legal? type C[T any] interface {
F T
} Also, what about embedded fields? The obvious syntax for them would be ambiguous ( In general, I agree with @jub0bs that I dislike interfaces being about representation instead of behavior. But also, we already did that when we added generics, so I find it difficult to argue that this proposal would cross any additional lines in that regard.
Because methods can be implemented regardless of representation. While fields are determined exclusively by representation. |
Just to clarify: are you referring to type sets, e.g. type Plusser interface {
~int | ~uint | ~string
} I'm asking because I question claims that the motivation for type sets ever was to specify representation rather than behaviour. In my example above, the idea is to capture the |
@Merovius - my thoughts:
Sounds good to me!
When we express an interface: Suppose the following types: type Named interface { Name string }
type Person struct { Name string }
type Employee struct {
Person
Number int
} Here Employee embeds the Person type, and therefore has a promoted field TLDR: I don't think we need to be able to specify the set of types that embed a field. Just describe the fields that we need to have accessible via our type.
I understand this, and pre-generics it made 100% sense. We needed to have a mechanism for polymorphism that abstracted way the internal representation of the object. This is why, I believe, that in one of the generic proposals we had the two distinct concepts: interfaces and constraints. However constraints were folded back into interfaces with the idea that interfaces are no longer a description of a set of behaviours but instead the representation of a set of types. With that in mind my proposal shouldn't be more offensive than the changes that have already been made to interfaces like |
I don't believe that really resolves my concern. An embedded field does more than just promote its fields. It also promotes methods. And it makes the embedded type available as a selector expression. In your example, if type A struct {
X int
}
type B struct {
Y int
}
type C struct {
X int
Y int
}
type D struct {
A
B
}
type E struct {
C
}
I think it is a valid answer that you couldn't express that constraint. But it still seems like a limitation to me, that is a strange corner.
FWIW I don't agree with this inference. I find it hard to argue that we should not include aspects of representation into constraints. But I don't think it is at all self-evident that we should do more of that. And it could prove a rather deep rabbit-hole, to explore all the aspects of representation you might be interested in. |
I am sorry, but it is unclear to me what the objection is. It feels to me that a lot of this is quite nit-picky, and perhaps a knee-jerk reaction to what would be a big change in interfaces which are very core to Go, and I would not want to rush this decision either. However, it seems clear to me that the idea of constraining types by data is not a novel idea. It is very analogous to interfaces that describe Getter/Setter functions, except that types wouldn't need to explicitly define those methods just to trivially return their data.
Can you explain why you think |
@jub0bs I do agree with you about the intent, but the mechanism is still a constraint on representation. There is still no possible argument that @davidmdm I chose the term "representation" instead of "data" because it is more accurate to what I'm talking about. An interface having "getters and setters" is relatively accurately described as dealing with "data", but not as "representation". If you define an interface FWIW I've so far avoided really taking a strong position on this proposal, because I don't really have one. I'm leaning towards rejecting it, because I don't like to expand the role of interfaces into constraining representation. But it's not a particularly strong argument, because as of right now, we have some of that already. But I'm also not super happy about that, so I disagree that "we can do some of X, so we should do more of X" is sound reasoning. I've just tried hammering out the details, to make the implications of this proposal clear. If we want to do this, we should be aware on the restrictions on union-elements, the limitations about embedded fields, or the use of type parameters. I asked about the first, because I wanted to ensure we don't accidentally decide to do something we can't implement. I asked about the latter two, because I remember that there are some limitations about using type parameters - for example, you can't have embedded fields be type-parameters, you can't have generic type-aliases because of some open questions on recursive types and… perhaps others? I'm not sure. And yes, to me, the limitation that you can't express a constraint on an embedded field is a definite limitation of this proposal. It is just as natural a constraint to express as a constraint to have a non-embedded field. It's possible to do this proposal even with that limitation - but we should acknowledge it exists. I just felt it would be useful to have these questions answered. I wasn't trying to pick nits. |
Thanks for that explanation, and I want to apologize if I was accusatory of any ill intention (nit-picking or otherwise). I suppose the conversation was going in directions that I failed to see as important, and it was starting to frustrate me. I apologize. If I may can I ask you to give your thoughts on the transition to the idea of interfaces as sets of types. To me fundamentally there seems to be a tension between Go pre and post generics around this topic. There's no doubt that using interfaces to describe behaviour gives types free reign to implement it regardless of its representation. An interface of methods is a higher and more powerful abstraction than an interface of structure or representation. However since Go 1.18 and the advent of generics, we've allowed interfaces to represent sets of types, and type unions are a direct consequence of that. Indeed we can express interfaces to be very restrictive as to the type or structure by making a type set of only 1 type: type Int interface { int }. It feels to me that being able to create constraints around the structure of types (fields or access to fields) is the missing piece that fits in between enumerating types via type unions, and the set of types that implement a set of behaviours. My view is that Go is like the ugly duckling stuck in the middle of an odd transition: interfaces represent a set of behaviour / interfaces represent a set of types. Today it is hard to make interfaces represent the set of types we may want. Indeed we need to modify the types to implement the interfaces and it makes it harder to work with third parties. This being said, I agree there may many limitations to the approach I have outlined in the proposal. I would be fine with the same limitations that method sets have with type unions today. I would be fine if they could only be used as type constraints and not as interface values in the same way that type unions cannot be used as values in programs today. Do we commit to interfaces as type sets or no? Without an answer to this question I do not think we should reject this proposal even if it hangs around a long time. |
I don't think that the salient decision is whether or not interfaces describe "sets of types" or not. That's largely a philosophical question - pre-Go-1.18-interfaces can just as much viewed as sets of types, as post-Go-1.18-interfaces can. The distinction is what kinds of sets of types we can describe. Pre Go 1.18, the sets we could describe where all phrased as "all types that included these methods in their method set". Post Go 1.18, we expanded that description language to include "this concrete type", "all types with this underlying type" and intersections and unions (with some limitations) of these sets. This proposal is to expand that description language to include "sets of struct types with a field But it will never be possible to describe all sets of types (there are complicated reasons why that would be infeasible, if not impossible, to implement). For example, "the set of all I don't really agree with you, that this proposal is "the (sic) missing piece" in the constraint language. It is certainly something we can't do right now. It's certainly something reasonable to want to do. But I'm not convinced that it is a particularly important missing piece, personally. For example, it would seem far more useful and powerful to me, to have variadic type parameters or some other facility to express concepts like "a function taking an arbitrary number of inputs and an arbitrary number of outputs, the last of which is an But making that decision requires 1. use cases and 2. the kinds of detailed questions I tried to get into above. Ultimately, those are the important questions that most proposals will come down to: What kinds of code does it enable us to write and is that justifying its cost? Evaluating the cost requires us to know what is proposed exactly.
Personally, I strongly agree that if we do this proposal, it should be allowed to use these interfaces as normal types. I've run into the FileInfo use case mentioned here myself just a few days ago. And also happened upon it when trying to avoid boilerplate with the subcommands package. Personally, I have more use-cases to use the proposed interfaces as normal types, than with generic code. So, I'd recommend focusing on these concrete questions. |
I have another example I would like to have supported. I originally posted it in #48522 but was directed here. This issue seems to have complications of how multiple constraints combine together, but in my use case I really care only about accessing to the embedded struct: type Base struct {
Name string
}
type Extended struct {
Base
Age int
}
func SayHi[T Base](x T) (T, string) {
return x, fmt.Sprintf("Hi, %s!", x.Name)
}
func main() {
x, hi := SayHi(Extended{Name: "foo", Age: 30})
fmt.Println(hi)
fmt.Println(x.Age)
} The example is a bit contrived, but the idea is that |
@mitar Personally, I'm not a fan. IMO the easiest way to write that function would be to have But note that I mentioned above, that we should - in my opinion - also consider how to express the constraint that a field is embedded, if we do accept this proposal. That would subsume what you want, because you could then write [1] And I'd also note that the choice of type-names suggests that you are trying to do OOP-hierarchies and inheritance. If we think that is a good idea, we should introduce inheritance, not inheritance-but-worse. In my opinion. |
But then it cannot return also the original Extended value as well.
No, I am trying to support base functionality where one can extend it for their needs but my code just cares about base functionality. So I do not need that extended struct overrides/shadows base fields/methods. Just that that extended data is passed through. Currently I am using // FindInStruct returns a pointer to the field of type T found in struct value.
// value can be of type T by itself and in that case it is simply returned.
func FindInStruct[T any](value interface{}) (*T, errors.E) {
// TODO: Replace with reflect.TypeFor.
typeToGet := reflect.TypeOf((*T)(nil)).Elem()
val := reflect.ValueOf(value).Elem()
typ := val.Type()
if typ == typeToGet {
return val.Addr().Interface().(*T), nil
}
fields := reflect.VisibleFields(typ)
for _, field := range fields {
if field.Type == typeToGet {
return val.FieldByIndex(field.Index).Addr().Interface().(*T), nil
}
}
errE := errors.WithDetails(
ErrNotFoundInStruct,
"getType", fmt.Sprintf("%T", *reflect.ValueOf(new(T)).Interface().(*T)),
"valueType", fmt.Sprintf("%T", value),
)
return nil, errE
} This allows me to search for |
It doesn't need to. It takes a pointer. |
I think that the under the spirit of this proposal, the example would work slightly differently. What we would want to express here is that IE: type Named interface {
Name string
}
// SayHi is constrained by the Named interface which represents
// the set of types that have access to a field Name of type string
func SayHi[T Named](x T) (T, string) {
return x, fmt.Sprintf("Hi, %s!", x.Name)
}
type Base struct {
Name string
}
type Extended struct {
Base
Age int
}
func main() {
// Here SayHi is instantiated as SayHi[Extended],
// since Extended satisfies the interface "Named" via its promoted field from Base.
x, hi := SayHi(Extended{Name: "foo", Age: 30})
fmt.Println(hi)
fmt.Println(x.Age)
} Under this proposal at least, I don't think that specifying "embeded-ness" of a field to be vital. If this were to pass it could be a separate proposal. Although I agree with you about this example having its roots in OOP and that I do not recommend thinking in those terms when it comes to Go. If we wanted to follow out the thinking of the example but with a Go flavour, we would think about the set of types that have a Base instead of the set of types that are (or extend) a Base. In that case, following this proposal, you could write: type Based interface {
Base Base
} Of which any type that has access to a Base field of type Base would satisfy the interface. This includes both the set of types that have a field explicitly called Base of type Base: In my opinion expressing the set of types that embed Base should not block this proposal in and of itself, and if that indeed is a desire that comes up frequently enough, it should be revisited on its own. edit - typos |
@davidmdm To be clear, my main concern was to have an answer of how to handle embedded fields. "Writing I'm not sure what I think about that answer, but it's something to think about. |
One thing I'd point out is that not every embedded field has the same name as the type. For example type A struct { X int }
type B = A
type C struct { B }
|
I found another approach: type Base struct {
Name string
}
func (b *Base) GetBase() *Base {
return b
}
type hasBase interface {
GetBase() *Base
}
type Extended struct {
Base
Age int
}
func SayHi[T hasBase](x T) (T, string) {
return x, fmt.Sprintf("Hi, %s!", x.GetBase().Name)
}
func main() {
x, hi := SayHi(&Extended{Base: Base{Name: "foo"}, Age: 30})
fmt.Println(hi)
fmt.Println(x.Age)
} So it seems I just want a way to not have to define |
I agree with @davidmdm's opinion. Personally, I even think "embedding field" should never play a role in constraints. Field sets in constraints should always denote behaviors, no others. Nitpick to @davidmdm's code: |
@go101 thanks. I did some copy-pasta! |
I am very confused by this "must be behaviour" discussion because that ship sailed when interfaces were extended to denote also constrains. Type Parametrization is clearly about types and not about behaviour. By definition: "an operation is permitted if it is permitted for all types in the type set defined by the constraint". Clearly if all types of the set permitt the operation "access to field X" this is within that definition. Now I also undertand that finding a generic way to express all possible definitions of a type is hard and maybe even not feasable but let's not waste time discussing about decisions that are already in place. |
Operators were the sole reason this syntax was introduced at all. Anybody using A | B for some other purpose than capturing an operator is basically abusing it. |
@gophun I don't believe that is true. For one, it is a necessary feature to allow specifying that a method must be on a pointer type, which has little to do with operators. More importantly, an important thought behind union elements was also #57644. And if we really where only concerned with operators, we would have only allowed Really, unions just say what they say: A type argument must be one of these listed types. And it's a reasonable constraint. No abuse here. |
That is not what this issue is about. This issue is about introducing a new kind of constraint. Allowing field access on already expressible constraints is #48522 (TBQH it's a bit strange to me, that these two issues constantly get mixed up. ISTM their titles are sufficiently clear in their respective intent). |
@Merovius yes I know that is another issue but in my confusion that the merits of this proposal are being discussed on some false premises I also failed to express that I don't support this proposal because it's introducing a constrain that is already inherit in the type definition itself if rules were relaxed (in accordance with the definition) so that #48522 was permitted. (at least when you know the list of types in question, the more generic case that this would enable is in my opinion more complexity for little added benefit over the #48522) @gophun access to operators is one of aspects but the also one of the main benefits, at least for me, is to use compile time type safety on many places where interface{} was used before. In that context, the constrain of type unions like |
It's ok to want type unions, but don't abuse generics for it. Type unions belong first and foremost on the non-generic side of the type realm, which would be #57644. |
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
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:
This fails like so:
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:
However this does not seem to be compatible with what is being released in go1.18.
Consider:
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: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:
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 methodGetX() 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:
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 }
vsinterface{ X int }
, but I believe this to be minimal.For all the prior questions: can't say. Defer to smarter people.
The text was updated successfully, but these errors were encountered: