golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
124.02k stars 17.68k forks source link

proposal: spec: allow conversion between return types and structs #33080

Open urandom opened 5 years ago

urandom commented 5 years ago

This comes from a conversation in #32941

The goal of this proposal is to allow easy conversion from a function return to a struct with the same field types and order.

This would allow to more easily pass a function result to a channel, without having to manually create temporary variables and filing them up in a struct before it is sent thorough a channel.

Example :

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 
}

Like converting between structs, tags should be ignored

tema3210 commented 5 years ago

As example we can have:

type T struct {
          i string,
          i2 int,
}
type T2 tuple (string,int)
func A() (string,int) {...}

And in another function:

func main() {
          var a T
          var b T2
          a = A() //valid, with this proposal
          b = A() //valid, with another proposal
          d,c := A() //valid, currently
}

Another proposal is: https://github.com/golang/go/issues/32941 We can add the tuple type, all this conversions, return type will still be multiple return,but tuple will be constructed in place, then converted to the struct(or just saved).

However this is related features, i don't see any valuable benefits of adding these features aside.

bcmills commented 5 years ago

There is an interesting duality for function arguments: today it is possible to pass the results of a multi-result function directly to another function that accepts the same argument list (https://play.golang.org/p/f3kNTppbVMc).

Should it be possible to make such a call by unpacking a struct?

If so, arguably that should require explicit syntax — what would it look like?

tema3210 commented 5 years ago

There is an interesting duality for function arguments: today it is possible to pass the results of a multi-result function directly to another function that accepts the same argument list (https://play.golang.org/p/f3kNTppbVMc).

Should it be possible to make such a call by unpacking a struct?

If so, arguably that should require explicit syntax — what would it look like?

Technically, yes, but I don't think that it's good idea. Readability will be almost lost(due to required declaration reading).

Personally I can't imagine syntax.

urandom commented 5 years ago

This proposal does not discuss assignability, only convertibility. The example @tema3210 would actually be invalid, as the proposal currently stands.

That being said, I don't see anything preventing a future discussion on assignability. Nevertheless, as for a hypothetical expansion syntax, why not just use ...?

bradfitz commented 5 years ago

Should it be possible to make such a call by unpacking a struct?

If so, arguably that should require explicit syntax — what would it look like?

I was proposing to @griesemer that maybe we also use ... to unpack a struct in a call:

type Pair { x, y int }
func f(a, b int) {}
func main() {
    var p Pair
    f(p...)
}

Or even:

   s := struct{_ int; _ int; _ string}{1, 2, "foo"}
   x, y, z := s...
bradfitz commented 5 years ago

And maybe using struct(...) to convert an arbitrary function call return (or literals too?) into an unnamed struct:

func f() (int, int, string) { return 1, 2, "foo"}
func g() (x int, y int, N string) { return 1, 2, "foo"}
func main() {
     fmt.Printf("%T\n", struct(f())) // struct { _ int; _ int; _ string }
     fmt.Printf("%T\n", struct(g())) // struct { x int; y int; N string }
     y := struct(1, 1.0, "foo")
     fmt.Printf("%T\n", y) // struct { _ int; _ float64; _ string }
}

But then you have issues of naming the struct fields. If you converted an interface method call into a struct, you'd need to use the name of the interface method's named return values (which often aren't named, or are named all lowercase, so the struct field would have unexported fields).

(Braindump from me + @griesemer)

ianlancetaylor commented 5 years ago

Suppose that instead of Pair(F()) we change the rules of composite literals so that a function call with multiple results can be used, and treated as though the results appeared in the composite literal as separate values.

That is, in the original example, instead of

    go func() { c <- Pair(F()) }() 

we permit

    go func( { c <- Pair{F()} }()

This would be permitted for slice/array composite literals as well. (It wouldn't work for maps as there would be no way to specify both the key and the value.)

This seems like a relatively simple extension to the way that functions with multiple results can be used today.

egonelbre commented 5 years ago

Any reason not use:

func NewPair(i int, s string) Pair {
    return Pair{i, s}
}

go func() { c <- NewPair(F()) }()
griesemer commented 5 years ago

@egonelbre Of course this is an option. But I think the point the proposal author is trying to make is that this extra work shouldn't be required: "The goal of this proposal is to allow easy conversion from a function return to a struct with the same field types and order." (https://github.com/golang/go/issues/33080#issue-467483332)

griesemer commented 5 years ago

Regarding https://github.com/golang/go/issues/33080#issuecomment-539713498, to clarify: Like for function invocations f(g() where g returns multiple results and f accepts multiple results, if S is a composite literal type (struct, array, or slice) we would only allow S{g()} and not S{a, b, g(), c, d} or S{g(), g()}.

rodcorsi commented 5 years ago

Why not specify the struct field that function assigns:

Pair{ i, s: F() }
type Point2D struct { x int, y int }
// ignore z
Point2D{ x, y, _: Point3D() }
ianlancetaylor commented 4 years ago

Hi, we're sorry this issue has been open for so long. In an attempt to reduce backlog of language change proposals, we're trying out a new template describing them. Would you mind filling out the template at https://go.googlesource.com/proposal/+/bd3ac287ccbebb2d12a386f1f1447876dd74b54d/go2-language-changes.md .

When you are done, please reply to the issue with @gopherbot please remove label WaitingForInfo.

Thanks!

urandom commented 4 years ago

func F() (int, string) { ... } func G() chan { Pair c := make(chan Pair) go func() { count, id := F() c <- Pair{count: count, id: id} }() return c }


After:
```go
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 
}
urandom commented 4 years ago

@gopherbot please remove label WaitingForInfo

bradfitz commented 4 years ago

Would this change make Go easier or harder to learn, and why?

It won't influence the learning curve

Adding anything to the language means there's more to learn. Maybe new users won't write that code initially, but they'll eventually want to read & understand other people's code that uses such new features.

urandom commented 4 years ago

Would this change make Go easier or harder to learn, and why?

It won't influence the learning curve

Adding anything to the language means there's more to learn. Maybe new users won't write that code initially, but they'll eventually want to read & understand other people's code that uses such new features.

My understanding of that question is that since this builds on an existing feature, it won't make learning Go harder. It adds more, but doesn't make things harder to learn, a new user, knowing that you can convert between types, will know what the new code does without knowing the new rule. Though it might be possible I've misunderstood the question.

jimmyfrasche commented 4 years ago

This is essentially @bradfitz's https://github.com/golang/go/issues/33080#issuecomment-534759183 but split into two parts and with a solution for field naming:

Introduce a new way for writing struct types without specifying field names. In struct (int, string), each field is automatically named Fn where n is its position in the definition. So struct (int, string) is 100% sugar for/completely identical to

struct {
  F0 int
  F1 string
}

This variant of struct declaration is a tuple type without introducing a new kind of type, just a convention and a shorthand. More important here, it gives a way to talk about structs with unspecified field names.

Introduce a new builtin tuple. It creates and populates a struct without specified field names based on its arguments: tuple(false, 3.14) has type struct (bool, float64).

Because it's a function, the usual rules apply so, for func f() (T, error), tuple(f()) has type struct (T, error).

With those and ... for struct unpacking, you can write:

ch := make(chan struct (T, error), 1)
ch <- tuple(f())
v, err := <-ch...
jimmyfrasche commented 4 years ago

This could also be solved by generics.

package tuple

type Len2(type T0, T1) struct {
  F0 T0
  F1 T1
}

func Pack2(type T0, T1)(f0 T0, f1 T1) Len2(T0, T1) {
  return Len2{f0, f1}
}

func (t Len2(T0, T1)) Unpack() (T0, T1) {
  return t.F0, t.F1
}

This can be easily generated from 2 to some n large enough for most reasonable cases.

With that the previous code example is

ch := make(chan tuple.Len2(T, error), 1)
ch <- tuple.Pack2(f())
v, err := (<-ch).Unpack()

That's almost as good as native syntax. It's more verbose and the arity gets brought up a lot, which is annoying but probably fine.

The only language change required is generics but ... for structs would obviate the Unpack method and allow aStructType{aTupleValue...}.

gregwebs commented 2 years ago

There's another language improvement that would help out this use case a little bit. Rust, TypeScript, Haskell, and other languages allow for specifying just the field name and then automatically using an in scope variable with the same name as the value. So a snipped from the example would become:

        count, id := F()
        c <- Pair{count, id}

Not a huge improvement for this example, but when these patterns are allowed for both structuring and de-structuring it can add up in a program in a lot of places like this and the nice thing is it doesn't negatively affect readability.

jimmyfrasche commented 2 years ago

For that to work there'd need to be some way to disambiguate it from sequentially filling the fields. In some other thread I used Pair{:count, :id}.

jba commented 1 year ago

As far as I can tell from a first reading, none of the suggestions here consider backwards compatibility.

In the current language, it is always backwards-compatible to add a comparable field to a struct, in the sense that all existing code will continue to compile. There are two exceptions to this:

Some proposals here change the calculus. The one that lets you write a struct conversion that takes multiple return values will make struct conversion much more common, so if your package defines

type S struct { Int int; Err error }

you won't be able to add a field to that because clients might be using it to convert calls to strconv.Atoi.

The proposal for S{F()} puts untagged literals back in play, in a way where there is no easy rewrite to a tagged form.

Allowing f(s...) where s is a struct has the same issue.

Any syntax that takes all struct fields together and translates them to another form—args, return values, whatever—will affect backwards compatibility.

griesemer commented 11 months ago

I believe we should seriously consider the ideas suggested here. I filed #64613 which is a restatement of the best ideas in this proposal.