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: allow conversion between return types and structs #33080

Open
urandom opened this issue Jul 12, 2019 · 18 comments
Open

Proposal: allow conversion between return types and structs #33080

urandom opened this issue Jul 12, 2019 · 18 comments

Comments

@urandom
Copy link

@urandom urandom commented Jul 12, 2019

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

@gopherbot gopherbot added this to the Proposal milestone Jul 12, 2019
@gopherbot gopherbot added the Proposal label Jul 12, 2019
@tema3210
Copy link

@tema3210 tema3210 commented Jul 12, 2019

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: #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
Copy link
Member

@bcmills bcmills commented Jul 12, 2019

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
Copy link

@tema3210 tema3210 commented Jul 12, 2019

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
Copy link
Author

@urandom urandom commented Jul 12, 2019

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
Copy link
Contributor

@bradfitz bradfitz commented Sep 24, 2019

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
Copy link
Contributor

@bradfitz bradfitz commented Sep 24, 2019

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
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Oct 8, 2019

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
Copy link
Contributor

@egonelbre egonelbre commented Oct 9, 2019

Any reason not use:

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

go func() { c <- NewPair(F()) }()
@griesemer
Copy link
Contributor

@griesemer griesemer commented Oct 9, 2019

@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." (#33080 (comment))

@griesemer
Copy link
Contributor

@griesemer griesemer commented Oct 9, 2019

Regarding #33080 (comment), 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
Copy link

@rodcorsi rodcorsi commented Oct 15, 2019

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() }
@bradfitz bradfitz added the dotdotdot label Dec 10, 2019
@ianlancetaylor
Copy link
Contributor

@ianlancetaylor ianlancetaylor commented Jan 21, 2020

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
Copy link
Author

@urandom urandom commented Jan 22, 2020

  • Would you consider yourself a novice, intermediate, or experienced Go programmer?
    

Experienced

  • What other languages do you have experience with?
    

C, Java, Kotlin, Rust, Python, Perl to name a few

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

It won't influence the learning curve

  • Has this idea, or one like it, been proposed before?
    

Not to my knowledge. Though an idea to turn the return tuple into a proper type was.

  • Who does this proposal help, and why?
    

This proposal mainly targets people working with channels, and goroutines that may produce errors (or other tuples), and readers of such code. It will help reduce the boilerplate around passing the valid data and the error across the channel. The proposal is generic enough that there may be other benefactors.

  • Is this change backward compatible?
    

Yes

  • Show example code before and after the change.
    

Before:

type Pair struct {
    i int
    s string
}

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:

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 
}
  • What is the cost of this proposal? (Every language change has a cost).
    
  •     How many tools (such as vet, gopls, gofmt, goimports, etc.) would be affected?
    

I assume gopls would need the most change, as it would need to suggest the function call during completion. I'm not sure how it affects other tools.

  •     What is the compile time cost?
    

I assume the compile time cost to be negligible, and equal to any other conversion operation.

  •     What is the run time cost?
    

I assume it shouldn't have any run time cost.

  • Can you describe a possible implementation?
    

This is all an assumption: Parsing would not need to be changed. During type checking, the call operation will to have the correct ctxMultiOK if being converted to a struct with more than one field. The convertop function will need to be extended so that if the dst.IsStruct is true, the src type is transformed to a TSTRUCT Etype with the fields being the list of return types, before calling IdenticalIgnoreTags. At some place after that, the ast needs to be transformed to insert a statement that assigns the call returns to variables, and the the conversion expression will need to be transformed into a struct literal expression using the temporary variables (there's probably a more intelligent way of doing this).

  •     Do you have a prototype? (This is not required.)
    

No

  • How would the language spec change?
    

The Expressions -> Conversions block would need to be extended to describe the functionality

  • Orthogonality: how does this change interact or overlap with existing features?
    

It's an additional rule to the already existing conversion expression.

  • Is the goal of this change a performance improvement?
    

No

  • Does this affect error handling?
    

Possibly, as the main drive behind it is passing errors as well as other values through a channel

  •     If so, how does this differ from previous error handling proposals?
    

Its a completely different part of error handling, not usually discussed.

  • Is this about generics?
    

No

@urandom
Copy link
Author

@urandom urandom commented Jan 22, 2020

@gopherbot please remove label WaitingForInfo

@bradfitz
Copy link
Contributor

@bradfitz bradfitz commented Jan 22, 2020

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
Copy link
Author

@urandom urandom commented Jan 23, 2020

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
Copy link
Member

@jimmyfrasche jimmyfrasche commented Apr 12, 2020

This is essentially @bradfitz's #33080 (comment) 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
Copy link
Member

@jimmyfrasche jimmyfrasche commented May 2, 2020

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Linked pull requests

Successfully merging a pull request may close this issue.

None yet
10 participants
You can’t perform that action at this time.