I propose the addition of a new built-in type tuple
and that functions should always returns a single value:
// Single return value of int.
func f() int {
return 1
}
// Single return value of the tuple (int, error).
func g() (int, error) {
return 1, errors.New("error")
}
Destruction of the new tuple type should be handled as before when returning multiple arguments, in addition to being able to store the tuple in a var with separate destruction:
// Destruct inline with call.
i, err := g()
// Destruct from tuple var.
r := g()
j, err2 := r
A naive idea for how defining a user tuple type could look:
type Pair tuple (int, int)
My proposal is inspired by the ideas and problems from this article about Monads in Go by @awalterschulze: https://awalterschulze.github.io/blog/post/monads-for-goprogrammers/
In the article he describes composing functions that return errors, for example (not from the article):
func f() (int, err) {
return 1, nil
}
func g(x (int, err)) (int, err) {
return x
}
// Returns 1 and nil from f().
val, err := g(f())
Having these additions to the language would allow for some interesting functional concepts to be implemented cleanly.
I have tried to look for earlier proposals without finding any, please correct me if it has been brought up before. Would love to hear your thoughts!
Thanks,
Max Ekman
Seems to me that I could write your example as:
func f() (int, error) {
return 1, nil
}
func g(i int, e error) (int, error) {
return i, e
}
// Returns 1 and nil from f().
val, err := g(f())
Yes, using tuples is very slightly shorter, but the benefit seems quite small. Is there another way that tuples provide a bigger benefit?
Yes in that case you are right.
I missed a small detail from the example in the article, there was another argument after the int, error pair. The specific section in the article of interest is called “Squinting” (I couldn’t link directly). Here is the initial example exactly from that section that will not currently compile:
func f() (int, error) {
return 1, nil
}
func g(i int, err error, j int) int {
if err != nil {
return 0
}
return i + j
}
func main() {
i := g(f(), 1)
println(i)
}
Allowing functions to return either one value (the tuple) or multiple values (the components of the tuple) based on context-only rather than an explicit syntax seems like it has potential to make things a bit confusing.
func f() (int, int) {
return 1, 2
}
func foo(args ...interface{}) {
// ???
}
foo(f())
Is args
[]interface{}{(1, 2)}
or []interface{}{1, 2}
?
To address the kind of thing in your example, I would be a much bigger fan of just allowing multi-value returns to automatically expand into function arguments, without the addition of a tuple type. See https://github.com/golang/go/issues/973#issuecomment-142733515 though.
Thanks for the link to the interesting comment! Gives some insight. However, it would not be a problem if all functions always returns a single value. In that case the problem lies in allowing destruction when calling another function or not.
In your case with my proposal args would have the value []interface{}{(1, 2)}
.
I'd definitely rather have it expand to []interface{1, 2}
. Also, I personally don't like the idea of having a tuple type that gets expanded into whatever it assigns. After learning several languages that have features such as array restructuring and tuple destructuring, it starts to get confusing on whichever syntax I should use to destructure the tuple. I already have this issue with needing to remember if the ellipsis for a "spread" operator comes before or after the array (ie fmt.Println(array...)
.
Also, typically tuples allow for individual access to the returned arguments. This would hurt Go, as it would encourage the behavior of ignoring errors and possibly leaking resources (ie someMethod(os.Open("file.txt")[0])
). This proposal doesn't offer a way to index into a tuple.
Either way, I think we might want to just add the features that we would _want_ from tuples into multiple-return, rather than introducing an entirely new concept
You can already implement a tuple in Go as it is now, look at the following example:
https://github.com/kmanley/golang-tuple
Furthermore, changing the language to remove multiple return is likely to break all existing Go code, and hence seems unadvisable.
Yes, I know a tuple type can be implemented. However by having a tuple as a native type the above mentioned composition pattern (and other functional patterns) can be implemented in a clean fashion. Please read the linked article if you haven’t to see what could be achieved cleanly with native tuples.
Regarding breaking existing code it will of course happen in varying degrees depending on how this would be implemented if accepted. Note however that this proposal is labeled for Go 2, which would allow backward incompatible changes (although encouraging to minimize them).
@ianlancetaylor
Having the return type be a fully fledged tuple type has some added benefits. The biggest one being that you can finally pass the result of a function through a channel without having to manually convert it to a struct. You can also define methods on them, which is always nice.
It also seems like there aren't a lot of negatives. Seems to be a backwards compatible change, by just promoting a quirk in the spec to a proper type like any other.
Two really good points there which I hadn’t thought about! Especially the last one; imagine a 2d point type with methods for example.
It might be interesting to consider whether we can introduce a conversion between a function result and a struct with the same types, as in the following. That might possibly be a smaller change to the language that achieves similar benefits.
type Pair struct {
i int
s string
}
func F() (int, string) { ... }
func G() chan Pair {
c := make(chan Pair)
go func() {
c <- Pair(F())
}()
return c
}
If we introduce a new kind of type we need to discuss things like how to initialize them, how to convert them, how to decompose them, and what composite literals look like. It seems to me that most of the answers for a tuple
will be the same as the answers for a struct
. That suggests that tuple
doesn't add much to the language. So let's think about what tuple
does add, and it would take to make that work with struct
.
In my experience, tuples are almost always less clear than the equivalent structs with named fields.
See related experience reports from Google's Guava libraries in Java, the Chromium project in C++, and numerous other sources.
(Note that Python's tuples are rendered somewhat less harmful through the use of namedtuple
, which adapts a tuple into a struct — but then why not return a struct in the first place?)
@ianlancetaylor A very interesting simplification! That would solve the (not initially proposed but still valuable) case of directly sending multiple return values on a channel or into another function.
In addition it would still be valuable to be able to unpack return values into non-last parameters of a function:
func f() (int, error) {
...
}
func g(i int, err error, flag bool) (int, error) {
...
}
i, err := g(f(), true)
I believe that would make it possible to chain higher order functions as mentioned in the linked article in the original post. Is that correct?
@ianlancetaylor
Indeed, a seamless conversion between returns and structs will solve a lot of pain points when dealing with channels.
Just noticed that @tema3210 posted an example containing a fictional Tuple type in the sum types/discriminated unions issue (https://github.com/golang/go/issues/19412#issuecomment-510840923) which could be of interest to see. It is not directly related to this issue however.
Some points to be considered:
It is hard for the parser w/ no context to do such a thing when we have multiple return values. The only real choice would be to actually do destructuring as also proposed, but that would mean that multiple return values would become only syntax sugar and that looks like a break of Go's philosophy of keeping things simple, don't you agree? I mean, from a philosophical point of view, it seems that go wants the programmer to know what is happening avoiding to hide details and such.
If it does not, as Go 2 already has the proposal for parametric polymorphism (a.k.a generics), I don't think it would be necessary to use []interface as the return types. But that could be a consideration only if the first point is taken to be invalid.
Apart from that, what has been shown for function composition looks pretty good. But would it be necessary to have tuples in order to achieve that?
My final point is: tuples would be crazy good if we had something like pattern matching with guards and some other functional stuff. I fail to see them as being that big of a deal when not having the whole "functional armor".
Some good points there @conilas.
Regarding the function composition in my last example; that would not need tuples, only allowing multiple return values as function parameters with additional parameters after it. Currently multiple return values directly passed to a function can only be the last argument.
I’m still intrigued by the simplification (conceptually) that functions would ever only be allowed to have a single return value, with multiple return values handled by returning a single tuple of some kind. If implemented cleverly it could be achieved without breaking too much backwards compatibility (i.e. letting the current multiple return syntax construct a tuple instead).
It might be interesting to consider whether we can introduce a conversion between a function result and a struct with the same types, as in the following. That might possibly be a smaller change to the language that achieves similar benefits.
type Pair struct { i int s string } func F() (int, string) { ... } func G() chan Pair c := make(chan Pair) go func() { c <- Pair(F()) }() return c }
If we introduce a new kind of type we need to discuss things like how to initialize them, how to convert them, how to decompose them, and what composite literals look like. It seems to me that most of the answers for a
tuple
will be the same as the answers for astruct
. That suggests thattuple
doesn't add much to the language. So let's think about whattuple
does add, and it would take to make that work withstruct
.
What happens if the Pair
struct has 2 int fields and the F
functions returns 2 ints too... and some day some one accidentaly swaps the order of fields in Pair
struct?
Tuple types don't add enough to the language to be worth the additional complexity of a new kind of type. Tuple types are too similar to struct types, and the additional facilities, while sometimes convenient, seem minor. Therefore, this is a likely decline. Leaving open for one month for final comments.
Maybe we don't need a new type, but add some of the discussed functionality to structs?
a := {1, "hello"}
(only possible when types can automatically be inferred)Pair{F()}
Well, there could be something like Voldemort types from D[1] - in which the type would still be alive after the execution context of the function and we could create it automatically, but would only exist in the context of the calling stack of the function?
Maybe w/o having to declare the struct type inside the function, maybe declaring, idk.
Or maybe the stdlib could provide something like Pair<T1,T2>
, Triple<T1, T2, T3>
since there will be parametric polymorphism in the next version?
Easy struct constructor: Pair{F()}
I think this doesn't look good. How would this be implemented for n-uples anyway? One constructor for each n value written in the compiler?
@darkdragon-001 If those ideas seem useful, they should probably be refined and turned into independent proposals. It's fine to discuss them here, but the goal should be to make a new proposal. Thanks.
I know this may be off-topic now, but the conversion between a function result and a struct could be made in the future with:
Pair{i, s: F()}
There were no further comments related to this specific proposal. Other suggestions should become new proposals (that can refer to this one if appropriate).
Most helpful comment
It might be interesting to consider whether we can introduce a conversion between a function result and a struct with the same types, as in the following. That might possibly be a smaller change to the language that achieves similar benefits.
If we introduce a new kind of type we need to discuss things like how to initialize them, how to convert them, how to decompose them, and what composite literals look like. It seems to me that most of the answers for a
tuple
will be the same as the answers for astruct
. That suggests thattuple
doesn't add much to the language. So let's think about whattuple
does add, and it would take to make that work withstruct
.