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 defining function using named function type #30931

Closed
iamgoroot opened this issue Mar 19, 2019 · 24 comments
Closed

proposal: Go 2: permit defining function using named function type #30931

iamgoroot opened this issue Mar 19, 2019 · 24 comments
Labels
FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Milestone

Comments

@iamgoroot
Copy link

iamgoroot commented Mar 19, 2019

Summary

As the title says it's all about named function types
If type 'transform' is an named function type allow for reusing that type data in function definition.

type transform func(string) string

func exec(form transform) {
	v := form("val")
	fmt.Println(v)
}

func main() {
	// current syntax

	exec(func(s string) string { return "" })

	// proposed (alternative) syntax

	exec(transform(s) { return "" })

}

Motivation

Named function type seems like a nice feature but now the best you can do with it is code documentation and also you can put methods on your functions.
If you give a name to a function signature - you want to use that name in code somehow.

Same syntax will do for single-method interfaces. But that's harder since we'll have to distinguish single-method and multi-method interfaces. The 'type' keyword and functions is an attempt to avoid that.

It's not that go needs lambda functions. It's that we have those types defined in projects and they are 100% compatible.

@gopherbot gopherbot added this to the Proposal milestone Mar 19, 2019
@ianlancetaylor ianlancetaylor changed the title Proposal: allow for function definition using aliased function type proposal: Go 2: permit defining function using named function type Mar 19, 2019
@ianlancetaylor ianlancetaylor added LanguageChange v2 A language change or incompatible library change labels Mar 19, 2019
@ianlancetaylor
Copy link
Contributor

See also #21498.

@iamgoroot
Copy link
Author

See also #21498.

Note, that this one is probably different since referenced proposal is based on deriving typing information from given context.

Current proposal deals with implicitly defined named types. This should allow faster parsing and less changes overall in comparison.

@DeedleFake
Copy link

DeedleFake commented Mar 19, 2019

I will say that it would be quite nice to be able to use

return http.HandlerFunc(rw, req) {
  // Do something.
}

instead of

return http.HandlerFunc(func(rw http.ResponseWriter, req *http.Request) {
  // Do something.
})

Some solution to the aforementioned related issue would probably fix this as well, but not in some cases, such as when returning. If the function returns an http.Handler, rather than an http.HandlerFunc directly, there'll be no way for the compiler to figure out what type to use there.

@iamgoroot
Copy link
Author

iamgoroot commented Mar 19, 2019

@DeedleFake In case of type aliasing you would probably write.


type Handler func(http.ResponseWriter, *http.Request)

http.HandlerFunc(Handler(rw, req){
    // Do something.
})

Also return types are included into named type definition, so there's zero guessing to do.

@DeedleFake
Copy link

DeedleFake commented Mar 19, 2019

Just want to make sure there aren't any semantic confusions here: There's no type aliasing there. The type alias syntax is type A = B, not type A B. The first creates an alias while the second creates an entirely new type with a new method set that has the same memory layout as the original type but that is otherwise completely distinct. Function types are loose in that they're far more willing to do implicit type conversions than other types, but it's still not a type alias. Never mind. My mistake. That's just the untyped literal system, I think. See here for a counter example.

@iamgoroot
Copy link
Author

You are right. It was a harsh misuse on my part.

@beoran
Copy link

beoran commented Mar 20, 2019

Actually, I like this as this is a arguably simpler approach to get the benefits of #21498, in a mostly backwards compatible way. I don't know, though if this would be practically possible though without too much impact on the speed of the compiler, since now there is no func keyword to tell the go parser that an inline function is following.

@iamgoroot
Copy link
Author

@beoran Mostly guessing but:

  1. Allow defining a block after function call token when parsing function calls
  2. When type information is available fail compile error if type is not named function type

Looking at go/less package there is a Lookup method that shows you to position your token defined in. So basically if you encounter any type usage you lookup the definition anyway and the definition has all typing info on your new function - what's left is to put parameter names into places they belong.

You if you have more insight on how things really work in compile time - lets discuss this.

@RaduBerinde
Copy link
Contributor

@DeedleFake There was some truth to your original assessment: https://play.golang.org/p/jVstCUCnxG-
Here f has a definite type that is not our defined type, and yet we can pass it directly. I think this is coming from the "assignability" definition in the language spec - one of the conditions is: x's type V and T have identical underlying types and at least one of V or T is not a defined type. (note: an earlier version of the spec said "named type" here which I think is more accurate).

@seebs
Copy link
Contributor

seebs commented Apr 16, 2019

