Skip to content
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: Go2: Tuple type #32941

Closed
maxekman opened this issue Jul 4, 2019 · 23 comments
Closed

Proposal: Go2: Tuple type #32941

maxekman opened this issue Jul 4, 2019 · 23 comments
Labels
Milestone

Comments

@maxekman
Copy link

maxekman commented Jul 4, 2019

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

@gopherbot gopherbot added this to the Proposal milestone Jul 4, 2019
@ianlancetaylor ianlancetaylor added v2 A language change or incompatible library change LanguageChange labels Jul 5, 2019
@ianlancetaylor
Copy link
Contributor

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?

@maxekman
Copy link
Author

maxekman commented Jul 5, 2019

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)
}

@ccbrown
Copy link

ccbrown commented Jul 5, 2019

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 #973 (comment) though.

@maxekman
Copy link
Author

maxekman commented Jul 5, 2019

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)}.

@deanveloper
Copy link

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

@beoran
Copy link

beoran commented Jul 8, 2019

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.

@maxekman
Copy link
Author

maxekman commented Jul 8, 2019

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).

@urandom
Copy link

urandom commented Jul 8, 2019

@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.

@maxekman
Copy link
Author

maxekman commented Jul 8, 2019

Two really good points there which I hadn’t thought about! Especially the last one; imagine a 2d point type with methods for example.

@ianlancetaylor
Copy link
Contributor

ianlancetaylor commented Jul 8, 2019

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.

@bcmills
Copy link
Contributor

bcmills commented Jul 8, 2019

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?)

@maxekman
Copy link
Author

maxekman commented Jul 8, 2019

@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?

@urandom
Copy link

urandom commented Jul 9, 2019

@ianlancetaylor

Indeed, a seamless conversion between returns and structs will solve a lot of pain points when dealing with channels.

@maxekman
Copy link
Author

Just noticed that @tema3210 posted an example containing a fictional Tuple type in the sum types/discriminated unions issue (#19412 (comment)) which could be of interest to see. It is not directly related to this issue however.

@conilas
Copy link

conilas commented Jul 17, 2019

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".

@maxekman
Copy link
Author

maxekman commented Jul 17, 2019

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).

@mdaliyan
Copy link

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.

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?

@ianlancetaylor
Copy link
Contributor

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.

@griesemer griesemer changed the title Proposal: Tuple type Proposal: Go2: Tuple type Sep 3, 2019
@darkdragon-001
Copy link

Maybe we don't need a new type, but add some of the discussed functionality to structs?

  • (Explicit) (un-)packing of function parameters
  • Easy declaration and instantiation: a := {1, "hello"} (only possible when types can automatically be inferred)
  • Easy struct constructor: Pair{F()}

@conilas
Copy link

conilas commented Sep 4, 2019

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?

[1] https://wiki.dlang.org/Voldemort_types

@ianlancetaylor
Copy link
Contributor

@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.

@rodcorsi
Copy link

rodcorsi commented Sep 5, 2019

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()}

@ianlancetaylor
Copy link
Contributor

There were no further comments related to this specific proposal. Other suggestions should become new proposals (that can refer to this one if appropriate).

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Projects
None yet
Development

No branches or pull requests