-
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: Tuple Types for Go #64457
Comments
Change https://go.dev/cl/546076 mentions this issue: |
Change https://go.dev/cl/546077 mentions this issue: |
Change https://go.dev/cl/546078 mentions this issue: |
Change https://go.dev/cl/546079 mentions this issue: |
If single-element tuples are prohibited, the expander '...' can be omitted. If the left side is a single value then it returns the tuple itself This is also very compatible with functions with multiple return values. |
In the Tuple Conversions section, the value If you have, say, a Can you use keyed literals like How would these tuples be represented in go/ast? You can send a tuple value in an interface to a dependency written for an earlier version of Go unaware of the new |
For a function returning a 2-tuple: func foo() ((int, int)) Is it both legal to get the result as: t := f() // get a tuple
a, b := f() // unpack the tuple Might be a bit confusing when writing code. Or, it might be better to introduce a new syntax for unpacking (at least in this case): (a, b) := f() // unpack the tuple |
@felix021 from what I understand you need the Proposal lgtm overall. |
It's worth noting that the inability to assign to tuple fields nor take their addresses creates a back-door syntax for marking a set of values as immutable at runtime. I'd rather have a real |
Avoiding c := make(chan (int, string), 1)
c <- (3, "Example")
v, ok := <-c // Is this a read and unpack or just a regular two-value read? Also, for a bit of prior art, Python handles single-value tuples in the way proposed, requiring a comma to indicate them, as does Rust. Just thought that that should be mentioned somewhere. My personal opinion on tuples is that while I do very much want them to be added to Go, I think that there's little point in adding them at this point unless they're either some kind of syntax sugar for struct types or a new type of struct. I quite like #63221, for example, but I could also see them being added as something like |
I imagine this would be legal? type MyTuple (int, string)
func (m MyTuple) DoSomething() {} If tuples aren't addressable, I guess |
I understand why this is being proposed that way. But the single biggest reason I want tuples is to be able to write generic functions that operate on function types with an arbitrary number of arguments/returns. In particular, this means we can't use tuples to circumvent the for t := xiter.Map(flag.Args(), func(s string) ((int, error)) { return strconv.Atoi(s) }) {
i, err := t...
// …
} TBQH I don't particularly see the point of tuples, at that point. They are immutable, sure, but why are immutable product types important, but immutable pointers or slices not? I think the only benefit that's left is that they are syntactically lighter than structs. Which, fair enough. Arguably, my generics-concern is better addressed by some form of variadic type parameters anyways, as you probably want a way to manipulate the wrapped function arguments anyways (e.g. you might want a So… my immediate reaction to this proposal is a resounding shrug, I think. |
@dfinkel while the tuple value itself is immutable but you could still have: var s *T = newT()
t := (s, 0)
s.Mutate() @carlmjohnson my understanding is that the values in the tuple aren't addressable so you can't do @Merovius I don't think tuples could address that because you'd still need to specify the tuple itself, specifically its types and therefore their number. |
Thanks for all the feedback! Just to be clear, as I said in the opening section, at this point we do not actually propose that we pursue any of these ideas further. This was simply meant to document a concrete experiment that could (if one wanted) be made into something real. I was primarily interested (at first) to find out if there were any syntactic problems with using a nice tuple notation that doesn't require special markers (there doesn't seem to be any problems); and secondly, I wanted to explore some of the semantic aspects. |
Some people took the time to think this through and brought up good points, so even though we don't plan to do anything further here, I think they deserve answers: @jimmyfrasche (comment) The Converting the result of a multi-valued function into a tuple would require explicit assignment to variables and then the creation of a tuple. A tuple type is a real type, different from a multi-valued result. Also, one cannot use keyed literals. The point here was to restrict tuples to areas where a struct may be overkill. In most cases, a struct is the better solution. I think it's probably a mistake to create "data structures" where tuples play a more important role besides just basic data values, exactly for the reasons pointed out by some of the critics of tuple types. In other words, I see a tuple as a kind of "compound basic type", nothing more. To see how they are implemented in the go/ast, look at CL 546076. Sending a tuple value via an interface to a piece of code unaware of tuples would have to be handled somehow. I haven't thought about it. @felix021 (comment) A tuple must be explicitly unpacked per this proposal. I mentioned at the end that one might be able to make it all work without explicit unpacking, but as you say, it could be confusing, which is why I think an explicit unpack operation is better. @dfinkel (comment) That point is that tuples are immutable - like other basic types. @DeedleFake (comment) Unpacking must be done explicitly. So Also, thanks for the information about the trailing comma use in Python and Rust. I didn't know about that detail, but it's also a pretty obvious approach (we do this in Go to address an ambiguity with type parameters and array declarations). @carlmjohnson (comment) A tuple type is a real type and so one could define methods on it. Tuples are addressable but tuple elements are not. Similar to how a float64 value is addressable, but it's exponent field is not. @Merovius (comment) I think using tuples in any more complex way leads to all the problems outlined by the critics of tuples. I really see them just as super-lightweight structs, to be used exactly (and only) when a struct seems overkill. I see them as a way to make slightly more complex basic types. For everything else, we should be using what we already have. But of course that's just my opinion. |
One of the use cases for tuples is to send the result of a function down a channel. For example a v, err := f()
ch <- (v, err) You could do func T2[X0, X1 any](x0 X0, x1 X1) ((X0, X1)) {
return (x0, x1)
} and then write ch <- T2(f()) but then you need a |
@Merovius that's a bit early (to say the least) to imagine how that could work but I think that one would want |
One thing I'm missing is the ability to be able to convert between a function's return values and a tuple, for the purpose of easily passing the function's values through a channel. That's probably the place I'd use such a type myself |
This comment was marked as outdated.
This comment was marked as outdated.
That's unrelated. @urandom is asking for the ability to do something more like func Example() (int, string) { ... }
c := make(chan (int, string))
c <- Example() |
@DeedleFake I was replying to another comment that has since been deleted The specific case @urandom's asking about is part of the more general "convert multiple returns into a tuple". Since |
|
I am going to retract this proposal in favor of #33080 and its rephrasing #64613. |
NOTE: This is a write-up of an internally worked out idea for tuple types, inspired by some of the tuple proposals listed below. The primary reason for this write-up is simply to document this work for future reference.
Importantly, at this point we do not actually propose that we pursue any of these ideas further because there don't seem to be convincingly strong use cases (where a struct wouldn't be a better choice) that justify the extra language complexity.
Tuple Types for Go
Various proposals suggest the introduction of a tuple type or related mechanism to Go:
proposal: spec: tuples as sugar for structs #63221
Proposes
struct(int, error)
as a shortcut for a tuple with anint
anderror
field.Uses a built-in function unpack to unpack struct into a multi-value.
proposal: Go 2: add tuple type #61920
Based on the idea that a function returning multiple results returns a tuple.
Not very clear about what a tuple type is.
Proposal: Go2: Tuple type #32941
Based on the idea that a function returning multiple results returns a tuple.
Same syntax as proposed in this proposal but overlooks problems.
proposal: spec: allow conversion between return types and structs #33080
A struct value may be implicitly converted to a multi-value returned by a function.
This is an alternative proposal, most closely related to #32941, but with more details worked out.
It uses the same lightweight and intuitive syntax suggested by some of these earlier proposals.
The syntax directly matches the commonly used mathematical notation for tuples:
(a, b)
,(a, b, c)
, etc.The proposed changes are backward compatible with existing code, with a tiny exception: array literals that use the
[...]E
array type notation where the closing bracket]
is not on the same line as the...
will fail to parse. Such code is extremely unlikely to occur in practice.Key features:
A tuple type is a new type (not a struct).
An implicitly typed new tuple value is created by writing down the tuple elements enclosed in parentheses,
like
(a, b, c)
for the 3-tuple with elementsa
,b
, andc
.A tuple type literal is written like a tuple value, but with types rather than values as elements.
A tuple value is unpacked into a multi-value (like the result of a multi-valued function) with the
...
operator:(a, b, c)...
unpacks the tuple(a, b, c)
into the valuesa
,b
,c
.(This avoids the need for an
unpack
builtin and reads pretty nicely.)A tuple element may be accessed (but not set) with a selector indicating the element field number.
For instance,
t.0
andt.1
access the elements 0 and 1 of tuplet
.(This may be a feature that we may want to leave out, at least in the beginning.)
A tuple may be converted into a struct and vice versa if they have the same sequence of element and field types, respectively.
That is the essence of the entire proposal.
Tuple types
A tuple is a finite sequence of values, called the tuple elements, each of which has its own type.
An n-tuple is a tuple with n elements, and n is the length of the tuple.
If n is zero, the tuple is the empty tuple.
A tuple type defines the set of all tuples of the same length and with the same sequence of element types.
To distinguish a single-element tuple type from a parenthesized (scalar) type, the element type must be followed by a trailing comma.
Note: We may want to disallow single-element tuple types (see the discussion section).
The result parameter list of a multi-valued function is not a tuple type, rather the existing syntax requires that such multi-valued results are enclosed in parentheses.
To avoid ambiguities (and for backward compatibility), a function returning a single tuple must enclose a tuple type literal (but not a named tuple type) in an extra pair of parentheses (see the discussion section).
Implicitly typed tuple expressions
A tuple expression creates a value of an n-tuple type from n individual values.
For a given tuple expression with n elements
(x0, x1, …, xn-1)
, the corresponding tuple type is the n-tuple type(t0, t1, …, tn-1)
where the typesti
correspond to types of the corresponding tuple elementsxi
.If a tuple element is an untyped constant, the corresponding tuple element type is the default type of that constant.
To distinguish a single-element tuple expression from a parenthesized (scalar) value, the tuple element must be followed by a trailing comma.
Note: We may want to disallow single-element tuple expressions (see the discussion section).
Typed tuple expressions
If a tuple type is provided explicitly, the usual composite literal syntax may be used to create tuple values.
Tuple elements may be omitted from right to left; omitted elements assume their respective zero value for the given tuple type.
Unpacking tuples
A n-tuple value unpacks into a sequence of n values (a multi-value) with the
...
operator.The sequence of the result types is the sequence of the tuple element types.
The result values may be assigned to variables or passed as arguments to functions following the same rules that apply to functions returning multiple values.
An unpacked tuple may not appear as part of a list of other values.
To make unpack operations work syntactically (without the need for an explicit semicolon), if the
...
token appears at the end of a line, a semicolon is automatically inserted into the token stream immediately after the...
token. This will break array literals using the[...]E{...}
notation if the closing bracket]
appears on a different line than the...
, a situation that is extremely unlikely to occur in actual Go code.It may be possible to avoid the unpacking operator
...
entirely by adjusting assignment rules such that "the right thing" happens based on the number of variables on the LHS of an assignment and whether the RHS is a tuple or not. For instancecould be permitted if
t
is a 3-tuple (to be clear, we are not suggesting this in this proposal - it's just a possibilty one might want to explore more). Using an explicit unpacking operator like...
seems like a good idea for readability, especially when passing an n-tuple to a function requiring n arguments. It also avoids an ambiguity with comma-ok expressions such ast, ok := <-c
where it would be unclear what should happen ifc
's element type is a tuple type.Accessing tuple elements
A tuple element may be accessed (read) directly with an element selector expression indicating the tuple element number, which must be in the range from zero to the tuple length minus one. Unless it is zero, the element number must start with a non-zero decimal digit.
The element number is either 0 or must start with a non-zero digit.
An element selector expression denotes a value, not a variable that can be set.
It is not an addressable operand.
Note: This is perhaps the most controversial part of this proposal.
It may be better to leave this feature out, to discourage uses of tuples where a proper struct would be the better choice.
See also the section Tuples considered harmful? below.
Tuple conversions
A tuple value
t
can be explicitly converted to a struct typeS
(or a struct values
to a tuple typeT
) if the tuple length matches the number of struct fields and each tuple element type in the sequence of tuple elements is identical to the corresponding struct field type in the sequence of struct fields.Tuple identity
Two tuple types are identical if they have the same length and if corresponding tuple element types are identical.
Tuple comparisons
Tuple types are comparable if all their element types are comparable.
Two tuple values are equal if their corresponding element values are equal.
The elements are compared in source order, and comparison stops as soon as two element values differ (or all elements have been compared).
Zero values for tuples
The zero value for a tuple type is a tuple where each element has the zero value for the element type.
Discussion
Empty tuples
The primary use of empty tuples is situations where we currently use empty dummy structs.
For instance, one might define a set type using a map and tuple type like so:
or send a signal without payload on a channel
Single-element tuples
Single-element tuples are problematic as they overlap syntactically and semantically with scalar values.
The proposed syntax proposes using a trailing comma to distinguish between the parenthesized value
(x)
and the single-element tuple(x,)
.A function signature declaring a result type of the form
(T,)
returns a scalar value of typeT
rather than a value of tuple type(T,)
because a single function result parameter may be enclosed in parentheses and be followed by a trailing comma.It is not obvious that single-element tuple types are worth the (small) extra complexity. Nor is it clear that they would be particularly useful. There are several approaches one could take:
Do nothing
Single-element tuples are permitted as proposed for consistency.
Disallow single-element tuples.
A single-element tuple type
(T,)
or tuple value(x,)
is rejected.This can be achieved in three ways: either by treating
(T,)
or(x,)
like a parenthesized type or scalar value (and thus newly permitting a trailing comma in parenthesized expressions); by rejecting a tuple type or value if n == 1 (assuming a trailing comma is present); or by changing the syntax such that trailing commas are not permitted.Treat a single-element tuple as a parenthesized scalar - they are one and the same.
(T,)
and(x)
mean the same as(T)
and(x)
, and a parenthesized scalar may be used like a single-element tuple.Unpacking a parenthesized scalar is a no-op:
(x)...
=(x)
=x
.Converting a parenthesized scalar to a matching single-field struct is permitted:
S((x))
=S{x}
.With this approach, and independently from it, there is still a choice between permitting or disallowing trailing commas syntactically.
Suggested initial approach
Disallowing single-element tuples by changing the syntax to
(which makes it impossible to distinguish between a tuple and a scalar) would permit switching to one of the other approaches in a backward-compatible way in the future. This might be a good initial approach to take.
A word on function signatures
If Go were designed with tuple types in mind from the start, functions would not need the notion of a multi-value return.
Instead, they might simply return a tuple value if needed.
The
...
unpack operator would be used to unpack a function tuple result into a multi-value.In such a world one would not need the extra parentheses around the literal result tuple type.
However, in such a world it would also be impossible to name individual function result parameters (assuming that there's no explicit way to name tuple fields except for access).
To disambiguate between a function returning multiple results and a function returning a single tuple, either a named tuple type or extra parentheses must be used:
Tuples considered harmful?
Various sources report that using tuples indiscriminately harms code readability and makes code evolution
difficult (thanks to @bcmills for this list):
A primary case against tuples is their use when a struct type would be a better choice, because of the ability to give names to struct fields, because fields can be added in the future by simply changing the struct type, and because some tuple libraries permit individual tuple field (read and write) access.
In contrast, tuple types as presented here can be given names like any other types, so extra tuple elements can be added by changing the tuple definition in one place (actual tuple expressions will still need to be updated, though, and that may be desirable).
Individual tuple elements can be accessed but not set or addressed.
Finally, tuples represent immutable values: once created they can only be taken apart, not modified.
Implementation
To study issues with this proposal we have built a basic prototype implementation that can parse, print, and type-check most tuple operations as proposed. The implementation is surprisingly straightforward.
No particular difficulties are anticipated for the compiler's backend as a tuple value can be treated essentially like a struct value.
The API of the reflect package and various libraries (go/ast, go/types) would need to be adjusted to accommodate for the new type.
An alternative (and much simpler implementation) might treat tuples as syntactic sugar for special tuple types (see #63221). Such an approach would simplify the design and implementation significantly because all the usual rules for structs would apply, and all but parsing code would be shared. A tempting choice is to map an n-tuple to a struct with n fields of blank (
_
) name. That falls apart because blank fields are ignored for struct comparison. But there are other alternatives.The text was updated successfully, but these errors were encountered: