-
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: permit referring to a field shared by all elements of a type set #48522
Comments
@gopherbot, please add label generics |
Reading "Composite types in constraints" in https://go.googlesource.com/proposal/+/refs/heads/master/design/43651-type-parameters.md I get the impression that this ought to work; the example there says "this doesn't work because the field types for X don't all match" which implies that if the field types did match, as they do here, then it would work.
|
We don't currently support field accesses of this kind even though the proposal says that this should/could work. We may not support this for Go1.18 as it doesn't seem like an essential feature. There's a trivial work-around that uses a method: package main
import "fmt"
type Point struct {
X, Y int
}
func (p Point) GetX() int { return p.X }
type Rect struct {
X, Y, W, H int
}
func (r Rect) GetX() int { return r.X }
type Elli struct {
X, Y, W, H int
}
func (e Elli) GetX() int { return e.X }
func GetX[P interface { Point | Rect | Elli; GetX() int }] (p P) int {
return p.GetX()
}
func main() {
p := Point { 1, 2}
r := Rect {2, 3, 7, 8}
e := Elli {4, 5, 9, 10}
fmt.Printf("X: %d %d %d\n", GetX(p), GetX(r), GetX(e))
} Of course, then you don't need generic code in the first place because you could just use dynamic method dispatch. And maybe you should. |
The reason I tried this out is for #48499, wherr the OP directly wants to get fields or pointers to fields from a set of similar structs. In that use case, the overhead of an accessor function and dynamic method dispatch would be not acceptable. So while it may not be essential, #48499, which is for use with Go language databases, goes to show that it is not academic, but actually would be very useful for existing code, in stead of the feature proposed in that issue. Furthermore it is more consistent and easier to learn to also allow it. If there is not enough time left to implement this for 1.18, then please consider this for 1.19. |
Open question here. I feel this is a bit restrictive because we have to explicitly say which struct statisfy the constraint. Like for the interface we may want something that will allow us to tell what is the constraint not who can pass. For example : package main
import "fmt"
type Point struct {
X, Y int
}
type Rect struct {
X, Y, W, H int
}
type Elli struct {
X, Y, W, H int
}
func GetX[P struct { X int }] (p P) int {
// here p is known to have only the field X anything more
return p.X
}
func main() {
p := Point { 1, 2}
r := Rect {2, 3, 7, 8}
e := Elli {4, 5, 9, 10}
fmt.Printf("X: %d %d %d\n", GetX(p), GetX(r), GetX(e))
} With this example we can pass any struct that has an |
possibly another way to describe this is..
Just stopping by to say that this is exactly something that I was looking for. Basically, how can I avoid explicitly defining the accessors (via an interface), especially in the case when I fully intend on allowing both Get & Set to occur. I feel that gymnastics are a totally reasonable expectation when I either want only 1 of Get/Set, OR if the Get/Set needs extra explicit handling. In that case, an interface makes sense. Otherwise, it's just boilerplate. |
I believe that this kind of generic structural typing would be extremely useful, and necessary for systems that have performance requirements that dynamic dispatch cannot handle. That being said the above suggestions for type sets with struct members only being structural seems to be against the grain of the type set. If I were to write the follow: type Named interface {
string | struct { Name string }
} I expect the set of types to be the set that consists of string and the struct literal that Has a Name property of type string. Therefore, and this might be extremely unpopular, would it not make more sense to extend the constraint/interface syntax to allow for struct members? type Xer interface {
X int
} In the same way that This way we don't need to touch what the tilde operator means, or how to interpret type unions? Otherwise I think we would be taking two steps backwards in regards to eventually supporting sum types (if ever). EDIT: To add a more concrete scenario suppose: type Point1D { X int }
type Point2D { X, Y int }
type Point3D { 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.
func SomePlanar2DOperation[T TwoDimensional](value T) { ... } |
Many interesting ideas here. But, if the use case of my original examples could work somehow, I don't mind the implementation details. |
I think my idea might need to be rewritten as a proposal. As far as whether common fields of a struct union should usable from the function body, I agree. But somebody who knows if it is implementable should comment. |
I have a real scenario where I'm working on ETL jobs that work nicely with generics. The only thing that prevents it to work is assigning a value of a Field to the struct. And since there is a lot of struct object and each has a lot of properties, adding setter / getter for each struct seems to defeat the whole purpose. |
It is strange that if all the structs have the same underlying type, then the code works. package main
import "fmt"
type Point struct {
X, Y, W, H int
}
type Rect struct {
X, Y, W, H int
}
type Elli struct {
X, Y, W, H int
}
func GetX[P interface { Point | Rect | Elli }] (p P) int {
return p.X
}
func main() {
p := Point { 1, 2, 0, 0}
r := Rect {2, 3, 7, 8}
e := Elli {4, 5, 9, 10}
fmt.Printf("X: %d %d %d\n", GetX(p), GetX(r), GetX(e))
} However, I didn't find the [Edit], maybe the reason why it works is the constraint has a core type: https://tip.golang.org/ref/spec#Core_types |
Promoted fields also work (if their underlying types are the same): package main
import "fmt"
type Base struct {
X, Y, W, H int
}
type Point struct {
Base
}
type Rect struct {
Base
}
type Elli struct {
Base
}
func GetX[P interface { Point | Rect | Elli }] (p P) int {
return p.X // okay
}
func main() {
p := Point {}
r := Rect {}
e := Elli {}
fmt.Printf("X: %d %d %d\n", GetX(p), GetX(r), GetX(e))
} But hybrid promoted and direct fields don't work: package main
import "fmt"
type Point struct {
X, Y, W, H int
}
type Rect struct {
Point
}
type Elli struct {
Point
}
func GetX[P interface { Point | Rect | Elli }] (p P) int {
return p.X // error: p.X undefined (type P has no field or method X)
}
func main() {
p := Point { 1, 2, 0, 0}
r := Rect {}
e := Elli {}
fmt.Printf("X: %d %d %d\n", GetX(p), GetX(r), GetX(e))
} |
Different but with some similarity: package main
func bar[F func(int) | func(int)int] (f F) {
f(1) // invalid operation: cannot call non-function f (variable of type F constrained by func(int)|func(int) int)
}
func main() {} |
It doesn't support method access either. Playground: https://gotipplay.golang.org/p/aEwSDTXYOlL Of course method access can be solved by adding the common method to the constraint:
I would like to ask
EDIT:
This quote has then been ported to the Go 1.18 specs:
Emphasis on usually, which doesn't exclude pushing into the interface's method set the intersection of the methods of the type terms. So... on a second thought, method access not working seems a bug (maybe a documentation bug). Am I misreading something? |
@blackgreen100 That is #51183 |
The Go 1.18 release is a huge release and we simply haven't had the bandwidth to get everything done. |
@blackgreen100 Note that the method set limitation is explicitly called out in the release notes (https://go.dev/doc/go1.18#generics). |
This isn't going to happen in Go 1.19. |
@go101 The comment you link to is not an actual response. For one, you suggest as a solution to "define Either way - the point is that your assumption that any interface will definitely be a valid type argument for a type-parameter constrained on it is anything but given. From what I can tell, you are pretty much alone in the belief that will be the case. And in any case - selector-expressions won't make that any harder. We already have to overcome significant practical hurdles to make that happen (as illustrated with |
Will there be a way to express a constraint as For example: type C interface{ ~struct{ Foo int } } Currently, this require a struct that has exactly 1 field called |
@phenpessoa That's #51259 |
This even can't be promised at today and the age before Go custom generics was introduced. It looks you are over-confident on your opinion. |
I prefer #51259. Allowing operations shared by all primitive types feels different. The intersection of the method sets is not included and the "field set" is more like that than indexing or addition or the like. |
Since this requests something that can already be done, and since methods contain code, which can differ amongst methods of the same signature, allowing the behaviour amongst methods is clearly more powerful and useful, while allowing this behaviour amongst fields simply clutters up the language, and invites pathological cases. It gets really pathological when you have:
Using methods, this is easily handled. But with fields, this can get utterly ridiculous. Fields hold data. What type you use to hold that data should not matter. But now you wish to make it extremely important that you choose the same type as others, regardless of whether it really matters or not. Seriously, the language already provides a straightforward and flexible way of accessing the fields above in a unified manner, by simply requesting that you specify the necessary code to access those fields, you simply tell everyone that you need a Extending this to fields adds no value and makes the language more complicated. Yes, in rare cases you can avoid specifying that With methods, it gives you the opportunity to decide exactly what If people really want to provide a unified and shared manner of access the "x" axis data point above, then asking them to provide a unified method of the same signature is much less of a burden than asking them to coordinate the field name and the data type to match exactly what other people have chosen. Can you imagine what will happen in most cases? Half of developers will chose It might be useful for a single developer, but as soon as code is shared it becomes useless. |
I found a lot of comments proposing to use golang/protobuf#65 As a user of this package, I felt helpless.. |
@seancfoley I'm not sure what you are trying to demonstrate with your example. I don't think this proposal would allow to use selector-expressions with a union of those types either - obviously, a selector expression can only be allowed if all types in the type set have a common field with a common type. Also, I don't find your general argument convincing. This proposal is about accessing a field that is common to all types in a given union. The author of that generic function knows what types are allowed and knows whether or not they share a common field. You are correct that the |
@Merovius My point is about the usefulness of a feature that would require similar data in different structs to synchronize on common field types. Sure, the author knows whether he can do it or not, so what? That doesn't mean it's a worthwhile addition to the language. It's not. The feature becomes useless in a rather trivial manner. It adds complexity to the language (something go has tried to avoid) for a benefit that is near zero. |
I am not sure if this is already covered with this proposal, but I have another use case I haven't yet seen mentioned: providing default struct with functions which accept it, but user can pass another struct to them which have the default one embedded. So users can attach additional data and methods, but functions I provide do not care about them, but should pass the value through. Example: 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's example should not work. Because the type set of |
@griesemer has been looking into possible ways to simplify the definition of generic type checking and in particular removing the concept of core types. It seems like we should wait on that effort before making any changes along these lines. This change might fall out of that, or it might not, but better to have a general cleanup than a pointwise fix that may or may not improve things. In the interim there is always the ability to add explicit get/set methods and adding those to the constraint. |
Based on the discussion above, this proposal seems like a likely decline. |
I request this be put on hold instead of be declined if we are explicitly waiting on @griesemer's work. It doesn't make sense to me to decline on just the potential of it coming out of another solution. |
I also don't see how this is a decline. It is a simple feature that seems
to be in popular demand. While it is possible to define getters and setters
as methods, the point of this proposal is that it would be a lot more
convenient if we could use generics so we don't have to do that.
…On Fri, 3 Nov 2023, 03:31 deef, ***@***.***> wrote:
I request this be put on hold instead of be declined if we are explicitly
waiting on @griesemer <https://github.com/griesemer>'s work. It doesn't
make sense to me to decline on just *the potential* of it coming out of
another solution.
—
Reply to this email directly, view it on GitHub
<#48522 (comment)>, or
unsubscribe
<https://github.com/notifications/unsubscribe-auth/AAARM6OMXD5ZC5RTFZT4243YCPRGPAVCNFSM5EPKXW2KU5DIOJSWCZC7NNSXTN2JONZXKZKDN5WW2ZLOOQ5TCNZZGEZTCMJVGIYQ>
.
You are receiving this because you authored the thread.Message ID:
***@***.***>
|
@griesemer |
This proposal has been added to the active column of the proposals project |
The bot did the wrong thing before. This was supposed to be declined, in favor of #63940. |
I think it's a bit unfortunate that this issue was closed in favor of #63940 because many Go developers are probably not familiar with the concept of core types. However, 8 percent of developers who answered the latest Go developer survey felt that generics were too limited for their needs. Perhaps it would be better to leave an issue like this open to better understand the challenges people are facing rather than having an umbrella issue that seems focused on the internal implementation? |
Core types are not an aspect of the internal implementation, they are defined in the language spec. It won't help anybody to discuss the same issue in two different places. |
Hi @creativecreature, FWIW note that #63940 opens citing this issue here as a motivation, and I don’t think this aspect will be forgotten as work (hopefully) progresses. From the opening comment of #63940:
|
What version of Go are you using (
go version
)?Does this issue reproduce with the latest release?
No, it is a generics issue, therefore tested with a recent tip only.
What operating system and processor architecture are you using (
go env
)?linux/amd64
go env
OutputWhat did you do?
I tried to compile this program (crtp.go) with generics:
with
tmp/golang-tip/bin/go build
What did you expect to see?
Program compiles, runs and outputs
X: 1 2 4
What did you see instead?
./crtp.go:19:11: p.X undefined (type bound for P has no method X)
All three structs in the type bound have an identical X /field/, so I think this is wrong. Of course there is no method but I don't think that matters here. I feel I should be able to use the public field X of p since p can only be one of the three Point, Rect, or Elli.
The text was updated successfully, but these errors were encountered: