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: fix inconsistent syntax with function literal declarations #27451

Closed
l0k18 opened this issue Sep 2, 2018 · 13 comments
Closed

Proposal: fix inconsistent syntax with function literal declarations #27451

l0k18 opened this issue Sep 2, 2018 · 13 comments
Labels
FrozenDueToAge LanguageChange Proposal v2 A language change or incompatible library change
Milestone

Comments

@l0k18
Copy link

l0k18 commented Sep 2, 2018

In go, composite literals all follow a rule: if you define it as a type with a type declaration, you can create a literal like this:

type Array []interface{}
type Map map[string]interface{}
var ExampleArray = Array{ 1, "two", struct{ int }{ 3 }}
var ExampleMap = Map{  "1": 1, "2", "two", "3" struct { int }{ 3 }}

But if I do it with a function variable I have to repeat the type specification and the type definition is effectively a no-op because there is no aliasing for function signatures:

type fn func()
var f1 = func() { fmt.Println("Hello from inside a closure") }
func main() {
    f1()	
}

If the function declaration was consistent with other composite literals it would greatly encourage the use of them, as for example a package can declare a type fn func() and write closures like this:

doif(condition, fn{ fmt.Println("true") }, { fmt.Println("false") })

Or line structured, if you like it better:

doif(condition, fn{
    fmt.Println("true") 
}, { 
    fmt.Println("false") 
})

This is a change that would be backwards compatible because it is only adding the ability to use types as type signature aliases for func types, and would cause no changes to how old code works.

@gopherbot gopherbot added this to the Proposal milestone Sep 2, 2018
@mvdan
Copy link
Member

mvdan commented Sep 2, 2018

I'm not sure how this would work syntactically. Right now, the expression name{ is always followed by a list of fields - be it slice elements, key-value expressions for maps, etc.