So, it seems to me that this is the root issue that caused me to run into a different issue (#31444). Right now, function literals can be passed to a function taking a defined function type if they have the same underlying type, because of assignability. This doesn't happen for defined types based on the basic types (like int) because those are all defined types.

I do see some difficulties from losing the func keyword to mark a function literal. I think a big part of the difficulty is that the function's signature includes both the type specifications and the names for the parameters (and possibly returns). If we include only the names, you can't read the function literal without going to look up the type definition to find out what the names correspond to. If you don't include the names, what are they? I suppose you could require that the names be present in the type, and use them, but that makes it even WORSE to try to read a function literal. Consider:

type Copier func(w Writer, r Reader) (bucky error)

[...]

// who the heck is bucky? (or w and r)
fnWhichTakesCopier(Copier { bucky = io.Copy(w, r) })

At the expense of making everything even harder to read, you could use explicit conversion:

fnWhichTakesCopier(Copier(func (w Writer, r Reader) error { return io.Copy(w, r) }))

but that seems awkward, even though it's exactly what we do for cases where we're passing in a typed value to something that wants a defined type.

Obviously, when struggling with a language design choice, the right place to look is the C preprocessor's macro definition rules. We could just use roughly the cpp macro redefinition rule of "you're allowed to provide that definition again, but it has to be identical":

fnWhichTakesCopier(Copier(w Writer, r Reader) error { ... })

and state that this is permitted only if the signature actually matches. So, once a function type name exists, that name can be used as a synonym for func, with the requirement that the signature must actually match the type's definition. This isn't too much worse than the general form of:

fnWhichTakesDefinedType(DefinedType{...})

@iamgoroot
Copy link
Author

iamgoroot commented Apr 16, 2019

If you don't include the names, what are they?

Given your last example with Copier you just need to go one step further and get in/out parameter type from definition but leave naming to the developer. This takes away most of the "repetitive" code

fnWhichTakesCopier(Copier(w, r) { ... })

If you are concerned about not knowing those types consider that this is how easy it is now:

Screen Shot 2019-03-19 at 8 53 20 PM

@seebs
Copy link
Contributor

seebs commented Apr 16, 2019

I think my concern is mostly that if you don't include the names, we have no clue what the parameters are named -- do we inherit them from the type? What if the type didn't name them? And if we include just the names, with no types, we have to go look at the type definition for the function type to find out what the parameters are. I'd sort of prefer to just accept that I'm redeclaring the parameters, but that way I can read the code without having to bounce around.

@iamgoroot
Copy link
Author

I think my concern is mostly that if you don't include the names, we have no clue what the parameters are named -- do we inherit them from the type? What if the type didn't name them? And if we include just the names, with no types, we have to go look at the type definition for the function type to find out what the parameters are. I'd sort of prefer to just accept that I'm redeclaring the parameters, but that way I can read the code without having to bounce around.

We include the name.

type Copier func(Writer, Reader) error
fnWhichTakesCopier(Copier(w, r) { return nil })

Type for parameter as for any other variable probably can be looked up in IDE. As example we don't redeclare field types while making a struct. We just know what they are. You have to know the types to write that function in full syntax as well.

Same syntax will do for single-method interfaces. But that's harder since we'll have to distinguish single-method and multi-method interfaces. The 'type' keyword and functions is an attempt to avoid that.

@ianlancetaylor
Copy link
Contributor

Under this proposal, how should we handle the names of result parameters?

@griesemer
Copy link
Contributor

Also, can this be used in the declaration of regular functions?

@seebs
Copy link
Contributor

seebs commented Apr 16, 2019

I don't know, but I would like to be able to declare regular functions that way, because then users of pkg/plugin can take advantage of the distinction between "a function with the same underlying type I expect" and "a function of this specific defined type".

@iamgoroot
Copy link
Author

Under this proposal, how should we handle the names of result parameters?

Do we need to? "reflect" package doesn't seem to have any support for it.

@iamgoroot
Copy link
Author

iamgoroot commented Apr 16, 2019

Also, can this be used in the declaration of regular functions?

I would rather not do that. This will make things complicated since you need to put actual function name into syntax. Also we probably love current syntax for regular functions as it is.
But there's always another way:

type Copier func(Writer, Reader) error
var CopierFunc = Copier(w, r) { return nil }

@seebs
Copy link
Contributor

seebs commented Apr 16, 2019

That turns out not to be the same, though! CopierFunc is now a variable referring to a function, not a function. Consider what you get from plugin.Lookup(...) for the name.

@iamgoroot
Copy link
Author

That turns out not to be the same, though! CopierFunc is now a variable referring to a function, not a function. Consider what you get from plugin.Lookup(...) for the name.

I can't quite see why would you want to abstract away any of the top level function declaration as those are mostly exported and represent type for interaction.
That's why I started with "I would rather not do that".
This proposal is to allow user to declare type of anonymous function that can be constructed in a particular way.

@ianlancetaylor
Copy link
Contributor

@iamgoroot The reflect package doesn't support names of local parameters either. But in some cases you need a name of a result parameter in order to change it in a deferred function.

That said, of course people could use the existing function declaration syntax for that case. But not permitting result parameter names does seem like an odd limitation.

@seebs
Copy link
Contributor

seebs commented Apr 17, 2019

I wouldn't want to "abstract away" the top-level function declarations, but I do want a way to declare a top level function of a defined type. So it may not be part of your use case, but it's part of a related use case, and your proposal is the closest I've seen to something that would address mine.

@iamgoroot
Copy link
Author

@iamgoroot The reflect package doesn't support names of local parameters either. But in some cases you need a name of a result parameter in order to change it in a deferred function.

That said, of course people could use the existing function declaration syntax for that case. But not permitting result parameter names does seem like an odd limitation.

My bad. Thanks for pointing that out. Then we could get away with same approach - use the names and reuse the types. But this should probably be optional if needed at all.
It's not to change how we define functions. It's to give a shorthand for things we use all the time and let some functional stuff to go on.

type Copier func(w Writer, r Reader) (bucky error)
fnWhichTakesCopier(Copier(w, r) bucky { bucky = io.Copy(w, r) })


type MutiRet func(s string, i int) (res string, err error)
fnWhichTakesCopier(MutiRet(s, i) (res, err) { 
res = "some res"
err = nil
})

@ianlancetaylor
Copy link
Contributor

Using an expression like exec(transform(s) { return "" }) is problematic because transform(s) looks like a function call. In this proposal it is nothing like a function call; it specifies the type of a function, and, further, it declare a new local variable/parameter s. I do not think that we want to have something that looks like a function call actually declare a new name. In Go names are declared by explicit declarations, using var or := (or type, func, const). We could consider adding a new way to declare names, but it shouldn't be something that looks exactly like a function call. That seems too confusing.

Without a more persuasive notation we aren't going to adopt this proposal. Closing, but please free to comment, or open a new issue, if you have a different approach.

@golang golang locked and limited conversation to collaborators Apr 29, 2020
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