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

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

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

urandom opened this issue Jul 12, 2019 · 22 comments
Labels
dotdotdot ... LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal
Milestone

Comments

@urandom
Copy link

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
@bcmills bcmills added v2 An incompatible library change LanguageChange Suggested changes to the Go language labels Jul 12, 2019
@tema3210
Copy link

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
Contributor

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

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

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

Any reason not use:

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

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

@griesemer
Copy link
Contributor

@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

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

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

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!

@gopherbot gopherbot added the WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided. label Jan 21, 2020
@urandom
Copy link
Author

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 commented Jan 22, 2020

@gopherbot please remove label WaitingForInfo

@gopherbot gopherbot removed the WaitingForInfo Issue is not actionable because of missing required information, which needs to be provided. label Jan 22, 2020
@bradfitz
Copy link
Contributor

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

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

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

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

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

@seankhliao seankhliao changed the title Proposal: allow conversion between return types and structs proposal: allow conversion between return types and structs Sep 16, 2022
@seankhliao seankhliao changed the title proposal: allow conversion between return types and structs proposal: Go 2: allow conversion between return types and structs Sep 16, 2022
@jba
Copy link
Contributor

jba commented Sep 8, 2023

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:

  • Untagged struct literals. These are explicitly exempted by the Go 1 compatibility promise. There is also a vet check for them, so people generally don't use them. It's always easy to switch to tagged literals.

  • If the struct has only exported fields, clients can define their own identical struct type and convert between the two. But nobody ever does this, because why would you?

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

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

@ianlancetaylor ianlancetaylor changed the title proposal: Go 2: allow conversion between return types and structs proposal: spec: allow conversion between return types and structs Aug 6, 2024
@ianlancetaylor ianlancetaylor added LanguageChangeReview Discussed by language change review committee and removed v2 An incompatible library change labels Aug 6, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
dotdotdot ... LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal
Projects
None yet
Development

No branches or pull requests