But if we do what you suggest, name{ could also be followed by any number of statements. I can't see how the parser could deal with that, as it doesn't have type information.

cc @griesemer @ianlancetaylor

@mvdan mvdan added LanguageChange v2 A language change or incompatible library change labels Sep 2, 2018
@cznic
Copy link
Contributor

cznic commented Sep 3, 2018

But if I do it with a function variable I have to repeat the type specification and the type definition is effectively a no-op because there is no aliasing for function signatures:

type fn func()
var f1 = func() { fmt.Println("Hello from inside a closure") }
func main() {
f1()
}

There's no 'repeating the type specification', because f1 is not of type fn and thus there's also no inconsistency. A literal of type fn cannot be written, one has to convert as in var f2 = fn(func() {}), or var f3 = fn(f1) or use the assignability rule of anonymous type as in var f4 fn = func() {}

Example: https://play.golang.org/p/Kkydsdu_j0P

@l0k18
Copy link
Author

l0k18 commented Sep 3, 2018

The short form for custom typed literals is not just one type of list, there is the array, which has a comma after each element, there is map with key:pair. Yes, the syntax requires comma for more than one item but statements terminate at the end of line or open of a bracket, they use semicolons, implicit in the newlines.

My point is that when I specify a type name for a specific map signature, I don't have to type the signature every time I declare a new map of this given type. A map signature is also in two parts like a function signature, and already the parser can differentiate between a simple alias type Bytes []byte and Bytes{128, 0, 255), type ErrorString string and var Name ErrorString but more important is that syntactically a map and a function share the same taxonomy, just with different markers: map[type]type versus func(type, type...)(type, type...). I mean, you could theoretically define a map with a compound type like struct and of course you can do a map of maps `map[type]map[type]type' though that is not a comma separated list it is a map() separated list.

Making a variable name for a function is not the same as a type alias for any other type, it is an inconsistent rule. If it made it simpler to parse, maybe the function brackets could still be required but at least you can use a shorter thing than the whole damned type signature every time you want to make a closure.

I mean, consider the definition of what a closure is - implicitly it does not really have to declare a return value because it shares namespace. This is a limitation with using closures, but also the benefit. But a closure can return a value as well, but to use it this way you have to type the type spec every damn time. It hobbles the closure. First class functions is great but such an unweildy selector requirement makes closures painful to use unless you only want a func(). Being able to return values out of a closure gives you a lot more options when using them particularly for chaining return values and passing them back from functions containing them.

@networkimprov
Copy link

I suggest you propose a different syntax than FuncType{...}

@ianlancetaylor
Copy link
Contributor

ianlancetaylor commented Sep 3, 2018

If I understand correctly, you are suggesting that this should be valid:

type fn func(x int) (r int)
var x, r string
var vfn = fn{ if x != 0 { r = 2 }; return }

I think the confusion about what x and r refer to in the function literal body is troubling.

@l0k18
Copy link
Author

l0k18 commented Sep 3, 2018

In that example it would refer to the outermost scope boundary, the package in that case. It is possible to write go programs that use package scope like 'globals' for some types of central node for a storage graph also.

The ambiguity that this adds is when the parser encounters a type symbol that is defined in the root of the file/package (depending on capitalisation) is that where it was before 'map or slice', this would just add a third condition 'function'. Likely most of the time someone would only define a couple that are frequently used. func() {} is not that noisy but func(i int) bool {} is a lot of preamble if you use it a lot.

@ianlancetaylor
Copy link
Contributor

Maybe I'm misunderstanding you, but it sounds like you are saying that in my example above x and r refer to the package scope variables x and r, meaning that my example will not compile. If that is the case, how can the function literal refer to the function's parameters?

@l0k18
Copy link
Author

l0k18 commented Sep 4, 2018

I would think normal scoping rules apply - the x from the parameter shadows the parent block scope, same as like other control blocks, in fact, basically closures follow the same scoping rules as for, if, switch and whatever other blocks I forgot that live inside functions, as all variable declarations do when inside a parentheses.

I think that the variable names of the parameters and number should match the prototype, and that the () should be there always after the name. Just to not have to repeat the type spec over, and returns can be anonymous, but must match the prototype, same as regular functions.

Oh, also, it totally slipped past me - those variable names in the type definition can be there but they do absolutely nothing and distinguish no ambiguities, in fact you are just making them for yourself by using them for no reason. I can sorta see why one might name interface parameter names, if there was anything unclear about it, since it can act like documentation, but as far as I can tell the names do nothing at all to the logic or the compilation except in a function scope where it declares a named variable (and you can't mix unnamed and named in one call either).

@griesemer
Copy link
Contributor

@l0k1verloren I think your suggestion is an interesting idea, and I'm pretty sure it has come up before (I've mentioned it in discussions myself, in the past). To make sure we're all on the same page, if I understand you correctly, what you're suggesting is that function types can be used more regularly. For example, right now we can write:

type T = []int
var _ = []int{1, 2, 3} // slice literal of type []int
var _ = T{1, 2, 3} // slice literal of type []int

but this doesn't work symmetrically:

type F = func(x int) float64
var _ = func(x int) float64 { return math.Sqrt(float64(x)) } // ok
var _ = F{ return math.Sqrt(float64(x)) } // not ok

If it were ok, it could shorten function literals (see example above), but also common function declarations like, perhaps:

func f F {
   return math.Sqrt(float64(x))
}

(As an aside, @cznic, in these examples the types of the functions and the type of F are indeed identical because we use alias declarations for the types.)

There are a couple of problems with this suggestion, both of which have been brought up already by @mvdan and @ianlancetaylor , respectively:

  1. Syntactically, when the parser sees a name followed by an open curly brace, as in F { it assumes what follows is a list of expressions or key:value pairs. It doesn't accept a list of statements (as it would have to, to recognize a function body). It's not trivial to fix, and the parser doesn't have type information at that time (it cannot know that F is a function - keep in mind that the F might be defined elsewhere, perhaps in a file that hasn't even been read yet). One would have to address that in the syntax somehow. For instance one could require the func keyword for functions. This would be not as short as you would like, but it would still allow things like:
var _ = func F { ... }

but it would also be more confusing. Alternatively, one might use a different notation for non-function literal bodies (no curly braces). But that would be not backward compatible. Or maybe one would have to write the () for function literals always as in:

var _ = F() { ... }

but that can't work because F() looks like a function call which cannot be followed by {, and so forth. There's multitudes of ways one might be able to address the syntactic issues, but finding a satisfying solution is not easy and one may end up with not much savings in terms of code size.

  1. There is a question as to the parameter name scope. Basically, in the examples above, functions of type F have a parameter x that is in scope in the function body, but it's not written down with the function closure (or declaration), but with the type. One could make that work in the compiler but it is at best unusual for Go, and at worst poses challenges for readability. For instance, changing the parameter names in the function type (which might be defined far away from the actual use of the function type) might change or invalidate the meaning of the function body.

Both of these problems are real.

@bcmills
Copy link
Contributor

bcmills commented Sep 6, 2018

There is a question as to the parameter name scope.

I think we could resolve that by retaining the parameter list, while allowing the types themselves to be elided:

type F = func(x int) float64
var _ = F(x) { return math.Sqrt(float64(x)) }
func f F(x) {
   return math.Sqrt(float64(x))
}

However, that could lead to parsing difficulties. (When we see F(x) on line 2, we have to look ahead to the curly-brace to know whether it is a type conversion or a function literal.)

@bcmills
Copy link
Contributor

bcmills commented Sep 6, 2018

See also #21498.

@networkimprov
Copy link

networkimprov commented Sep 7, 2018

[Edited to add func]

type F func(int) float64
var f F
f = F {      (i) (o) { o = math.Sqrt(float64(i)); return } } // (a, b, ...) when req'd
f = F {      (i) o   { o = math.Sqrt(float64(i)); return } } // single return value
f = F { func (i) o   { o = math.Sqrt(float64(i)); return } } // +func for explicit context?

@ianlancetaylor
Copy link
Contributor

This is an interesting idea but as discussed above there are problems that currently have no good solutions. Closing this proposal; we can reconsider if good solutions are proposed.

@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