-
Notifications
You must be signed in to change notification settings - Fork 17.7k
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: tuples as sugar for structs #63221
Comments
I'm not sure the What if instead of |
I'd be fine with that but honestly I prefer struct because
|
If there is a new keyword it should probably be |
I think this makes sense. It's similar to the existing situation with struct literals initialized without field names, i.e. One question I have though is whether it would be valid to directly use func F1() int { return 1 }
func F2() (int, string) { return 2, "something" }
func main() {
v := pack(F1()) // Allowed?
v = pack(F1()) // Still allowed? Same type?
v2 := pack(F2()) // Allowed?
} |
Yes, unpack is basically the mirror image of unkeyed struct initialization. @DeedleFake I copied your example and put the examples in inline: func F1() int { return 1 }
func F2() (int, string) { return 2, "something" }
func main() {
v := pack(F1()) // v is the same as if the call were pack(1) so: struct(int){1}
v = pack(F1()) // this is still allowed and is the same type
v2 := pack(F2()) // This is allowed by func call rules and is the same as pack(2, "something")
} |
I think it should work like the current system where you can do func Foo() (a string, b int, c error)
func Bar(a string, b int, c error)
packed := pack(Foo())
// ...
Bar(unpack(packed())) In Python, you need One question is if this should work (I think it should): func Foo() []string
func Bar(...string)
packed := pack(Foo())
// ...
Bar(unpack(packed())) A vararg is just a final slice, so unpack should be able to just transparently unpack into it. I am less sure if this should work: func Foo() (a, b, c string)
func Bar(...string)
packed := pack(Foo())
// ...
Bar(unpack(packed())) I think probably not, but it's a harder call. |
@carlmjohnson You are correct about the return type matching (though you have an extra Neither of those last two would work, though: changes to varargs would need to be a separate proposal. |
This works now, so I guess the unpack version of it should too: func Foo() (a, b, c string)
func Bar(...string)
Bar(Foo()) Varadics are different issue and I guess can be handled separately. |
Interesting. I could have sworn that was an error! Yes that would work. And I also misread your first example: func Foo() []string
func Bar(...string)
Bar(unpack(pack(Foo()))) This will not work. It's the same as |
In some languages with tuples, a tuple is a type that can be passed to a function and thus part of a function signature. In this design, that wouldn't be supported is that right? I think one common use will be dealing with configuration functions that take more than 4 arguments. Currently, my rule of thumb is that any function that takes more than 4 arguments should take a config struct instead. With tuples, I could instead see APIs designed and used like this:
I am unsure if that's a good idea or not but I think the temptation will be high because its the path of least resistance. Defining a struct is going to be much heavier by comparison and who wants to do that just to handle one function. If we do think that's a good idea, then being apple to pass the tuple as a type would be nice. |
How does |
|
https://go.dev/ref/spec#Type_identity
By construction there are no tags and the field names are automated so as long as they have the same number of types in the same order they're the same proof!: https://go.dev/play/p/Ek8vIDKndw- this also works if one (but not both) of the packages define the type: https://go.dev/play/p/pUGiqhUxzQB |
Okay. I thought it would work, but then I confused myself. This works because all the fields are public, but this does not because the fields are private. |
You could include a tuple in a function signature like: func F(n int, tup struct(Point, Color)) It's an ordinary type (specifically a struct!) so you can use it however or whenever you would any other type.
I do not think that will be common at all. As your own example shows it buys nothing over not doing it other than having to include an extra pack and unpack. Using a defined struct is immensely superior for the use case specifically because you can name the fields and easily omit irrelevant ones by using a keyed literal. The major use case for tuples is having some types that you need to bundle together but there's no real need for anything other than that. If you've ever written a type with no methods like type locationWeightPair {
L location
W weight
} just so you could use it as a map key or throw it down a channel, you could just use |
I really like this proposal. The more I think about it the nicer it seems. I'm trying to figure out if it could help with two-value iterator problem over in #61405, and I think it probably could, but only partially. You could do something like // t is a struct(string, error)
for t := range tupleSeq {
v, err := unpack(t) // Needs an extra line, but nicer than needing to manually unpack a whole struct.
// ...
} The ability of |
It would only help with iterators if range auto-expanded tuples, which is what, for example, Python does. Since tuples aren't a separate kind of type in this proposal and there doesn't seem to be any interest in raising the number of loop variables past two so I don't see that happening under this or any other proposal, realistically. You could write generic functions to convert a 2-iter into a pair and vice versa. That may be useful. The xiter funcs that are supersets of what is commonly called zip and zipLongest define types that are basically tuples.
That's more necessary than it is useful. I think it would make sense and be fine for some types like |
Go already has tuples in the special case where the type happens to be the same for all the items: arrays. For example, Given that, I think it would make sense to expand |
I think there is no need add new keyword or functions, just use () to pack tuple in the right hand side and unpack in the left hand side. For example: a := 100
b := "string"
c := []int{1,2,3}
// pack
t := (a, b, c)
// and unpack
(x, y, z) := t
// or () can be omitted to
x, y, z := t // same as current syntax, no need introduce extra complexity
// so x is 100, y is "string" and z is []int{1,2,3} |
@leaxoy There are no new keywords in this proposal: just use of one keyword in a new context and two new predeclared identifiers. I don't think just using |
Do we allow tuple as a field in a struct ? type TupleInside struct{
FieldOne string
FieldTwo int
FieldThree struct(int, string, float64)
} If we do, how do we marshal/unmarshal the tuple case ? |
I think yes, because it’s just a struct with some sugar for the declaration.
When you unpack a struct containing a tuple, the target variable would be a struct of the appropriate type. Just like if you have |
The code in your comment is 100% equivalent to type TupleInside struct {
FieldOne String
FieldTwo int
FieldThree struct {
F0 int
F1 string
F2 float64
}
} It's just syntax sugar. |
Sure, I understand it's just syntax sugar. For example, if we want to marshal the type TupleInside struct {
FieldOne string `json:"field_one"`
FieldTwo int `json:"field_two"
FieldThree struct(int, string, float64) `json:"field_three"`
} After de-sugar and marshalling, we would get json as following: {
"field_one": "field_one",
"field_two": 123,
"field_three": {
"F0": 234,
"F1": "f1",
"F2": 345.345
}
} So, that means we cannot customize the tag name for each field in Please correct me if I got something wrong, thanks. |
That is correct. |
Yes, it is true that struct tags are not a part of this proposal, and so I expect most folks will want to avoid using tuple-like structs in types intended for reflection-based marshalling. I don't see that as a significant problem, though. Not all types are useful in JSON serialisation, and that's okay. If you are doing JSON serialisation then you will choose your types with that in mind. |
Random thought: Make the field names something visible, but awful and non-idiomatic, like |
Arguably Fn already satisfies that. I don't think it would be unreasonable to access the fields, for example, to sort a slice of tuples so I think effectively outlawing it overshoots somewhat. |
Hmmm... What if structs allowed numeric field names, without syntactic support for it? You wouldn't be able to define a numeric field name in a normal struct, but when defining a tuple the resulting struct type definition would get numeric fields instead of named ones. Then, allow structs to be indexed into, i.e. In other words, type Tuple struct(int, string) would effectively be equivalent to type Tuple struct {
0 int
1 string
} except that the second would be syntactically illegal because you can't start an identifier with a number.
|
I could imagine tools and libraries doing the wrong thing by failing to special case numeric field names but not well enough to come up with a specific example. In that vein I idly wonder if it would be possible to decree that the fields follow an unexported scheme like |
I suspect some reflection libraries may only look at exported fields by checking whether the first rune of a name is |
One argument for numeric fields is that they could be unexported but made exported later. Though yes, I'd prefer they be exported for the rare case where you do want to just take a quick peek in the crate without having to take the whole thing apart. The argument against doesn't just include reflect but go/ast and co. There is now an assumption that the fields are all legal identifiers so violating that could cause some fun problems. |
A numeric field would still be stored as a string. Only go/parser would have to be adjusted if one were to allow actually writing such fields. If |
Only the parser has to be adjusted for the standard library to work. My concern, perhaps unfounded, is tools compiled against the new version whose logic hasn't been updated and doing something wrong because it expects the field name to be a valid Go identifier or as @dsnet points out not realizing that "2" is exported because it has an ad hoc check instead of using one of the various IsExported predicates. If that's the only change that needs to be made to such code it's probably not that bad. |
Since the standard selector syntax code generation accessing fields would work if the name is left unaltered. There could be issues if it assumes that it can write out the field name in a new struct definition or create a variable using the field name as a prefix. Other than that the only bugs I can think of it causing are incorrectly categorizing it as exported. |
Assuming numeric field names, presumably given type tup struct(int, int)
var s struct {
tup
a, b int
} both |
Would |
it's a natural grammar |
@dsnet I'd just allow decimal integers. A leading zero is only permitted if the field name is 0. So |
There are two proposals for field access in generics:
The |
Would |
Library changes for numeric struct fields: add a predicate to go/token to check if a string matches change token.IsExported to check if the first rune is uppercase or the above predicate is satisfied (ast.IsExported just calls token.IsExported) in go/ast, note that Ident may contain a numeric string when used in a Field in a FieldList used in a StructType parsed from a tuple or in a SelectorExpr.Sel; possibly add an IsTuple method to StructType Make a note in go/types.Tuple that there are tuples now but these are unrelated and change the unexported isExported predicate to match the token version; possibly add an IsTuple method to Struct go/parser,printer,format do not need any visible changes The reflect IsExported predicates do not need to be changed and would work as-is. Possibly add an IsTuple predicate and Pack/Unpack helpers. Pros: No "accidental tuples" so encoding/json could always output a tuple struct as a list without having to worry about backwards compat. Safe to define Undeniably stylish. Cons: Possible to cause issues in tools that use go/ast and libraries that use reflect that do their own checks for exportedness or require that field names are identifiers. More complicated. Conclusion: I am starting to lean toward this. It's costlier than the simple Good idea, @DeedleFake! |
|
oops, yes: corrected to: |
If tuple |
Why? Embedding isn't inheritance so there's no IS-A relationship. Metonymy aside, I wouldn't say that my car is an engine though it contains one last I checked. |
A more interesting embedding case is two tuples of different length: type T1 struct(int)
type T2 struct(int, int)
var s struct { T1; T2; f int } There's an If [warning: this section is not something I'm recommending, just saying this as part of an argument] There's nothing technically blocking allowing users to add numeric fields to struct. A major reason to use them over an identifier scheme is that it avoids incorrectly opting in existing structs. If user defined numeric fields are added at the same time as or later than tuples, there is no such concern. That would allow var n = struct {
4 int
2 int
}{-1, 1} If Given those two things it seems important to
|
#64613 proposes
|
I generally agree, but I think that I prefer |
#66651 proposes variadic generics that would allow this proposal to be written as ordinary code: (along with generic type aliases which are coming soon, see: #46477) package tuple
type Of[T... any] = struct { F T }
func Pack[T... any](v T) Of[T] {
return Of[T]{v}
}
func Unpack[T... any](t Of[T]) T {
return t.F
} |
Updates:
only allowunpack
when all fields are exportedunpack
skips unexported fields of structs from different packagesunpack
is treated the same as unkeyed struct literalsThis proposal adds basic tuples to Go with one piece of sugar and two builtins.
The sugar is for
struct(T0, T1, …, Tn)
to be shorthand for a struct type withn
fields where thei
th field has typeTi
and is namedfmt.Sprintf("F%d", i)
. For example,struct(int, string)
is a more compact way to writestruct { F0 int; F1 string }
.This gives us a tuple type notation for and answers all questions about how they behave (as the desugared struct they are equivalent to). By naming the fields
F0
and so on, this both provides accessors for the individual elements and states that all tuple fields are exported.The variadic
pack
builtin returns an anonymous tuple, with the appropriate types and values from its arguments. In particular, by ordinary function call rules, this allows conversion of a function with multiple returns into a tuple. Sopack(1, "x")
is equivalent tostruct(int, string){1, "x"}
and, givenfunc f() (int, error)
, the statementt := pack(f())
produces the same value fort
as the below:The
unpack
builtin takes any struct value and returns all of fields in the order of their definition, skipping_
fields and unexported fields from a different package. (This has to be somewhat more generally defined as tuples aren't a separate concept in the language under this proposal.) This is always the inverse ofpack
. Example:The
struct()
sugar let's us write pairs, triples, and so on for values of mixed types without having to worry about names. Thepack
andunpack
builtins make it easier to produce and consume these values.No changes are needed to the public API of
reflect
orgo/types
to handle tuples as they're just structs, though helpers to determine if a given struct is "tuple-y" may be useful.go/ast
would need a flag inStructType
noting when a struct used the tuple syntax but as long as the implicit field names are explicitly added by the parser. The only place this would be needed is for converting an AST back to a string.The only potential danger here is
unpack
. If it's used on a non-tuple struct type from a different package it would be a breaking change for that package to add an additional exported field. Go 1 compat should be updated to say that this is a acceptable just as it says that adding a field that breaks an unkeyed struct literal is acceptable. Additionally, a go vet check should be added that limitsunpack
to structs with exclusively "F0", "F1", …, "Fn" field names. This can be relaxed at a later time.This is a polished version of an earlier comment of mine: #33080 (comment) In the years since I've written many one-off types that could have just been tuples and experimented with generics+code generation to fill in the gap. There have been multiple calls for tuples in random threads here and there and a few proposals:
The text was updated successfully, but these errors were encountered: