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: Go 2: permit function assignment when parameters are assignable #27371

Closed
mccolljr opened this issue Aug 30, 2018 · 16 comments
Closed
Labels
FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Milestone

Comments

@mccolljr
Copy link

mccolljr commented Aug 30, 2018

In Go, it is valid to use the outputs of a function as inputs to another when there are the same number of outputs as inputs, and when each output's type is assignable to the type of the corresponding input (CASE 1):

package main

import (
	"fmt"
)

// alias for readability
type Any = interface{}

func Works(a, b Any) {
	fmt.Printf("got %T %T\n", a, b)
}

func Try() (float32, int) {
	return 1.4, 2
}

func main() {
	Works(Try())
}

Similarly, we can use the results of a function call to return from a function as long as both functions have the same number of outputs and the output of the called function is assignable to the corresponding output of the returning function (CASE 2):

package main

import (
	"fmt"
)

// alias for readability
type Any = interface{}

func Works() (Any, Any) {
	return Try()
}

func Try() (float32, int) {
	return 1.4, 2
}

func main() {
	a, b := Works()
	fmt.Printf("got %T %T\n", a, b)
}

However, it is not possible to do the following. It fails with cannot use Try (type func() (float32, int)) as type func() (interface {}, interface {}) in argument to Fails (CASE 3):

package main

import "fmt"

// alias for readability
type Any = interface{}

func Fails(try func() (Any, Any)) {
	fmt.Println(try())
}

func Try() (float32, int) {
	return 1.4, 2
}

func main() {
	Fails(Try)
}

I propose that the this third case be enabled in Go. Specifically, I propose we allow a function whose signature is func(/* ... */) (A0, A1, ...An) to be assignable to func(/* ... */) (B0, B1, ...Bn) where An is assignable to Bn for all n (and of course when the inputs are also compatible via the existing rules).

Because CASE 2 is valid, it seems like it would only be a matter of auto-generating a one-line wrapper function during the assignment. As an example, let's look at a working version of CASE 3:

package main

import "fmt"

// alias for readability
type Any = interface{}

func Fails(try func() (Any, Any)) {
	fmt.Println(try())
}

func Try() (float32, int) {
	return 1.4, 2
}

func main() {
        wrapped := func() (Any, Any) { return Try() }
	Fails(wrapped) // doesn't fail anymore
}

This would make it easier to maintain type safety in user code while still interfacing with libraries in terms of interfaces.

Edit:

A better example:

var X func(int) (A, B)

func Test(x int) (X, Y) { /* ... */ }

// currently invalid, but the proposal is to make this a valid assignment when
//X is assignable to A and Y is assignable to B
X = Test
@gopherbot gopherbot added this to the Proposal milestone Aug 30, 2018
@mvdan
Copy link
Member

mvdan commented Aug 30, 2018

I don't think this is possible at the moment - a function can't have many types at once in Go. Have you looked at the recent Go2 generics draft yet? It would likely cover your use case here.

@ianlancetaylor ianlancetaylor changed the title Proposal: expand assignability rules for func types proposal: Go 2: permit f(g()) when g returns multiple results and f is variadic Aug 30, 2018
@ianlancetaylor ianlancetaylor added LanguageChange v2 A language change or incompatible library change labels Aug 30, 2018
@ianlancetaylor
Copy link
Contributor

@mvdan If I'm reading this correctly, I think it is technically possible. The function doesn't have many types, we're instead applying assignability conversions on the results. The interesting aspect is that the outer function is variadic. I think it could be done, it's just potentially confusing.

@mccolljr
Copy link
Author

@ianlancetaylor I think you might have mis-understood the proposal. While accepting function returns in a variadic function call would also be interesting, this proposal is as follows:

Specifically, I propose we allow a function whose signature is func(/* same arguments */) (A0, A1, ...An) to be assignable to func(/* same arguments */) (B0, B1, ...Bn) where An is assignable to Bn for all n (and of course when the inputs are also compatible via the existing rules).

I think using ellipses in my original proposal was a confusing choice, so I updated the wording a bit above. It's about allowing assignment of a function with concrete returns to a type where the return values are specified by interfaces implemented by the return types of the function being assigned... if that makes sense.

@ianlancetaylor ianlancetaylor changed the title proposal: Go 2: permit f(g()) when g returns multiple results and f is variadic proposal: Go 2: permit function assignment when parameters are assignable Aug 30, 2018
@ianlancetaylor
Copy link
Contributor

Ah, sorry for misunderstanding. In that case I agree with @mvdan: I'm not sure this is possible in general.

@cherrymui
Copy link
Member

@mvdan I'm not sure I understand it correctly, but I don't see why the proposal needs a function to have multiple types. It only says a func value of one type (say f) in certain cases could be assignable to a variable (argument/result) of a different func type (say g). It requires neither f nor g to have multiple types.

@networkimprov
Copy link

Agreed, that a func taking N arguments ought to be assignable to a type func(a1, a2... aN interface{}). And same for return values. That it doesn't work seems like an oversight, and not nec a Go2 feature.

@mccolljr
Copy link
Author

@networkimprov I think you're experiencing the same misunderstanding that ian did, and I think that's my fault for being unclear.

Here's a (hopefully) better example:

var X func(int) (InterfaceType, error)

func Test(x int) (ConcreteType, error) { /* ... */ }

// currently invalid, but the proposal is to make this a valid assignment
X = Test

i.e. func(int) (Concrete, error) would be assignable to func(int) (Interface, error) as long as Concrete implements Interface

@sirkon
Copy link

sirkon commented Aug 30, 2018

Weak typing it is, i.e. generally bad idea. And IDE can reduce the burden to acceptable level anyway.

See the video: https://www.youtube.com/watch?v=EA6wzEVW7P4&feature=youtu.be

@mccolljr
Copy link
Author

@sirkon I appreciate the thought, but I am familiar with IDEs for Go... I am less concerned with the writability of code, and more with the readability as well as the guarantees provided by being able to write my functions in terms of concrete types. @mvdan is right, the new generics proposal for Go2 does cover this need. Although I am confused why this would necessarily be a Go2 feature. As far as I can tell (which is admittedly limited), it would not prevent existing code from working as expected and it does not claim any new keywords. It might certainly be more complicated than I am imagining to implement after having looked through the compiler typecheck source code, but it could perhaps be a special case when a function is the value being assigned/assigned to.

@ianlancetaylor
Copy link
Contributor

We're treating all language changes as Go 2 issues, backward compatible or not.

@ianlancetaylor
Copy link
Contributor

This seems related to https://golang.org/doc/faq#covariant_types .

@networkimprov
Copy link

I understood you, type T func(a1, a2... aN interface{}) is not variadic, it takes exactly N arguments, so any function taking N arguments should be assignable to a T.

I suggest adding your better example to the proposal blurb.

But a set of arguments (CASE 1&2) is not a type, whereas T is. And T does look rather generic :-)

@mccolljr
Copy link
Author

@networkimprov no, that's not right. This has nothing to do with input parameters. I'm speaking specifically for output parameters.

If a function returns types (A, B), then the proposal is to make that assignable to a value expecting a function with return types (X, Y), provided A is assignable to X and B is assignable to Y, and of course provided the input parameters are the exact same.

@ianlancetaylor Makes sense regarding all proposed changes being go2, my misunderstanding.
I don't think this is quite covariant types since it's really not making a statement about type equivalence. I'm not suggesting that func() (A, B) should be considered the same type as func() (X, Y). Rather, that var x func() (X, Y) = func() (A, B) {/*...*/} be allowed without the incantation var x func() (X, Y) = func() (X, Y) { return (func() (A, B) {/*...*/})() } when the second form would be valid go code. Of course this would apply to pre-defined functions and not just closures like in that example, but for example interface signatures wouldn't care about this.

@bcmills
Copy link
Contributor

bcmills commented Sep 6, 2018

I'm not suggesting that func() (A, B) should be considered the same type as func() (X, Y). Rather, that var x func() (X, Y) = func() (A, B) {/*...*/} be allowed without the incantation var x func() (X, Y) = func() (X, Y) { return (func() (A, B) {/*...*/})() } when the second form would be valid go code.

Assignments in Go generally do not allocate unless they are assignments to interface types.

For that use-case, an explicit conversion seems more in line with the rest of the design of the language, along the lines of []byte and string:

type F = func(int) (A, B)

var X F

[…]
	X = F(Test)  // Explicit conversion, but avoids boilerplate.
[…]

@mccolljr
Copy link
Author

@bcmills I'm alright with that - explicit conversion seems like a reasonable implementation to me.

@ianlancetaylor
Copy link
Contributor

It's worth noting that if we are going to support implicit assignment for functions according to these guidelines, then we should support them for methods too. For example

type T int
func (T) M(int) {}
type I interface {
    M(interface{})
}
// One would expect this to work if this proposal were adopted.
var x I = T(0)

And of course similarly we should be able to assign one interface type to another if the method parameters are assignment-compatible as described in this proposal.

(If we require an explicit conversion as @bcmills suggests, we could require the explicit conversion between interface types.)

This type system complexity is all designed to save people from writing one line wrapper functions. The additional type and language complexity does not seem worth the benefit.

It may be interesting to consider that the wrapper function can be generated using generics as suggested in the current design draft. It requires a separate wrapper function for each number of input parameters and number of result parameters, and the type arguments can not be inferred. This would look something like the following. It's a fair amount of boilerplate for a simple example like this, but presumably one would only be reaching for this ability when there were several similar functions.

// alias for readability
type Any = interface{}

func Fails(try func() (Any, Any)) {
	fmt.Println(try())
}

func Try() (float32, int) {
	return 1.4, 2
}

contract wrappable(t1 T1, t2 T2, t3 T3, t4 T4) {
     _  = T3(t1)
     _  = T4(t2)
}

func Wrapper(type T1, T2, T3, T4 wrappable)(f func() (T1, T2)) func() (T3, T4) {
    return func() (T3, T4) {
        r1, r2 := f()
        return T3(r1), T4(r2)
    }
}

func main() {
	Fails(Wrapper(float32, int, interface{}, interface{})(Try))
}

All in all the benefits of this proposal do not seem worth the additional language complexity.

@golang golang locked and limited conversation to collaborators Oct 10, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests

8 participants