[edit: As of today, end of May 2025, this proposal has received ~1000 comments. Please read the summary before adding more comments. Your ideas may have been discussed already. Thanks.]
[edit: As of June 4, 2024, a small group of commenters has come to a narrow conclusion on a workable syntax. In order to avoid going in circles, please consider the preceding comments before suggesting alternatives. Most likely, they have been discussed already.]
Many languages provide a lightweight syntax for specifying anonymous functions, in which the function type is derived from the surrounding context.
Consider a slightly contrived example from the Go tour (https://tour.golang.org/moretypes/24):
func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}
var _ = compute(func(a, b float64) float64 { return a + b })
Many languages permit eliding the parameter and return types of the anonymous function in this case, since they may be derived from the context. For example:
// Scala
compute((x: Double, y: Double) => x + y)
compute((x, y) => x + y) // Parameter types elided.
compute(_ + _) // Or even shorter.
// Rust
compute(|x: f64, y: f64| -> f64 { x + y })
compute(|x, y| { x + y }) // Parameter and return types elided.
I propose considering adding such a form to Go 2. I am not proposing any specific syntax. In terms of the language specification, this may be thought of as a form of untyped function literal that is assignable to any compatible variable of function type. Literals of this form would have no default type and could not be used on the right hand side of a :=
in the same way that x := nil
is an error.
Uses 1: Cap'n Proto
Remote calls using Cap'n Proto take an function parameter which is passed a request message to populate. From https://github.com/capnproto/go-capnproto2/wiki/Getting-Started:
s.Write(ctx, func(p hashes.Hash_write_Params) error {
err := p.SetData([]byte("Hello, "))
return err
})
Using the Rust syntax (just as an example):
s.Write(ctx, |p| {
err := p.SetData([]byte("Hello, "))
return err
})
Uses 2: errgroup
The errgroup package (http://godoc.org/golang.org/x/sync/errgroup) manages a group of goroutines:
g.Go(func() error {
// perform work
return nil
})
Using the Scala syntax:
g.Go(() => {
// perform work
return nil
})
(Since the function signature is quite small in this case, this might arguably be a case where the lightweight syntax is less clear.)
Comment From: griesemer
I'm sympathetic to the general idea, but I find the specific examples given not very convincing: The relatively small savings in terms of syntax doesn't seem worth the trouble. But perhaps there are better examples or more convincing notation.
(Perhaps with the exception of the binary operator example, but I'm not sure how common that case is in typical Go code.)
Comment From: davecheney
Please no, clear is better than clever. I find these shortcut syntaxes impossibly obtuse.
On Fri, 18 Aug 2017, 04:43 Robert Griesemer notifications@github.com wrote:
I'm sympathetic to the general idea, but I find the specific examples given not very convincing: The relatively small savings in terms of syntax doesn't seem worth the trouble. But perhaps there are better examples or more convincing notation.
— You are receiving this because you are subscribed to this thread. Reply to this email directly, view it on GitHub https://github.com/golang/go/issues/21498#issuecomment-323159706, or mute the thread https://github.com/notifications/unsubscribe-auth/AAAcAxlgwt-iPryyY-d5w8GJho0bY9bkks5sZInfgaJpZM4O6pBB .
Comment From: ianlancetaylor
I think this is more convincing if we restrict its use to cases where the function body is a simple expression. If we are required to write a block and an explicit return
, the benefits are somewhat lost.
Your examples then become
s.Write(ctx, p => p.SetData([]byte("Hello, "))
g.Go(=> nil)
The syntax is something like
[ Identifier ] | "(" IdentifierList ")" "=>" ExpressionList
This may only be used in an assignment to a value of function type (including assignment to a parameter in the process of a function call). The number of identifiers must match the number of parameters of the function type, and the function type determines the identifier types. The function type must have zero results, or the number of result parameters must match the number of expressions in the list. The type of each expression must be assignable to the type of the corresponding result parameter. This is equivalent to a function literal in the obvious way.
There is probably a parsing ambiguity here. It would also be interesting to consider the syntax
λ [Identifier] | "(" IdentifierList ")" "." ExpressionList
as in
s.Write(ctx, λp.p.SetData([]byte("Hello, "))
Comment From: neild
A few more cases where closures are commonly used.
(I'm mainly trying to collect use cases at the moment to provide evidence for/against the utility of this feature.)
Comment From: faiface
I actually like that Go doesn't discriminate longer anonymous functions, as Java does.
In Java, a short anonymous function, a lambda, is nice and short, while a longer one is verbose and ugly compared to the short one. I've even seen a talk/post somewhere (I can't find it now) that encouraged only using one-line lambdas in Java, because those have all those non-verbosity advantages.
In Go, we don't have this problem, both short and longer anonymous functions are relatively (but not too much) verbose, so there is no mental obstacle to using longer ones too, which is sometimes very useful.
Comment From: jimmyfrasche
The shorthand is natural in functional languages because everything is an expression and the result of a function is the last expression in the function's definition.
Having a shorthand is nice so other languages where the above doesn't hold have adopted it.
But in my experience it's never as nice when it hits the reality of a language with statements.
It's either nearly as verbose because you need blocks and returns or it can only contain expressions so it's basically useless for all but the simplest of things.
Anonymous functions in Go are about as close as they can get to optimal. I don't see the value in shaving it down any further.
Comment From: bcmills
It's not the func
syntax that is the problem, it's the redundant type declarations.
Simply allowing the function literals to elide unambiguous types would go a long way. To use the Cap'n'Proto example:
s.Write(ctx, func(p) error { return p.SetData([]byte("Hello, ")) })
Comment From: neild
Yes, it's the type declarations that really add noise. Unfortunately, "func (p) error" already has a meaning. Perhaps permitting _ to substitute in for an inferenced type would work?
s.Write(ctx, func(p _) _ { return p.SetData([]byte("Hello, ")) })
I rather like that; no syntactic change at all required.
Comment From: martisch
I do not like the stutter of _. Maybe func could be replaced by a keyword that infers the type parameters:
s.Write(ctx, λ(p) { return p.SetData([]byte("Hello, ")) })
Comment From: davecheney
Is this actually a proposal or are you just spitballing what Go would look like if you dressed it like Scheme for Halloween? I think this proposal is both unnecessary and in poor keeping with the language's focus on readability.
Please stop trying to change the syntax of the language just because it looks different to other languages.
Comment From: cespare
I think that having a concise anonymous function syntax is more compelling in other languages that rely more on callback-based APIs. In Go, I'm not sure the new syntax would really pay for itself. It's not that there aren't plenty of examples where folks use anonymous functions, but at least in the code I read and write the frequency is fairly low.
Comment From: bcmills
I think that having a concise anonymous function syntax is more compelling in other languages that rely more on callback-based APIs.
To some extent, that is a self-reinforcing condition: if it were easier to write concise functions in Go, we may well see more functional-style APIs. (Whether that is a good thing or not, I do not know.)
I do want to emphasize that there is a difference between "functional" and "callback" APIs: when I hear "callback" I think "asynchronous callback", which leads to a sort of spaghetti code that we've been fortunate to avoid in Go. Synchronous APIs (such as filepath.Walk
or strings.TrimFunc
) are probably the use-case we should have in mind, since those mesh better with the synchronous style of Go programs in general.
Comment From: dimitropoulos
I would just like to chime in here and offer a use case where I have come to appreciate the arrow
style lambda syntax to greatly reduces friction: currying.
consider:
// current syntax
func add(a int) func(int) int {
return func(b int) int {
return a + b
}
}
// arrow version (draft syntax, of course)
add := (a int) => (b int) => a + b
func main() {
add2 := add(2)
add3 := add(3)
fmt.Println(add2(5), add3(6))
}
Now imagine we are trying to curry a value into a mongo.FieldConvertFunc
or something which requires a functional approach, and you'll see that having a more lightweight syntax can improve things quite a bit when switching a function from not being curried to being curried (happy to provide a more real-world example if anyone wants).
Not convinced? Didn't think so. I love go's simplicity too and think it's worth protecting.
Another situation that happens to me a lot is where you have and you want to now curry the next argument with currying.
now you would have to change
func (a, b) x
to
func (a) func(b) x { return func (b) { return ...... x } }
If there was an arrow syntax you would simply change
(a, b) => x
to
(a) => (b) => x
Comment From: myitcv
@neild whilst I haven't contributed to this thread yet, I do have another use case that would benefit from something similar to what you proposed.
But this comment is actually about another way of dealing with the verbosity in calling code: have a tool like gocode
(or similar) template a function value for you.
Taking your example:
func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}
If we assume we had typed:
var _ = compute(
^
with the cursor at the position shown by the ^
; then invoking such a tool could trivially template a function value for you giving:
var _ = compute(func(a, b float64) float64 { })
^
That would certainly cover the use case I had in mind; does it cover yours?
Comment From: neild
Code is read much more often than it is written. I don't believe saving a little typing is worth a change to the language syntax here. The advantage, if there is one, would largely be in making code more readable. Editor support won't help with that.
A question, of course, is whether removing the full type information from an anonymous function helps or harms readability.
Comment From: mrkaspa
I don't think this kind of syntax reduces readability, almost all modern programming languages have a syntax for this and thats because it encourages the use of functional style to reduce the boilerplate and make the code clearer and easier to maintain. It's a great pain to use anonymous functions in golang when they are passed as parameters to functions because you have to repeat yourself typing again the types that you know you must pass.
Comment From: hooluupog
I support the proposal. It saves typing and helps readability.My use case,
// Type definitions and functions implementation.
type intSlice []int
func (is intSlice) Filter(f func(int) bool) intSlice { ... }
func (is intSlice) Map(f func(int) int) intSlice { ... }
func (is intSlice) Reduce(f func(int, int) int) int { ... }
list := []int{...}
is := intSlice(list)
without lightweight anonymous function syntax:
res := is.Map(func(i int)int{return i+1}).Filter(func(i int) bool { return i % 2 == 0 }).
Reduce(func(a, b int) int { return a + b })
with lightweight anonymous function syntax:
res := is.Map((i) => i+1).Filter((i)=>i % 2 == 0).Reduce((a,b)=>a+b)
Comment From: firelizzard18
The lack of concise anonymous function expressions makes Go less readable and violates the DRY principle. I would like to write and use functional/callback APIs, but using such APIs is obnoxiously verbose, as every API call must either use an already defined function or an anonymous function expression that repeats type information that should be quite clear from the context (if the API is designed correctly).
My desire for this proposal is not even remotely that I think Go should look or be like other languages. My desire is entirely driven by my dislike for repeating myself and including unnecessary syntactic noise.
Comment From: griesemer
In Go, the syntax for function declarations deviates a bit from the regular pattern that we have for other declarations. For constants, types, variables we always have:
keyword name type value
For example:
const c int = 0
type t foo
var v bool = true
In general, the type can be a literal type, or it can be a name. For functions this breaks down, the type always must be a literal signature. One could image something like:
type BinaryOp func(x, y Value) Value
func f BinaryOp { ... }
where the function type is given as a name. Expanding a bit, a BinaryOp closure could then perhaps be written as
BinaryOp{ return x.Add(y) }
which might go a long way to shorter closure notation. For instance:
vector.Apply(BinaryOp{ return x.Add(y) })
The main disadvantage is that parameter names are not declared with the function. Using the function type brings them "in scope", similar to how using a struct value x
of type S
brings a field f
into scope in a selector expression x.f
or a struct literal S{f: "foo"}
.
Also, this requires an explicitly declared function type, which may only make sense if that type is very common.
Just another perspective for this discussion.
Comment From: dimitropoulos
Readability comes first, that seems to be something we can all agree on.
But that said, one thing I want to also chime in on (since it doesn't look like anyone else said it explicitly) is that the question of readability is always going to hinge on what you're used to. Having a discussion as we are about whether it hurts or harms readability isn't going to get anywhere in my opinion.
@griesemer perhaps some perspective from your time working on V8 would be useful here. I (at least) can say I was very much happy with javascript's prior syntax for functions (function(x) { return x; }
) which was (in a way) even heavier to read than Go's is right now. I was in @douglascrockford's "this new syntax is a waste of time" camp.
But, all the same, the arrow syntax happened and I accepted it because I had to. Today, though, having used it a lot more and gotten more comfortable with it, I can say that it helps readability tremendously. I used the case of currying (and @hooluupog brought up a similar case of "dot-chaining") where a lightweight syntax produces code that is lightweight without being overly clever.
Now when I see code that does things like x => y => z => ...
and it is much easier to understand at a glance (again... because I'm familiar with it. not all that long ago I felt quite the opposite).
What I'm saying is: this discussion boils down to: 1. When you aren't used to it, it seems really strange and borderline useless if not harmful to readability. Some people just have or don't have a feeling one way or another on this. 2. The more functional programming you're doing, the more the need for such a syntax pronounces itself. I would guess that this has something to do with functional concepts (like partial application and currying) that introduce a lot of functions for tiny jobs which translates to noise for the reader.
The best thing we can do is provide more use-cases.
Comment From: firelizzard18
In response to @dimitropoulos's comment, here's a rough summary of my view:
I want to use design patterns (such as functional programming) that would greatly benefit from this proposal, as their use with the current syntax is excessively verbose.
Comment From: griesemer
@dimitropoulos I've been working on V8 alright, but that was building the virtual machine, which was written in C++. My experience with actual Javascript is limited. That said, Javascript is a dynamically typed language, and without types much of the typing goes away. As several people have brought up before, a major issue here is the need to repeat types, a problem that doesn't exist in Javascript.
Also, for the record: In the early days of designing Go we actually looked at arrow syntax for function signatures. I don't remember the details but I'm pretty sure notation such as
func f (x int) -> float32
was on the white board. Eventually we dropped the arrow because it didn't work that well with multiple (non-tuple) return values; and once the func
and the parameters where present, the arrow was superfluous; perhaps "pretty" (as in mathematically looking), but still superfluous. It also seemed like syntax that belonged to a "different" kind of language.
But having closures in a performant, general purpose language opened the doors to new, more functional programming styles. Now, 10 years down the road, one might look at it from a different angle.
Still, I think we have to be very careful here to not create special syntax for closures. What we have now is simple and regular and has worked well so far. Whatever the approach, if there's any change, I believe it will need to be regular and apply to any function.
Comment From: bcmills
In Go, the syntax for function declarations deviates a bit from the regular pattern that we have for other declarations. For constants, types, variables we always have:
keyword name type value
[…] For functions this breaks down, the type always must be a literal signature.
Note that for parameter lists and const
and var
declarations we have a similar pattern, IdentifierList Type
, which we should probably also preserve. That seems like it would rule out the lambda-calculus-style :
token to separate variable names from types.
Whatever the approach, if there's any change, I believe it will need to be regular and apply to any function.
The keyword name type value
pattern is for declarations, but the use-cases that @neild mentions are all for literals.
If we address the problem of literals, then I believe the problem of declarations becomes trivial. For declarations of constants, variables, and now types, we allow (or require) an =
token before the value
. It seems like it would be easy enough to extend that to functions:
FunctionDecl = "func" ( FunctionSpec | "(" { FunctionSpec ";" } ")" ).
FunctionSpec = FunctionName Function |
IdentifierList (Signature | [ Signature ] "=" Expression) .
FunctionLit = "func" Function | ShortFunctionLit .
ShortParameterList = ShortParameterDecl { "," ShortParameterDecl } .
ShortParameterDecl = IdentifierList [ "..." ] [ Type ] .
The expression after the =
token must be a function literal, or perhaps a function returned by a call whose arguments are all available at compile time. In the =
form, a Signature
could still be supplied to move the argument type declarations from the literal to the FunctionSpec
.
Note that the difference between a ShortParameterDecl
and the existing ParameterDecl
is that singleton IdentifierList
s are interpreted as parameter names instead of types.
Examples
Consider this function declaration accepted today:
func compute(f func(x, y float64) float64) float64 { return f(3, 4) }
We could either retain that (e.g. for Go 1 compatibility) in addition to the examples below, or eliminate the Function
production and use only the ShortFunctionLit
version.
For various ShortFunctionLit
options, the grammar I propose above gives:
Rust-like:
ShortFunctionLit = "|" ShortParameterList "|" Block .
Admits any of:
func compute = |f func(x, y float64) float64| { f(3, 4) }
func compute(func (x, y float64) float64) float64 = |f| { f(3, 4) }
func (
compute = |f func(x, y float64) float64| { f(3, 4) }
)
func (
compute(func (x, y float64) float64) float64 = |f| { f(3, 4) }
)
Scala-like:
ShortFunctionLit = "(" ShortParameterList ")" "=>" Expression .
Admits any of:
func compute = (f func(x, y float64) float64) => f(3, 4)
func compute(func (x, y float64) float64) float64 = (f) => f(3, 4)
func (
compute = (f func(x, y float64) float64) => f(3, 4)
)
func (
compute(func (x, y float64) float64) float64 = (f) => f(3, 4)
)
Lambda-calculus-like:
ShortFunctionLit = "λ" ShortParameterList "." Expression .
Admits any of:
func compute = λf func(x, y float64) float64.f(3, 4)
func compute(func (x, y float64) float64) float64) = λf.f(3, 4)
func (
compute = λf func(x, y float64) float64.f(3, 4)
)
func (
compute(func (x, y float64) float64) float64) = λf.f(3, 4)
)
Haskell-like:
ShortFunctionLit = "\" ShortParameterList "->" Expression .
func compute = \f func(x, y float64) float64 -> f(3, 4)
func compute(func (x, y float64) float64) float64) = \f -> f(3, 4)
func (
compute = \f func(x, y float64) float64 -> f(3, 4)
)
func (
compute(func (x, y float64) float64) float64) = \f -> f(3, 4)
)
C++-like: (Probably not feasible due to ambiguity with array literals, but maybe worth considering.)
ShortFunctionLit = "[" ShortParameterList "]" Block .
Admits any of:
func compute = [f func(x, y float64) float64] { f(3, 4) }
func compute(func (x, y float64) float64) float64) = [f] { f(3, 4) }
func (
compute = [f func(x, y float64) float64] { f(3, 4) }
)
func (
compute(func (x, y float64) float64) float64) = [f] { f(3, 4) }
)
Personally, I find all but the Scala-like variants to be fairly legible. (To my eye, the Scala-like variant is too heavy on parentheses: it makes the lines much more difficult to scan.)
Comment From: ianlancetaylor
Personally I'm mainly interested in this if it lets me omit the parameter and result types when they can be inferred. I'm even fine with the current function literal syntax if I can do that. (This was discussed above.)
Admittedly this goes against @griesemer 's comment.
Comment From: neild
Whatever the approach, if there's any change, I believe it will need to be regular and apply to any function.
I don't quite follow this. Function declarations necessarily must include the full type information for the function, since there's no way to derive it with sufficient precision from the function body. (This isn't the case for all languages, of course, but it is for Go.)
Function literals, in contrast, could infer type information from context.
Comment From: griesemer
@neild Apologies for being imprecise: What I meant this sentence is that if there were new different syntax (arrows or what have you), it should be somewhat regular and apply everywhere. If it's possible that types can be omitted, that would be again orthogonal.
Comment From: neild
@griesemer Thanks; I (mostly) agree with that point.
I think the interesting question for this proposal is whether having some syntax is a good idea or not; what that syntax would be is important but relatively trivial.
However, I can't resist the temptation to bikeshed my own proposal a bit.
var sum func(int, int) int = func a, b { return a + b }
Comment From: firelizzard18
@neild's proposal feels right to me. It's pretty close to the existing syntax, but works for functional programming as it eliminates the repetition of the type specifications. It's not that much less compact than (a, b) => a + b
, and it fits well into the existing syntax.
Comment From: bcmills
@neild
go var sum func(int, int) int = func a, b { return a + b }
Would that declare a variable, or a function? If a variable, what would the equivalent function declaration look like?
Under my declaration schema above, if I'm understanding correctly it would be:
ShortFunctionLit = "func" ShortParameterList Block .
func compute = func f func(x, y float64) float64 { return f(3, 4) }
func compute(func (x, y float64) float64) float64 = func f { return f(3, 4) }
func (
compute = func f func(x, y float64) float64 { return f(3, 4) }
)
func (
compute(func (x, y float64) float64) float64 = func f { return f(3, 4) }
)
I don't think I'm a fan: it stutters a bit on func
, and doesn't seem to provide enough of a visual break between the func
token and the parameters that follow.
Comment From: bcmills
Or would you leave out parens from the declaration, rather than assigning to literals?
func compute f func(x, y float64) float64 { return f(3, 4) }
I still don't like the lack of visual break, though...
Comment From: neild
Would that declare a variable, or a function? If a variable, what would the equivalent function declaration look like?
A variable. The equivalent function declaration would presumably be func sum a, b { return a+b }
, but that would be invalid for obvious reasons--you can't elide parameter types from function declarations.
The grammar change I'm thinking of would be something like:
ShortFunctionLit = "func" [ IdentifierList ] [ "..." ] FunctionBody .
A short function literal is distinguished from a regular function literal by omitting the parentheses on the parameter list, defines only the names of the incoming parameters, and does not define the outgoing parameters. The types of the incoming parameters and the types and number of outgoing parameters are derived from the surrounding context.
I don't think there's any need to allow specifying optional parameter types in a short function literal; you just use a regular function literal in that case.
Comment From: griesemer
As @ianlancetaylor pointed out, the light-weight notation really only makes sense when it permits the omission of parameter types because they can be inferred easily. As such, @neild 's suggestion is the best and simplest I've seen so far. The one thing it doesn't permit easily though is a light-weight notation for function literals that want to refer to named result parameters. But perhaps in that case they should use the full notation. (It's just a bit irregular).
We might even be able to parse (x, y) { ... }
as short form for func (x, y T) T { ... }
; though it would require a bit of parser look-ahead, but perhaps not too bad.
Comment From: neild
As an experiment, I modified gofmt to rewrite function literals into the compact syntax and ran it against src/. You can see the results here:
https://github.com/neild/go/commit/2ff18c6352788aa8f8cbe8b5d5d4c73956ca7c6f
I didn't make any attempt to limit this to cases where it makes sense; I just wanted to get a sense for how the compact syntax might play out in practice. I haven't dug through it enough yet to develop any opinions on the results.
Comment From: bcmills
@neild Nice analysis. Some observations:
-
The fraction of cases in which the function literal is bound using
:=
is disappointing, since handling those cases without explicit type annotations would require a more complicated inference algorithm. -
The literals passed to callbacks are easier to read in some cases, but more difficult in others. For example, losing the return-type information for function literals that span many lines is a bit unfortunate, since that also tells the reader whether they're looking at a functional API or an imperative one.
-
The reduction in boilerplate for function literals within slices is substantial.
-
defer
andgo
statements are an interesting case: would we infer the argument types from the arguments actually passed to the function? -
A couple of trailing
...
tokens are missing from the examples.
Comment From: neild
defer
and go
are indeed a quite interesting case.
go func p {
// do something with p
}("parameter")
Would we derive the type of p
from the actual function parameter? This would be quite nice for go
statements, although you can of course achieve much the same effect by just using a closure:
p := "parameter"
go func() {
// do something with p
}()
Comment From: fbnz156
I would totally support this. Frankly I don't care how much it "looks like other languages", I just want a less verbose way to use anonymous functions.
Comment From: networkimprov
EDIT: Borrowing the composite literal syntax...
type F func(int) float64
var f F
f = F { (i) (o) { o = float64(i); return } }
f = F { (i) o { o = float64(i); return } } // single return value
f = F { func (i) o { o = float64(i); return } } // +func for good measure?
Comment From: vp2177
Just an idea: Here is what OP's example would look like with an untyped function literal with Swift's syntax:
compute({ $0 + $1 })
I believe this would have the advantage of being fully backwards compatible with Go 1.
Comment From: UlisseMini
I just found this because i was writing a simple tcp chat app, basically i have a structure with a slice inside it
type connIndex struct {
conns []net.Conn
mu sync.Mutex
}
and i'd like to apply some operations to it concurrently (adding connections, sending messages to all etc)
and instead of following the normal path of copy-pasting the mutex locking code, or using a daemon goroutine to manage access i thought i'd just pass a closure
func (c *connIndex) run(f func([]net.Conn)) {
c.mu.Lock()
defer c.mu.Unlock()
f(c.conns)
}
for short operations its overly verbose (still better then lock
and defer unlock()
)
conns.run(func(conns []net.Conn) { conns = append(conns, conn) })
This violates the DRY principle as i've typed out that exact function signature in the run
method.
If go supported infering the function signature i could write it like this
conns.run(func(conns) { conns = append(conns, conn) })
I don't think this makes the code less readable, you can tell it is a slice because of append
, and because i've named my variables well you can guess it is a []net.Conn without looking at the run
method signature.
I'd avoid trying to infer the types of paramaters based on the function body, instead add inference only for cases where it is obvious (like passing closures to functions).
i'd say this does not harm readibility as it gives the reader an option, if they don't know the type of the paramater they can godef
it or hover over it and get the editor to show it to them.
Sorta like how in a book they don't repeat the characters introduction, except we would have a button to show it / jump to it.
I'm bad at writing so hopefully you survived reading this :)
Comment From: mwmahlberg
I think this is more convincing if we restrict its use to cases where the function body is a simple expression.
I dare to object. This still would lead to two ways of defining a function, and one of the reasons why I fell in love with Go is that while it has some verbosity here and there, it has a refreshing expressiveness: you see where a closure is because there is either a func
keyword or the parameter is a func, if you trace it.
conns.run(func(conns []net.Conn) { conns = append(conns, conn) })
This violates the DRY principle as i've typed out that exact function signature in the run method.
DRY is important, no doubt. But applying it to each and every part of programming for the sake of keeping up the principle at the cost of the ability to understand the code with the smallest amount of effort possible, is a bit overshooting the mark, imho.
I think the general problem here (and a few other proposals) is that the discussion is mostly about how to safe effort writing the code, whereas imho it should be how to safe effort reading the code. Years after one has written it. I have recently come along a poc.pl
of mine and I am still trying to figure out what it does... ;)
conns.run(func(conns) { conns = append(conns, conn) })
I don't think this makes the code less readable, you can tell it is a slice because of append, and because i've named my variables well you can guess it is a []net.Conn without looking at the run method signature.
From my point of view, there are several issues with this statement. I do not know how others see it, but I hate guessing. One might be right, one might be wrong, but surely one has to put effort into it - for the benefit of saving to „type“ []net.Conn
. And the readability as well as the comprehensibility of code should be supported by good variable names, not based on it.
To conclude: I think the focus of the discussion should shift away from how to reduce minor efforts when writing code to how to reduce efforts to comprehend said code.
I close with quoting Dave Cheney quoting Robert Pike (iirc)
Clear is better than clever.
Comment From: muirdm
The tedium of typing out function signatures can be somewhat relieved by auto completion. For example, gopls offers completions that create function literals:
I think this provides a good middle ground where the type names are still in the source code, there remains only one way to define an anonymous function, and you don't have to type out the entire signature.
Comment From: noypi
will this be added or not? ... for those who don't like this feature can still use the old syntax. ... for us who want better simplicity, we can use this new feature hopefully, it's been 1 year since i wrote go, i am not sure if the community still thinks this is important, ... will this be added or not?
Comment From: ianlancetaylor
@noypi No decision has been made. This issue remains open.
https://golang.org/wiki/NoPlusOne
Comment From: charbugs
I back this proposal and I think this feature, in conjunction with generics, would make functional programming in Go more developer friendly.
Here is what I would like to see, roughly:
type F func(int, int) int
// function declaration
f := F (x, y) { return x * y}
// function passing
// g :: func(F)
g((x, y) { return x * y })
// returning function
func h() F {
return (x, y) { return x * y }
}
Comment From: stephanoparaskeva
I'd love to be able to type (a, b) => a * b
and move on.
Comment From: glococo
I can't belive arrow functions are still not available in Go lang. It is amazing how clear and simple is to work with in Javascript.
Comment From: fbnz156
JavaScript can implement this trivially since it doesn't care about the parameters, the number of them, the values, or their types until they're actually used.
Comment From: eliasnaur
Being able to omit types in function literals would help a lot with the functional style I use for the Gio layout API. See the many "func() {...}" literals in https://git.sr.ht/~eliasnaur/gio/tree/master/example/kitchen/kitchen.go? Their actual signature should have been something like
func(gtx layout.Context) layout.Dimensions
but because of the long type names, the gtx
is a pointer to a shared layout.Context
that contains the incoming and outgoing values from each function call.
I'm probably going to switch to the longer signatures regardless of this issue, for clarity and correctness. Nevertheless, I believe my case is a good experience report in support of shorter function literals.
P.S. One reason I'm leaning towards the longer signatures is because they can be shortened by type aliases:
type C = layout.Context
type D = layout.Dimensions
which shortens the literals to func(gtx C) D { ... }
.
A second reason is that the longer signatures are forward compatible with whatever resolves this issue.
Comment From: rogpeppe
I came here with an idea and found that @networkimprov had already suggested something similar here.
I like the idea of using a function type (could also be an unnamed function type or alias) as the specifier for a function literal, because it means that we can use the usual type inference rules for parameters and return values, because we know the exact types in advance. This means that (for example) auto-completion can work as usual and we wouldn't need to introduce funky top-down type-inference rules.
Given:
type F func(a, b int) int
my original thought was:
F(a, b){return a + b}
but that looks too much like a normal function call - it doesn't look like a
and b
are being defined there.
Throwing out other possibilities (I don't like any of them particularly):
F->(a, b){return a + b}
F::(a, b){return a + b}
(a, b := F){ return a + b }
F{a, b}{return a + b}
F{a, b: return a + b}
F{a, b; return a + b}
Perhaps there's some nice syntax lurking around here somewhere :)
Comment From: neild
A key point of composite literal syntax is that it doesn't require type information in the parser. The syntax for structs, arrays, slices, and maps is identical; the parser does not need to know the type of T
to generate a syntax tree for T{...}
.
Another point is that the syntax also does not require backtracking in the parser. When there is ambiguity whether a {
is part of a composite literal or a block, that ambiguity is always resolved in favor of the latter.
I still rather like the syntax I proposed somewhere earlier in this issue, which avoids any parser ambiguity by retaining the func
keyword:
func a, b { return a + b }
Comment From: jimmyfrasche
I removed my :-1:. I'm still not :+1: on it but I am reconsidering my position. Generics are going to cause an increase in short functions like genericSorter(slice, func(a, b T) bool { return a > b })
. I also found https://github.com/golang/go/issues/37739#issuecomment-624338848 compelling.
There are two major ways being discussed for making function literals more concise: 1. a short form for bodies that return an expression 2. eliding the types in function literals.
I think both should be handled separately.
If FunctionBody
is changed to something like
FunctionBody = Block | "->" ExpressionBody
ExpressionBody = Expression | "(" ExpressionList ")"
that would mostly help function literals with or without type elision and would also allow very simple function and method declarations to be lighter on the page:
func (*T) Close() error -> nil
func (e *myErr) Unwrap() error -> e.err
func Alias(x int) -> anotherPackage.OriginalFunc(x)
func Id(type T)(x T) T -> x
func Swap(type T)(x, y T) -> (y, x)
(godoc and friends could still hide the body)
I used @ianlancetaylor's syntax in that example, the major downside of which is that is that it requires the introduction of a new token (and one that would look odd in func(c chan T) -> <-c
!) but it might be okay to reuse an existing token such as "=", if there's no ambiguity. I'll use "=" in the remainder of this post.
For type elision there are two cases 1. something that always works 2. something that only works in a context where the types can be deduced
Using named types like @griesemer suggested would always work. There seem to be some issues with the syntax. I'm sure that could be worked out. Even if they were, I'm not sure it would solve the problem. It would require a proliferation of named types. These would either be in the package defining the place where they're used or they would have to be defined in every package using them.
In the former you get something like
slices.Map(s, slices.MapFunc(x) = math.Abs(x-y))
and in the latter you get something like
type mf func(float64) float64
slices.Map(s, mf(x) = math.Abs(x-y))
Either way there's enough clutter that it doesn't really cut the boilerplate down much unless each name is used a lot.
A syntax like @neild's could only be used when the types could be deduced. A simple method would be like in #12854, just list every context where the type is known—parameter to a function, being assigned to a field, sent on a channel, and so on. The go/defer case @neild brought up seems useful to include, as well.
That approach specifically does not allow the following
zero := func = 0
var f interface{} = func x, y = g(y, x)
but those are cases where it would pay to be more explicit, even if it were possible to infer the type algorithmically by examine where and how those are used.
It does allow many useful cases, including the most useful/requested:
slices.Map(s, func x = math.Abs(x-y))
v := cond(useTls, FetchCertificate, func = nil)
being able to choose to use a block independent of the literal syntax also allows:
http.HandleFunc("/bar", func w, r {
// many lines ...
})
which is a particular case increasingly pushing me toward a :+1:
One question that I haven't seen raised is how to deal with ...
parameters. You could make an argument for either
f(func x, p = len(p))
f(func x, ...p = len(p))
I don't have an answer to that.
Comment From: Splizard
@jimmyfrasche
- eliding the types in function literals.
I believe this should be handled with the addition of function-type literals. Where the type replaces 'func' and the argument types are emitted (as they are defined by the type). This maintains readabillity and is fairly consistent with the literals for other types.
http.Handle("/", http.HandlerFunc[w, r]{
fmt.Fprinf(w, "Hello World")
})
- a short form for bodies that return an expression
Refactor the function as its own type and then things become much cleaner.
type ComputeFunc func(float64, float64) float64
func compute(fn ComputeFunc) float64 {
return fn(3, 4)
}
compute(ComputeFunc[a,b]{return a + b})
If this is too verbose for you, then type alias the function type inside your code.
{
type f = ComputeFunc
compute(f[a,b]{return a + b})
}
In the special case of a function with no arguments, the brackets should be omitted.
type IntReturner func() int
fmt.Println(IntReturner{return 2}())
I pick square brackets because the contracts proposal is already using extra standard brackets for generic functions.
Comment From: jimmyfrasche
@Splizard I stand by argument that that would just push the clutter out of the literal syntax into many extra type definitions. Each such definition would need to be used at least twice before it could be shorter than just writing the types in the literal.
I'm also not sure it would play too well with generics in all cases.
Consider the rather strange function
func X(type T)(v T, func() T)
You could name a generic type to be used with X
:
type XFunc(type T) func() T
If only the definition of XFunc
is used to derive the types of the parameters, when calling X
you'd need to tell it which T
to use even though that's determined by the type of v
:
X(v, XFunc(T)[] { /* ... */ })
There could be a special case for scenarios like this to allow T
to be inferred, but then you'd end up with much of the machinery as would be needed for type elision in func literals.
You could also just define a new type for every T
you call X
with but then there's not much savings unless you call X
many times for each T
.
Comment From: billinghamj
I think this could play really nicely with Go generics too - making slightly more functional styles of programming easy to do.
eg Map(slice, x => x.Foo)
Comment From: beoran
@jimmyfrasche
The short function body syntax you propose by itself would already be extremely useful, seeing that in well factored code, we often have a lot of one-line function bodies. This also seems a lot easier to implement than the type inference for function literal parameters, the proposal could be short, and if accepted, it could be implemented quickly. So perhaps we should split off the short function body syntax as a separate issue?
Comment From: jimmyfrasche
I think they're related enough that they should be considered together, but I'm happy to file a separate proposal if someone from the review team thinks that would be helpful.
I doubt there will be any language changes until generics are in, regardless of how easy they are to implement.
Comment From: egonelbre
One wild idea, maybe quite a few of the lightweight-syntax needs can be handled by adding an implicit accessors for fields.
type Person struct {
Name string
}
// equivalent to `fn := func(p *Person) string { return p.Name }`
fn := (*Person).Name
p := &Person{Name: "Alice"}
fmt.Println(fn(p))
// prints "Alice"
This would mesh nicely with generics:
names := slices.Map(people, (*Person).Name)
This didn't seem large enough idea to make a separate proposal.
Comment From: seh
I assume you meant to write:
// equivalent to `fn := func(p *Person) string { return p.Name }`
That is, dereference p
to reach its "Name" field.
Comment From: gonzojive
Is there a more detailed proposal for the algorithm that derives types? I see
I'd avoid trying to infer the types of paramaters based on the function body, instead add inference only for cases where it is obvious (like passing closures to functions).
I suspect some amount of inference based on the function body is helpful. e.g.
var friends []person
fmt.Printf("my friends' names: %v", functional.Map(friends, (f) => f.name()))
That seems like something programmers would expect to work. It seems the context and function body will both narrow the number and assignment of allowed types for ins/outs. If not, adding partial type information would make life easier:
var friends []person
fmt.Printf("my friends' names: %v", functional.Map(friends, (f): string => f.name()))
Which raises another question: Will types be allowed for some parameters and return values and not others?
Comment From: lukechampine
Might as well mention here that I forked the Go compiler to support extremely concise lambda syntax: https://twitter.com/lukechampine/status/1367279449302007809?s=19
Both parameter and return types are inferred, so lambdas can only be used in contexts where those types can be determined at compile time. For example, this would not compile:
fn := \ x + x
but this would:
var fn func(int) int = \ x + x
The body assumes that parameters are named x
,y
,z
,a
,etc. This is pretty radical and I highly doubt it would be adopted, but it's fun to see how far such things can be pushed.
The fork also supports new builtins methods on slices, which solves the other big ergonomics issue with map/filter/reduce, but that's not relevant to this issue.
Comment From: dolmen
I'm against lambda expressions that would not have the func
keyword.
I like that the func
keyword directly shows me there is a function definition here.
I don't want extremely concise lambda expressions that would hide function definition and reduce readability.
Note: I'm also in favor of adding syntax to reduce the use of short lived functions (anonymous function defined and immediately executed: a function definition followed by a list of arguments in the same expression) because that are cases where the func
keyword is pollution.
Comment From: DmitriyMV
@dolmen I think
func(x,y) => x*y
would fit this definition pretty well. Although I'm unsure about x*y
part without return
, since it would require compiler to infer anonymous function return type (or lack of thereof) from expression directly.
I also pretty sure that any significant syntax changes like this are delayed until generics work is completed. I also pretty sure that we should talk about anonymous function syntax we generics syntax in mind.
Comment From: DeedleFake
Branching off of #47358, the more that I've been looking at the newly proposed generics-based packages for the standard library, the more that I'm convinced that some kind of short-form function syntax is a good idea. Code like
slices.EqualFunc(s1, s2, func(v1, v2 int) bool {
return v1 == v2
})
is already adding a fair amount of bulk just for a single function call, along with coupling the function definition to explicit types that make maintenance more difficult later. More complicated calls can be much, much worse. For example, the discarded slices.Map()
function:
names := slices.Map(make([]string, 0, len(users)), users, func(user User) string { return user.Name })
One thing that I've noticed though is that a good 99% of the situations in which short-form functions are useful are as arguments to function calls. What if there was an alternate syntax for short-form anonymous function declarations that was only available as a function call argument? In other words, function arguments would go from just being any expression to being Expression | ShortAnonymousFunction
, or something, but the syntax wouldn't be legal anywhere else.
Also, while I think that the most important thing for a short-form function is that it have inferred argument types, some way of doing single-expression functions without requiring return would be nice, too.
And, while I'm at it, I may as well bikeshed a bit. I've become quite partial to the Kotlin syntax recently. I think a variant of it could work pretty well for Go, though I'm not exactly stuck on it if syntax disagreements are the primary delay in implementation:
// Single-expression body, so no explicit return is necessary.
slices.EqualFunc(s1, s2, { v1, v2 -> v1 == v2 })
// Body has multiple expressions, so an explicit return is necessary.
slices.EqualFunc(s1, s2, { v1, v2 ->
d := v1 - v2
return (d < 3) && (d > -3)
})
// Earlier map example:
names := slices.Map(make([]string, 0, len(users)), users, { user -> user.Name })
That last example still needs an explicit type at the call-site because of the make()
call, but maybe that could be fixed with something like #34515.
Comment From: ct1n
Maybe
func compute(func(float64, float64) float64)
compute(func(a, b float64) float64 { return a + b })
type G func(int) func(int) int
var g G = func(a int) func(int) int { return func(b int) int { return a + b } }
becomes
func compute(func(float64, float64) float64)
compute(func(a, b): a + b)
type G func(int) func(int) int
var g G = func(a): func(b): a + b
and perhaps also allowing
var h = func(a int): func(b int): a + b
Comment From: zippoxer
@ct1n I really like this syntax for Go!
The func
keyword is consistent with the language and a lot more glance-able than () =>
.
With type inference:
func(a, b): a + b
Comment From: DeedleFake
I like the syntax. It's not my favorite, but I could live with it. I'm particularly not fond of the colon. I'd prefer something like ->
, as it's more visible at a glance.
In my opinion, the biggest reason for this is the type inference, not the expression body, though that's also nice. I'd definitely want a short syntax that supports full function bodies:
func LogHandler(h http.Handler) http.Handler {
return http.HandlerFunc(func(rw, req) -> {
log.Printf("%v %v", req.Method, req.URL)
h.ServeHTTP(rw, req)
})
}
Comment From: ianlancetaylor
I think that if we do something in this area, there is one initial choice to make: whether to keep the func
keyword or not.
If we keep the func
keyword, then I think the most natural syntax is to permit omitting the types of the parameters, omitting the results, and permit omitting the return
keyword if there is only a single expression (or sequence of expressions).
func(a, b) { a + b }
This would be permitted in any assignment where the type of the left hand side is known (this includes function calls).
In this version, the block can contain multiple statements just like an ordinary function literal, but in that case the return
keyword may not be omitted.
If we omit the func
keyword, then I at least think that we need some specific operator. Many languages use the =>
operator for things like this. In that case, we have
(a, b) => a + b
and perhaps
(a int, b float64) => a + int(b)
In this version, the right hand side of =>
can only be an expression.
We could support all of these forms if we really want to.
I think that if we do something here we need to be careful to not stray too far syntactically from what Go permits today. I think that the options here do that.
Comment From: neild
func(a, b) { a + b }
is ambiguous, isn't it? a
and b
could be either types or parameter names. I proposed above that we could resolve that ambiguity by dropping the parentheses in the short form: func a, b { a + b }
.
Comment From: ianlancetaylor
Sigh, you're quite right. Missed that. Thanks.
I'm not too fond of func a, b { a + b }
because of the syntactic difference. Hmmm.
Comment From: jimmyfrasche
If the inferred-type and explicit type syntax are the same it's a bit confusing that (x, y int)
means x
is int
instead of being inferred and someone under that confusion will try to write something like (x, y int, z)
.
Using =>
is fine by me but limiting the short syntax to only an expression is going to end up like Python's lambda:
which is not a popular decision.
I stand by my previous comment https://github.com/golang/go/issues/21498#issuecomment-633140695 that the two things should be kept orthogonal: allow func a, b { return a + b }
and func a, b => a + b
and func(a, b int) int => a + b
and func (Type) Close() error => nil
Comment From: griesemer
If we consider syntactic sugar for simple (single-expression) function literals we should actually make them short and concise and leave away the func
keyword; otherwise, why bother.
Comment From: faiface
@griesemer I think (args...) => result
would be perfect.
(x) => x + 1
(x, y) => x + y
(person) => person.Age >= 18
Comment From: neild
If we consider syntactic sugar for simple (single-expression) function literals we should actually make them short and concise and leave away the func keyword; otherwise, why bother.
It's worth asking whether the goal is to make single-expression function literals extremely concise, or whether it is to permit eliding inferable types from longer literals. For example, the func passed to filepath.Walk
or testing.T.Run
will rarely be a single-expression function.
I worry that syntax that applies only to single-expression functions would interact poorly with Go's explicit error handling. I suspect that languages which uses exception-based error handling have more single-expression functions than Go, which often requires additional error handling states. Providing sugar for single-expression functions might encourage improperly discarding errors to avoid adding additional expressions to a function.
Comment From: griesemer
For example, the func passed to filepath.Walk or testing.T.Run will rarely be a single-expression function.
Agreed. But then what's the point of saving a few keystrokes. The respective functions bodies will dominate the code and in those cases one probably wants to see all the types.
I worry that syntax that applies only to single-expression functions would interact poorly with Go's explicit error handling.
Can you provide a concrete example for this? If a single-expression function returns an error, it's (probably) a multi-valued expression, so it could only be used (if at all) in a suitable assignment. But I can only think of contrived examples.
Comment From: jimmyfrasche
Agreed. But then what's the point of saving a few keystrokes. The respective functions bodies will dominate the code and in those cases one probably wants to see all the types.
The names of the parameters should be sufficient for readability given the context of the function it's being passed to. The majority of the time the types are just things your eye has to leap over to get to the next relevant bit and they're very easy to look up if it becomes relevant. If the types are important to understanding the code (a) you're probably doing something too tricky and (b) you can still write them out by using the less concise syntax.
Comment From: neild
Can you provide a concrete example for this?
I don't have a specific example to mind, but my thought is that since any call chain in Go which can contain errors must include explicit error propagation there are probably fewer cases of passing around functions that return a single value than in languages where errors are passed up as exceptions.
But then what's the point of saving a few keystrokes.
That applies to any type inference, doesn't it?
I don't see much difference between eliding the types in these two ways of fetching a row; in both cases, you're just saving a few keystrokes:
var row *Row = iter.Next() // explicit
row := iter.Next() // implicit
iter.Do(func(row *Row) { }) // explicit
iter.Do(func row { }) // implicit
Comment From: lukechampine
Can you provide a concrete example for this? If a single-expression function returns an error, it's (probably) a multi-valued expression, so it could only be used (if at all) in a suitable assignment. But I can only think of contrived examples.
func unwrap[T any](v T, err error) T {
if err != nil {
panic(err)
}
return v
}
strs := []string{"1", "2", "3"}
ints := slices.Map(strs, (s) => unwrap(strconv.Atoi(s)))
IMO, this is part of a larger point: if lambdas look nicer than normal functions, people will try to contort their code so that it can use lambdas. Not really much we can do about this, except to anticipate the sort of contortions people are likely to reach for, and provide better alternatives (e.g. higher-order functions that play nice with error returns).
Comment From: DeedleFake
I've never liked the discrepancy between => expr
and { stmts }
that a lot of languages have. It makes converting back and forth kind of awkward, for one thing, which I think is part of the cause of people contorting code to fit the single-expression syntax.
Some languages try and get around it by using the normal syntax and then implicitly returning if the last thing in the function is a single, otherwise unused expression, but that has its own issues, including pointless syntactic overlap in larger functions and an unclear return point depending on control flow.
How about a compromise? Keep the same syntax, but allow an implicit return if and only if the function body is a single expression. In other words:
// Legal.
func Example(a, b int) int { a + b }
// Not legal.
func Example(a, b int) int {
c := a + b
c
}
That would make converting back and forth easier, as you'd only need to add return then, but it would also remove some of the readability problems that stem from long functions with implicit returns. The same rule would then apply to anonymous functions and, presumably, any type-inferred anonymous functions that this discussion leads to.
Also, along with error returns, I think it's worth mentioning that the lack of any conditional expression in Go somewhat limits a lot of single-expression function usages, making easy conversion to a regular function body even more necessary, in my opinion.
Comment From: gonzojive
To support either statements or expressions in the body, why not do what JavaScript and Typescript do? Allow both, and differentiate with curly brackets:
(a, b) => a + b (a, b) => { if b == 0 { return 0, fmt.Errorf("div %v by zero", a) } return a/b }
This was probably already mentioned.
(As a separate matter, I'm not sure how the operator version of this allows partial or complete specification of return types.)
On Wed, Jan 5, 2022, 5:20 PM DeedleFake @.***> wrote:
I've never liked the discrepancy between => expr and { stmts } that a lot of languages have. It makes converting back and forth kind of awkward, for one thing, which I think is part of the cause of people contorting code to fit the single-expression syntax.
Some languages try and get around it by using the normal syntax and then implicitly returning if the last thing in the function is a single, otherwise unused expression, but that has its own issues, including pointless overlap in larger functions and an unclear return point depending on control flow.
How about a compromise? Keep the same syntax, but allow an implicit return if and only if the function body is a single expression. In other words:
// Legal.func Example(a, b int) int { a + b } // Not legal.func Example(a, b int) int { c := a + b c }
That would make converting back and forth easier, as you'd only need to add return then, but it would also remove some of the readability problems that stem from long functions with implicit returns. The same rule would then apply to anonymous functions and, presumably, any type-inferred anonymous functions that this discussion leads to.
Also, along with error returns, I think it's worth mentioning that the lack of any conditional expression in Go somewhat limits a lot of single-expression function usages, making easy conversion to a regular function body even more necessary, in my opinion.
— Reply to this email directly, view it on GitHub https://github.com/golang/go/issues/21498#issuecomment-1006207988, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAUO553VYWWXS4Z4UJYCUDUUTU5FANCNFSM4DXKSBAQ . You are receiving this because you commented.Message ID: @.***>
Comment From: sa-
Now that we have generics, it would make code so much more readable if we could have this with accompanying collection methods
What currently works with golang 1.18
func main() {
original := []int{1, 2, 3, 4, 5}
newArray := Map(original, func(item int) int { return item + 1 })
newArray = Map(newArray, func(item int) int { return item * 3 })
newArray = Filter(newArray, func(item int) bool { return item%2 == 0 })
fmt.Println(newArray)
}
func Map[T, U any](s []T, f func(T) U) []U {
r := make([]U, len(s))
for i, v := range s {
r[i] = f(v)
}
return r
}
func Filter[T any](s []T, f func(T) bool) []T {
r := make([]T, len(s))
counter := 0
for i := 0; i < len(s); i++ {
if f(s[i]) {
r[counter] = s[i]
counter++
}
}
return r[:counter]
}
I would like to do something like this (or with some syntax that is equally concise):
func main() {
original := []int{1, 2, 3, 4, 5}
newArray := original.
Map((item) -> item + 1).
Map((item) -> item + 3).
Filter((item) -> item%2 == 0)
fmt.Println(newArray)
}
Note that the Map and Filter methods would be provided on slices instead of having to define them.
I hope that this example helps to convey that the conciseness doesn't come at the cost of readability, but it actually helps readability instead.
Comment From: billinghamj
@sa- I think the chaining you've shown there realistically isn't possible without some kind of specific support for a pipelining operator. It's extremely unlikely that maps/slices/arrays will get methods added to them in such a way which allows [].Map
etc.
e.g. original.[Map]((item int) int => item + 1)
- syntax is obviously highly debatable, I think there was a JS proposal which looked more like original |> Map(%, item => item + 1)
I think for the arrow functions to really be useful, the typing should be inferred the same way it is typical variable assignment (:=
) - you often have complex types in these situations, which would make the line unreasonably long. Changing func (...) { return ... }
to (...) => ...
is not much of a saving when the majority of the space is the typing anyway
Comment From: sa-
I would like to clarify that my proposal shouldn't need a pipelining operator, nor am I particularly interested in such an operator.
If the Map
and Filter
methods are provided on slice and array types from the language, this shouldn't be necessary
Comment From: beoran
@sa- looks like you just rediscovered Ruby stabby lambdas. Which is a good notation for one line function parameters.
Comment From: f-feary
Discussing adding methods to slice and map types is a huge left turn from what this PR is supposed to be about, which is a shorthand for func literals.
Given the pattern long established for len
, cap
, delete
, close
, the entire std/strings
package, and most recently the exp/slices
and exp/maps
packages, adding methods to primitive types would be a significant change deserving of it's own issue to track and discuss.
Comment From: billinghamj
If the Map and Filter methods are provided on slice and array types from the language, this shouldn't be necessary
Discussing adding methods to slice and map types is a huge left turn from what this PR is supposed to be about
This has been discussed quite a bit, and has always been a pretty clear no. I think the chance of that ever happening in Go is pretty close to zero
Comment From: ianlancetaylor
@bradfitz and @griesemer and I discussed for a while, and wound up reinventing @neild 's suggestion from https://github.com/golang/go/issues/21498#issuecomment-355125728, fleshed out a bit at https://github.com/golang/go/issues/21498#issuecomment-355353058 and https://github.com/golang/go/issues/21498#issuecomment-355427633.
In any context where an expression has a known function type, you are permitted to write func identifier-list { body }
. There are no parentheses around the identifier-list, which distinguishes this case from that of a normal function literal. The types of the parameters and the results are taken from the known type. We can go further and say that if the body is an expression or list of expressions, the return
keyword may be omitted.
For the initial expression in this issue this permits
compute(func a, b { a + b })
This seems to be unambiguous and reasonably short. The main drawbacks are that it doesn't work with :=
(because the type is not known) and the fact that the syntax is perhaps too similar to ordinary func
syntax in that it hinges on the absence of parentheses.
This was discussed above and then the discussion moved on. Does anybody see any problems with this? Thanks.
Comment From: jimmyfrasche
so f(func a, b { a + b })
and f(func a, b { return a + b })
are identical?
Comment From: ianlancetaylor
Yes.
Comment From: DeedleFake
It looks a little odd to me without the parentheses, but overall I think it'll work quite nicely. The inclusion of func
makes it slightly longer than I'd like, but it's still quite reasonable, definitely.
The lack of a short syntax has definitely become a bit more of an annoyance with generics, so I'm looking forward to something like this potentially being added. Not only will it make code more readable, it should help with refactoring, too.
Edit: Example of a few small cleanups from one of my own projects:
// Old:
slices.SortFunc(peers[1:], func(p1, p2 *ipnstate.PeerStatus) bool {
return p1.HostName < p2.HostName
})
slices.EqualFunc(peers, old, func(p1, p2 *ipnstate.PeerStatus) bool {
return p1.HostName == p2.HostName && slices.Equal(p1.TailscaleIPs, p2.TailscaleIPs)
})
// New:
slices.SortFunc(peers[1:], func p1, p2 { p1.HostName < p2.HostName })
slices.EqualFunc(peers, old, func p1, p2 {
p1.HostName == p2.HostName && slices.Equal(p1.TailscaleIPs, p2.TailscaleIPs)
})
That first one in particular becomes significantly cleaner.
Comment From: jimmyfrasche
Does f(func { fmt.Println("foo") })
require f
take a func()
, a func() (int, error)
, or in either case?
Comment From: DeedleFake
@jimmyfrasche
I'd say that the type should be inferred from the usage, not from the return type. If you're passing it to something, and that thing wants a function with no returns, then the body being an expression is just ignored and there's no automatic, and incorrect, return. Otherwise, treat it as a return and do the type checking the usual way.
Comment From: zigo101
Personally, I think that increases the load of cognition burden much.
Comment From: beoran
@jimmyfrasche In the case of func { fmt.Println("foo") } f must take an func() (int, error), of course. It is the most consistent and type safe way.
We could then allow to writef(func { _,_ = fmt.Println("foo") })
for an f that takes a func().
Comment From: jimmyfrasche
Can you do func { nil, io.EOF }
or would you need a return
?
Does anything special need to be done for variadic parameters or are they silently variadic?
I'm :+1: on doing something here and :+1: on using func
without ().
I'm neither :+1: or :-1: on overloading {} on this but leaning toward :-1:. I prefer a separate syntax as I've said earlier. It keeps it concise while staying clear enough at a glance
Reusing {} would also cause issues should #12854 be accepted as then func {{}}
is legal and even if that's fine I imagine it would be easy to forget those outer {}.
Comment From: beoran
func { nil, io.EOF } seems ok to me even with #12854 . Arguably this is a func literal, so it seems consistent.
Comment From: magical
Reusing {} would also cause issues should https://github.com/golang/go/issues/12854 be accepted as then func {{}} is legal and even if that's fine I imagine it would be easy to forget those outer {}.
func() {{}}
is already legal (a function containing a single, empty block). I would expect the paren-less version to have the same meaning.
Comment From: MrTravisB
I would really like to see some kind of better syntax for anonymous functions but this seems like a marginal gain. The only real benefit is removing the need for a return statement for single line functions. Removing the parens around params actually makes it move difficult to read in my opinion.
Comment From: ianlancetaylor
Does f(func { fmt.Println("foo") }) require f take a func(), a func() (int, error), or in either case?
Good question. I think that either case should compile.
Comment From: ianlancetaylor
@MrTravisB
The only real benefit is removing the need for a return statement for single line functions.
I think the bigger benefit is not having to explicitly write the parameter and result types.
Of course, that may also make the code harder to read in some cases.
Comment From: aarzilli
For the initial expression in this issue this permits
go compute(func a, b { a + b })
To me this looks too much like passing two arguments to compute: func a
and b { a + b }
.
Comment From: f-feary
@ianlancetaylor @MrTravisB
The only real benefit is removing the need for a return statement for single line functions.
I think the bigger benefit is not having to explicitly write the parameter and result types.
It may be worth having the discussion of implicit return statements / single expression bodies, but I feel like that may be a distinct feature request.
Removing the type information, which adds a lot of code yet could be cheaply inferred, is definitely the obvious benefit of a shorthand syntax.
Comment From: sammy-hughes
Coming in late as I had no idea there was an active discussion on a ticket like this. I'm literally gathering notes for a proposal that would have been a dup of this.
@griesemer suggested, as many have echoed, that the func keyword can be dropped. I think that misses the point. Back in September, @ct1n suggested a syntax that efficiently handles my pain-points, presents limited difficulty when lexing, and is effectively type-able.
The following are my biggest pain points: 1. mentally computing the signature is more work than writing the actual logic, 2. deeply-nested fsigs are legitimately hard to read 3. IIF's become a sea of parentheses and braces.
My pain points all go away if I could elide the return type for single-expression functions, especially since I almost exclusively use this pattern to simplify terms, either to implement operators over properties of a structured type, or to elide terms from the final callsite by currying.
Here is a real-world example I was wrestling with last night. The class of behavior is a backoff-context, but here that's shown via immediately-invoked closures. I was writing a quadratically-scaling backoff timer as a function over attempts:
quadraticCursor := (
func(a, b int) func(int)int {
return (
func(c int) func(int)int {
return func(x int) int {
return a*x*x+b*x+c
}
}
)(a+b)
)(Alpha.PropX.AsInt(), Beta.PropY.AsInt())
)
Call it a contrived case, but this is a real-world example, and I hate it. 90% of that is just dealing with Go versus actual logic.
In contrast, the following, any version of the syntax, would be magnificent!
ApproachA := (func(a, b int) (func(c int) func(x int) a*x*x+b*x+c)(a+b))(Alpha.PropX.AsInt(), Beta.PropY.AsInt())
ApproachB := (func(a, b int) return (func(c int) return func(x int) return a*x*x+b*x+c)(a+b))(Alpha.PropX.AsInt(), Beta.PropY.AsInt())
ApproachC := (func(a, b int): (func(c int): func(x int): a*x*x+b*x+c)(a+b))(Alpha.PropX.AsInt(), Beta.PropY.AsInt())
ApproachD := (func(a, b int): return (func(c int): return func(x int): return a*x*x+b*x+c)(a+b))(Alpha.PropX.AsInt(), Beta.PropY.AsInt())
ApproachE := (a, b => (c => x => a*x*x+b*x+c)(a+b))(Alpha.PropX.AsInt(), Beta.PropY.AsInt())
ApproachF := (a, b int => (c int => x int => a*x*x+b*x+c)(a+b))(Alpha.PropX.AsInt(), Beta.PropY.AsInt())
ApproachG := (a, b -> (c -> x -> a*x*x+b*x+c)(a+b))(Alpha.PropX.AsInt(), Beta.PropY.AsInt())
ApproachH := (a, b int -> (c int -> x int -> a*x*x+b*x+c)(a+b))(Alpha.PropX.AsInt(), Beta.PropY.AsInt())
ApproachI := (func a, b {(func c {func x {a*x*x+b*x+c})(a+b)})(Alpha.PropX.AsInt(), Beta.PropY.AsInt())
ApproachJ := (func a, b int {(func c int {func x int {a*x*x+b*x+c})(a+b)})(Alpha.PropX.AsInt(), Beta.PropY.AsInt())
ApproachK := (func a, b int {(return (func c int {return (func x int {return a*x*x+b*x+c})(a+b)})(Alpha.PropX.AsInt()), Beta.PropY.AsInt())
- A presents possible lexing ambiguities between types and values. It writes quickly, and it makes sense if you know what you're looking at. Problematic?
- B fits existing patterns, and the 'return' operator is a strikingly-clear lexing hint.
- C as alternate to B appeals on resemblance to the walrus operator (':='), and once a dev makes that connection, it should universally make sense.
- D is extra, but whatever. I don't want to wait 3 more years for this!
- E is the "fat arrow", and it's used in JS for just such a feature. I would object that the token appears in structural pattern-matching implementations (Rust, Elixir, C++23). @griesemer previously opined that such a feature in Go would likely prefer using the
case
operator, a decision reflected also in Python's SPM impl, described in PEP-636. That objection answered, borrowing from JS/TS makes a Node to Go transition easier. - F is a repeat of E, but with type declarations.
- G is a repeat of E, but using the skinny arrow.
- H is a repeat of F, but using the skinny arrow, of G, but with type declarations.
- I is S-expr syntax but with curly-braces, unambiguously easy on the lexer.I am intrigued by the idiom suggested, with a logic structure resembling structured literals (e.g. struct, array, slice)
- J is a repeat of I, but with type declarations.
- K is a repeat of J, but with 'return' operator.
I personally think Approaches B and D are the most flexible with the fewest sacrifices, while still looking very much like existing Go code.
Comment From: f-feary
@sammy-hughes It seems like what you're looking for is just inferring the return type from any function body, as opposed to inferring more or less the whole function signature from its usage (as an anonymous function), which seems to be the common denominator with most of the suggestions floated around here.
However I would say that approaches A-K are all horrifyingly difficult to reason with, in my opinion even moreso than the original.
Comment From: leaxoy
Don't create new ugly syntax, just learn from other language pls.
Comment From: bradfitz
A concrete example I shared with @griesemer and @ianlancetaylor earlier.
Imagine code like this:
// Read is a generic version of DB.Read to use a func that returns (T, error)
// instead of the caller having to store the result into a closed over variable.
//
// It runs fn with a new ReadTx repeatedly until fn returns either success or
// an error that's not an etcd.ErrTxStale.
func Read[T any](ctx context.Context, db *DB, fn func(rx *Tx) (T, error)) (T, error) {
var ret T
err := db.Read(ctx, func(rx *Tx) (err error) {
ret, err = fn(rx)
return
})
if err != nil {
var zero T
return zero, err
}
return ret, nil
}
Currently, to use that function, caller code ends up looking like:
authPath, err := cfgdb.Read(ctx, s.db, func(rx *cfgdb.Tx) (*dbx.AuthPath, error) {
return rx.AuthPath(r.URL.Path)
})
...
usage, err := cfgdb.Read(ctx, s.db, func(rx *cfgdb.Tx) (billingpolicy.Usage, error) {
return billingpolicy.BillingUsage(rx, user.Domain)
})
...
return cfgdb.Read(ctx, grh.ctl.DB(), func(rx *cfgdb.Tx) (scim.Resource, error) {
return grh.getRx(rx, scimID)
})
... which is pretty annoying, writing all the types.
Under the latest proposal, that becomes:
authPath, err := cfgdb.Read(ctx, s.db, func rx { rx.AuthPath(r.URL.Path) })
...
usage, err := cfgdb.Read(ctx, s.db, func rx { billingpolicy.BillingUsage(rx, user.Domain) })
...
return cfgdb.Read(ctx, grh.ctl.DB(), func rx { grh.getRx(rx, scimID) })
Which IMO is great.
Comment From: seankhliao
would you be able to do
func x { something; somethingelse }
if yes, what would the canonical style look like, and how do you make the judgement call between old and new style
Comment From: DeedleFake
@seankhliao
We can go further and say that if the body is an expression or list of expressions, the return keyword may be omitted.
If I'm reading it correctly, then no, that would not be allowed unless somethingelse
included a return
or it was a void function. The elision of return
is only allowed if the body consists entirely of a single expression or comma-separated list of expressions. Essentially, if you can write return <something to return>
, you can remove the return
keyword if the entire body is just <something to return>
.
Comment From: seankhliao
I meant for my example to represent functions that span multiple lines, and the urge to cram it all into a single line
Comment From: f-feary
@seankhliao Simple: Don't. The objective is to make code easier to read and write; not harder.
Comment From: neild
@bradfitz
Under the latest proposal, that becomes:
authPath, err := cfgdb.Read(ctx, s.db, func rx { rx.AuthPath(r.URL.Path) })
Does it? That requires a very complex interaction with type inference:
func rx { rx.AuthPath(r.URL.Path) }
Here we know that we have a func that takes a single parameter. We don't know the type of that parameter, we don't know whether the func returns any parameters or the types of those parameters (if any). If we assign the literal to a variable with a known type, however, we immediately know all of that.
However, in this case we're assigning this func to a variable (function input parameter) of type func(*cfgdb.Tx) (T, error)
for some as-yet-unknown T
.
Type inference needs to identify the type of rx
as *cfgdb.Tx
, resolve the return types of the func based on that, and then use those return types to identify the type T
. We need to dance between deriving the type of the func literal and the type parameter of Read
. That may be technically possible, but I don't think its implicit in this proposal that it would be allowed. I don't believe there are any existing cases in the current generics design where we perform type inference using an unknown or partially-specified type as input.
Comment From: ianlancetaylor
@neild I think that we would want to apply "lightweight function type determination" before type inference. In this example, we would see that the type of the function is func(*cfgdb.Tx) (T, error)
. So we know that rx
is *cfgb.Tx
, and that in turn tells us the type of the result of the function body rx.AuthPath(r.URL.Path)
(in this case *dbx.AuthPath, error
). All of that happens before type inference.
When we get to type inference, we have to infer that T
is *dbs.AuthPath
, which is straightforward.
Perhaps I am missing something.
Comment From: ianlancetaylor
@seankhliao
would you be able to do
func x { something; somethingelse }
if yes, what would the canonical style look like, and how do you make the judgement call between old and new style
Yes, you could write that. But you can already write your function literals that way today.
sort.Slice(s, func(a, b int) bool { if a < 0 { panic("!!") }; return a < b })
I'm not sure this syntax suggestion introduces any wholly new problems.
Comment From: neild
@ianlancetaylor Consider these cases:
// Case A: authPath is inferred to be *dbx.AuthPath
authPath, err := cfgdb.Read(ctx, s.db, func rx { rx.AuthPath(r.URL.Path) })
// Case B: authPath is explicitly `any`.
authPath, err := cfgdb.Read[any](ctx, s.db, func rx { rx.AuthPath(r.URL.Path) })
In case B, we have explicitly defined the type parameter, so the third parameter to Read
has type func(*Tx) (any, error)
. The lightweight function seems like it should work here, since we could write this with explicit types:
authPath, err := cfgdb.Read[any](ctx, s.db, func (rx *Tx) (any, error) {
return rx.AuthPath(r.URL.Path)
})
But for that to work, we need to derive the type of the result in cases A and B in entirely different ways.
We could always infer the return type of a lightweight func from the input types and the func body (which makes case B fail to compile), but that's limiting--returning a concrete type that implements error
from a lightweight func would result in the func return type being inferred as that concrete type rather than error
.
I think this is much simpler if a lightweight func can only be assigned to a variable whose type is fully known.
Comment From: ianlancetaylor
I think that we should never set the result type of a lightweight function from the function body We should only set it from the type to which it is being assigned.
The question is what we should do when the lightweight function is being assigned to a function of generic type. In that case we set the result type to something like, in this case, (T, error)
. Once we have made that determination, we can, during type inference, unify the type from the function body with the type T
. Of course we can only do that if the type from the function body is known a priori.
It's true that there are several moving parts here. They don't yet seem impossible.
- lightweight function of type
func(X) Y
- function is assigned to parameter of type
func(*cfgdb.Tx) (T, error)
- therefore
X
is*cfgdb.Tx
andY
is(T, error)
- start type inference: must deduce
T
from the type of the lightweight function - lightweight function is already type
T
, so we need to look at the function body (this is a new inference step) - function body is type
*dbx.AuthPath
, so unify that withT
In case B the result type is (any, error)
, and there is no type inference.
Comment From: ianlancetaylor
I think this is much simpler if a lightweight func can only be assigned to a variable whose type is fully known.
By the way, that is undeniable. But I think we should put some extra effort into making this play well with generics, because generics encourages greater use of small function literals.
Comment From: jimmyfrasche
Does what this print depend on the order of the constraints?
func f[F func() | func() (int, error)](v F) {
fmt.Printf("%T", v)
}
func main() {
f(func { fmt.Println() })
}
Comment From: sammy-hughes
TypeScript has separate semantics for the return-half of a function declaration and the return-half of a function type. For the declaration signature, inputs: outputs
. For the type signature, inputs => outputs
. As observed above, by @nelid. I would like a way to add constraints on elided, genericized in/out param types, like so:
A := func x, y: func(int8)int16 {func v {x*y*v}}
B := func x, y int8 {func v: int16 {x*y*v}}
C := func x, y {func v {int16(int8(x)*int8(y)*int8(v))}}
D := func p {func v {(func c, ok { ok && c.X*c.Y*v || 0 })(p.(Point))}}
- A shows possible type declaration for in/out parameters of a curried function, hypothetically providing a generic type constraint for x and y, and a concrete final type.
- B is the same idea a different way around.
- C is how I expect to safely provide a concrete type as a final type, having elided any type declarations
- D is....nonsense, yeah. A Javascript dev seeing D might expect it to evaluate as either
saw true...saw truthy....satisfied, returning last seen
or elsesaw false, skiiiip...saw falsey, returning last seen
. Total nonsense in Go terms, though.
I think the story on D is "Geeze. Chill, dude, you're already possibly getting lambdas the same year as generics, and now you want ternary expressions?!?". I'm just trying to tease out how one might express that sort of thing, or failing that, if it is agreed that it's ok for this new syntax to be what it is, change the world for some people, and be a total meh for others.
Comment From: DeedleFake
@jimmyfrasche
I would imagine that that should result in a compile-time error of an inability to infer F
. If you specify the type manually, it's pretty straightforwards:
f[func()](func { fmt.Println() })
f[func() (int, error)](func { fmt.Println() })
The question is whether or not detecting that is feasible without too much trouble, which I don't know.
Comment From: sammy-hughes
@f-feary, all enumerated forms that I gave as examples were condemned, but they represented 11 lines compressed into 1. Of course it's hard to read when compressed like that. The challenge is to ensure they are actually still readable.
IforTheAth := (
func a, b {func x {a*x*x+b*x+a+b}}
)(Alpha.PropX.AsInt(), Beta.PropY.AsInt())
That is the very same thing as my example of current, live Go.
IforTheBth := (
func a, b {
func x {
a*x*x+b*x+a+b
}})(Alpha.PropX.AsInt(), Beta.PropY.AsInt())
The point is to explore how various forms read when deeply nested, in any syntax that's been discussed (but could actually work), and how partial type declarations might look. I definitely preferred the func(<inputs) return <expression>
syntax, as I think it leaves clear lexical markers for specifying types as needed, but it does feel like most folks are going to the func <inputs> {<expression>}
form.
Comment From: Splizard
compute(func a, b { a + b })
If Go is going to have this level of type-inference, then it needs to be consistent across the language and include struct type-inference (#12854).
However, I believe it would be better served to avoid the rabbit hole of complex type-inference and stick with well defined function types, which unlike structs, currently lack a convenient constructor.
Here's an example:
http.ListenAndServe(":8080", http.HandlerFunc w, r {
fmt.Fprint(w, "Hello World")
})
Since the literal function is strongly typed, it can be assigned to variables using :=
and is easier to read, developers only need to be familiar with the function type in order to understand the arguments. It's also trivial to lookup the declaration of http.HandlerFunc
to check the argument types, and this syntax enables the function to passed as an interface implementation which you cannot do with a naked func
(at least not without wrapping it inside of an http.HandlerFunc
anyway).
It also works well with generics, where there are already plenty of type-inference possibilities to explore.
type Adder[T any] func(a, b T) T
fn := Adder[int] a, b { a + b }
Comment From: sammy-hughes
@Splizard, I think I'm likely the only one asking for partial type declaration, so take that as warning, but I don't like that your suggested syntax drops the func keyword. That said, given what has generally been established....
A := func a, b { a + b} Adder[int]
B := func a, b { a + b } as Adder[int]
C := Adder[int](func a, b { a + b })
D := Adder[int]{ a + b }
I think I just fell in love with example D. If a function is declared in context of a type-coercion, I'm unclear on whether the desired effect would be a runtime coercion on the inferred types, or if rather I prefer the coercion serve as constraint on possible types, statically.
Even if I'm changing what you said, I think I at least get your point.
Seriously, though, I really like form D above. I think @griesemer suggested named types as a way to specify parameter signatures, and I think it just clicked for me. If both facts are true, that form D above is received well and that function types which are assignable for every term across both signatures are themselves assignable types, the version of that suggestion in form D above is the most versatile I've yet seen.
Comment From: ianlancetaylor
@jimmyfrasche Nice example. I think it has to be an error. I think it would be fine to say that we can't infer anything from an argument that is a lightweight function literal. That would leave your example with no way to infer F
.
Comment From: ianlancetaylor
@Splizard We are already inconsistent about type inference. Given x = 1
we infer the type of 1
from the type of x
. But we don't do that for x = {1, 2, 3}
.
I'm not saying that we can't do #12854, just that a decision on that is independent from a decision on this issue.
Comment From: jimmyfrasche
@ianlancetaylor wouldn't it work if there were a separate syntax for the return-a-single-expression case? It also seems better for readability as you could disambiguate those cases at a glance. Is there a reason for preferring to reuse the same syntax for both cases?
Comment From: DeedleFake
@jimmyfrasche
Is there a reason for preferring to reuse the same syntax for both cases?
One point that I can think of is that it makes refactoring easier. If you want to change from a single-expression function to a regular full body, all you have to do is add a few newlines and a return
. If you have a separate syntax, such as func a, b -> a + b
, you have to remove the ->
and add {}
, which is a lot more annoying. I think the ease of refactoring is worth the annoyance of a few minor edge cases, but that's just my personal preference. Little things like that can make the difference between a simple language like Go or Ruby and a language like Java where essentially have to use an IDE that can do a bunch of transformations of various kinds for you to be remotely productive in it.
Comment From: ianlancetaylor
@jimmyfrasche Yes, I think that would make your example work, but I don't see a reason to index on that case. I think we should try to support generic, but I don't see why people would write generic functions with a type parameter that accepts various different function types. If that case is indeed rare then I don't think we need to introduce syntax to support it.
Comment From: Splizard
@ianlancetaylor There is an asymmetry in Go with regards to function types versus composite types and basic types.
I can define a struct, map, slice, or basic type and then use them without referring to the builtin expression for that type.
type MyBasicType int
_ = MyBasicType(1)
type MyStruct struct { a,b int }
_ = MyStruct{1, 2}
type MyMap map[int]int
_ = MyMap{1: 2}
type MySlice []int
_ = MySlice{1, 2}
type MyFunc func(a, b int)
_ = MyFunc(func(a, b int) {})
(observe how MyFunc
sticks out).
We are already inconsistent about type inference.
I disagree, Go is consistent about type-inference, {1, 2, 3}
is not a valid Go expression or value, nor do I believe it should be. However, it could be inferred as a composite value, in the same fashion as func a,b {a + b}
. In these cases, the components of the type are being inferred, and if Go supported this level of inference, it should apply to both composite types and function types in order to remain consistent.
just that a decision on that is independent from a decision on this issue.
The language is a whole, I don't think it's healthy for the language to be considering changes in isolation, what links this issue to #12854, is the level of type-inference that the syntax func a,b {a + b}
introduces.
Having a constructor for defined function types, that avoids the func
keyword, would provide a lighterweight anonymous function syntax, without closing the door on adding extra type-inference capabilities in the future. I like the style of func a,b {a + b}
.
More technically, I would propose that a function type can be followed by zero or more argument names that name the arguments of that function within the scope of the function's block.
_ = func(int,int) int a, b { a + b }
type MyFunc func(int,int) int
_ = MyFunc a, b { a + b }
@sammy-hughes I imagine that you would prefer that the arguments are optionally named (or that they cannot be renamed)?
_ = func(a, b int) int { a + b }
type MyFunc func(a, b int) int
_ = MyFunc{ a + b }
Comment From: jimmyfrasche
@ianlancetaylor
Another one: func { <-c }
probably means returning the result of <-c
or it could be a callback that waits and c
is chan struct{}
.
Ambiguities around type inference would be annoying but I'm more worried about ambiguities around reading the code, though perhaps that's overblown and the context within the line will be sufficient. The fact that this works 99%+ of the time but has some weird edges seems like it would be if anything more troublesome since when it does show up it would be all the more confusing.
@DeedleFake I've used languages with different expression and block syntax and it is a little annoying to switch up the forms but it doesn't happen often and it's not a very big deal (and entirely automatable).
Comment From: sammy-hughes
@Splizard, I love this idea. A version of it, what you've expressed or something close, would rapidly infect every greenfield project I write, if it goes live.
I think we're dealing with a related but different API. I think it's direct, well-constrained, and broadly useful; orthogonality with single-function-interfaces in the best way. @bradfitz's example earlier was a prime candidate for just such a feature.
If the version I'm describing of the version @Splizard described becomes adopted, it would actually put the lambda/single-expression function in a very similar boat to structs, as regards #12854. This functionality would cover a lot of ground, but I can't prove to myself that they're the same feature. Shorthand, type-inferred functions and named-type function instantiation are distinct features, even if neighbors...I want them both, please, sirs?
Dropping the func keyword for such a case feels like a "javascript-grade" move, but I now recognize it.
type A struct{x int; y int;}
type BFunc func(*cfgdb.Tx)(billingpolicy.usage, error)
type CFunc func(tx *cfgdb.Tx)(rx billingpolicy.usage, err error)
D := A struct{n0, n1}
E := A{n0, n1}
F := A{x: n0, y: n1}
G := BFunc(tx) {return rx, err}
H := BFunc(txPtr) {return rx, err}
I := BFunc(tx) (rx, err) {rx, err = ....; return}
J := CFunc(tx) (rx, err) {rx, err = ....; return}
K := BFunc tx {rx, err}
L := CFunc tx, rx, err {...?...?}
var M BFunc = func tx {rx, err}
- A is effectively a pointer to metadata describing a tuple of two ints.
- B is effectively nil function pointer? Unsure, but metadata describes an input tuple and output tuple.
- C is ~~not assignment compatible~~ with B. I had a memory misattributed to the incompatibility of
func()int
andfunc()(a int)
. BFunc and CFunc are compatible types. - D is nonsense, on purpose, as it is analogous to insisting on the func keyword, if dealing in named function types.
- E is the standard instantiation pattern
- F is...interesting for this conversation. F repeats E, but there would not be a corollary between struct and func here.
- G is a function signature as instance of BFunc. 0th value of in is of type
*cfgdb.Tx
, and is now bound to the variabletx
. No type inference here. - H is identical to H. Function parameter names are not semantically meaningful, once in IR.
- I highlights the ambiguity about BFunc vs CFunc, and what SHOULD be allowed.
- J is the contrapoint to I, and pushes that question further.
- K is the overlap of lightweight function syntax and named-type function instantiation. This is not an argument for validity, but only an example.
- L indicates that there is some ambiguity to this approach, regarding the overlap between lightweight function syntax and named-type function instantiation.
- M describes a lightweight function declaration assignment to a concretely-typed variable.
I'm still wondering how much overlap there is between these two features, but if ever there was a "distinctively Go" way to implement lambdas, this would be it. At the same time, I'm quite certain this will feel alien to many. I assert thematic congruity, not fair reception.
Quick note on x = {1, 2, 3}
, I opined quite pontifically in #48499, but I find nothing inherently problematic with that sort of expression. Guaranteeing compatibility decades down the road....."unclear", which Go-style is an alias for "invalid".
Meanwhile, @jimmyfrasche, I don't accept that the type inferred for the function, func { <-c }
, could be ambiguous. Channel c has a definite type. It could be a definite parametric type, but that is still a definite type, as an instance of func blocking on a channel read, and eventually producing someType(0), false
or someType(value), true
Comment From: sammy-hughes
additional to the above but distinctly, @Splizard, both the API you describe and I describe, I suggest the feature(s) be presented with named function types always ~~beginning with "Fn" or "fn"~~, as with gopls rule for package-declared error names.
EDIT: Hah! Thanks, @DeedleFake, right. Names always ending with "Func"
Comment From: DeedleFake
@sammy-hughes
Go already has a naming convention for named function types: They end with Func
.
Comment From: sammy-hughes
@Splizard, looks like someone beat you to the idea ... by a few years. Conveniently enough, it was shot down for reasons you addressed!
The core issue was that the API as proposed put the naming authority with the type declaration. This would have required a change in the type API, so the issue was closed. A further concern was, for the API as described, difficulty distinguishing between an interface literal and a function definition, which is again a non-issue.
Three questions, @ianlancetaylor, and if unclear, see examples below: 1. Is my intuition justified That @Splizard has suggested a parallel and potentially competing feature? 2. Given that this is similar to the proposal of #36855, and addresses the mortal issues, should the ticket be reopened? 3. Do you see any technical reason why this version of a lightweight function syntax would be inferior to the type-inference version, either from maintenance or implementation difficulties?
Examples:
type AFunc func(int, int) int
type BFunc func(int) int
func C(a, b int) int { return a+b }
var D AFunc = func(a int, b int) int {return a+b}
var E AFunc = func a, b {a+b}
F := Afunc a, b {a+b}
G := AFunc(a, b) {return a+b}
H := AFunc(func a, b {a+b})
I := (func(int, int)int)(func a, b {a+b})
J := []AFunc{
nil, //0 ... sorry. It was bothering me.
AFunc(a, b) c {c = a+b; return}, //1
AFunc(a, b) c {c = a+b; c}, //2
AFunc a, b (c) {c = a+b; c}, //3
func a, b (c) {c = a+b; c},//4
func a, b to c {c = a+b; c},//5
func a, b -> c {c = a+b; c}//6
}
- AFunc is a "Today Go" named function type
- BFunc is not compatible with AFunc
- C is a "Today Go" package-level function. C is assignable to AFunc slots, but is of unnamed type
func(int, int) int
- D is a "Today Go" anonymous function, assigned to a named function type
- E is the "consensus" form of "lightweight anonymous function syntax"
- F is @Splizard's suggestion, a variant of E, as an instantiation of a declared type
- G is to D as F is to E, being my version of what @Splizard proposed
- H is another way to spell F.
- I is what H is, but as an unnamed type, "func(int, int)int"
- J is proof to anyone with a soul that the "consensus" form of "lightweight anonymous function syntax" should not support named out-parameters. Like, I could live with 5 and 6, but 5 becomes a reserved word then, and 6 being confusing for folks expecting
<params> -> <expression>
syntax.
Note that J above is not an argument against multiple-expression "lightweight anonymous function syntax." I met block-level expressions in Rust, where the last evaluated expression in a block is what the outer block sees. I love the feature, and I'll fight for it....in its own ticket.
Comment From: ianlancetaylor
Is my intuition justified That @Splizard has suggested a parallel and potentially competing feature?
I'm not sure I really understand @Splizard 's suggestion. If I do, then it seems to me that it requires me to write out the function type including all the parameter and result types, in order to have a name that can be used for the lightweight literal. If that is correct, then, yes, I think that seems unrelated to this proposal. This proposal is about having a way to write lightweight function literals that infer the parameter and result types from the context in which the function literal appears. If I have to write out those types explicitly, in a different type definition, then I'm not getting the full benefit of this proposal.
Given that this is similar to the proposal of #36855, and addresses the mortal issues, should the ticket be reopened?
No, but a new proposal could be opened. But I'm not too persuaded by the idea. People don't write function types very often, so there wouldn't be much use today. And for future uses I don't see a big advantage to writing the types in one place and using them in another. A small advantage, yes, as the function type can be reused, but not a big advantage. The main argument in favor may be consistency, but Go has many inconsistencies of this sort; consistency arguments are valid but not in themselves persuasive.
Do you see any technical reason why this version of a lightweight function syntax would be inferior to the type-inference version, either from maintenance or implementation difficulties?
Yes: it requires me to explicitly write down the types somewhere.
Comment From: jba
Yes: it requires me to explicitly write down the types somewhere.
In the common case of a function argument, someone already has to do that in the formal parameter. The problem with the idea is that it would effectively require every
func Foo(f func(int, int) bool)
to become
type FooFunc func(int, int) bool
func Foo(f FooFunc)
so that callers could take advantage of the shorter syntax. That is too much to expect of every function author.
Comment From: DeedleFake
That is too much to expect of every function author.
And if the author doesn't, then requiring me to do it in turn eliminates about 90% of the benefit of this proposal.
Comment From: beoran
So, taking a step back from the details of the syntax for this proposal, it seems one important benefit that this proposal should have is that we would not have to write out the types of the parameters and of the return values.
An implicit return might be considered as a separate feature.
With that said I think we can limit the syntax choices to the three following ones:
now: func(a int, b string) (string, error) { return foo(a,b) }
a: func a,b { return foo(a, b) }
b: -> a,b { return foo(a,b) }
c: => a,b { return foo(a,b) }
Comment From: fzipp
With that said I think we can limit the syntax choices to the three following ones:
now: func(a int, b string) (string, error) { return foo(a,b) } a: func a,b { return foo(a, b) } b: -> a,b { return foo(a,b) } c: => a,b { return foo(a,b) }
I find -> too similar to the channel send/receive operator <-, and => too similar to the relational operator <=. Go has a keyword for functions (func), so I'd expect it to indicate any form of a function.
Comment From: beoran
@fzipp That is a good argument. So the three important requirements are having the func keyword, not having types for the parameters and return values, and backwards compatibility.
Then really only the syntax that remains is the func a,b {...}
syntax as proposed by @ianlancetaylor. I hope we can focus the discussion on that syntax then.
Comment From: sammy-hughes
it requires me to write out the function type including all the parameter and result types
@ianlancetaylor, not really. The suggestion was that you could, optionally, write out a function type, not that it is required. Applied to standard syntax for anonymous functions, it could be considered a competing proposal, but when applied to the consensus syntax for lightweight anonymous functions, it covers edge-cases when inference is problematic. I frequently use higher-order functions, but it appears @Splizard and I overrated how common that is.
Acknowledging that, I still want some space between "Specify everything" and "Specify nothing", and I assume partially-specified types invalid, as corollary to the named in/out params rule. My fussiness stems from assumptions about possible implementations, but I want the following to work:
type exampleFunc func(t int64, v int64) int64
type exampleBox struct{f exampleFunc}
A := exampleBox{
func a, b { a+b }
}
B := exampleFunc(func a, b { a+b })
If the underlying type is either func(int,int) int
or func[T interface{~int64}, U interface{~int64}, V interface{~int64}](a T, b U) V
, then both imperatives above are invalid, as neither are compatible with func(int64, int64) int64
would not be valid.
An implicit return might be considered as a separate feature.
@beoran, while I prefer the return
token be included, @ianlancetaylor suggested omitting the return
token. The discussion I pushed above was assuming consensus on the grammar for lightweight syntax as func ident-list { body }
.
To serve the specific questions I put to @ianlancetaylor, and any discussion regarding motivating usecases, I gave examples contrasting examples for the standard syntax for anonymous functions, consensus proposal syntax for single-expression functions, the special case proposed by @Splizard, and extension of standard syntax for anonymous functions with the special case applied. I had intended to also cover examples for named-parameter cases, but quickly realized that combining named outparams and the consensus proposal syntax is not reasonable, even supposing a variety of possible separation tokens.
The problem with the idea is that it would effectively require every ... to become ...
@jba, I think you're missing something. A named-type as alias for a particular function signature is compatible with the unnamed type, the function signature that is the underlying type. If you're talking about a different concern, and if it's still relevant with the special-case suggestion having been tabled, feel free to clarify.
Comment From: beoran
@sammy-hughes I am only consideing the "outer" syntax at this time. Whether or not we can omit the return might be a different issue. Other proposals have been rejected due to implicit returns being not go like, so I assumed it is better to keep it for now
Comment From: sammy-hughes
@beoran, I mean, I prefer the explicit return
token. It helps anchor the pattern for tokenizing, and it also does a better job of saying "Hey! This is a function defintion!" to anyone reading my code, including future me. I had assumed that the return was being elided, but frankly, this is a "change the world" feature for me, return
token or no. I want this capability ASAP.
Historical, not an argument for either, but I observed the stipulation that the return token is implied, and that the body may only ever be an expression or list of expressions that are valid in a return clause. If that changed at some point, my confusion would be explained.
Comment From: dolmen
@Splizard @sammy-hughes See also #30931 (which is a proposal I would vote for if it was still open).
Comment From: dolmen
@ianlancetaylor
What aboutl lightweight function given to an any
target?
Long version:
If I have the following function:
func compute(f func()) {
fmt.Printf("%T\n", f)
}
func main() {
compute(func() {
})
}
If I change the signature to func compute(f any)
the code still works and clients don't have to be modified.
In contrast, with the proposed syntax that relies on type inference, compute(func {})
might not compile anymore.
At least the behavior of a lightweight anonymous function would have to be specified when given as any
.
Comment From: sammy-hughes
@dolmen, I'd object that for the example, you are correct only if the return
token is elided, but it doesn't represent a breaking change or a meaningful problem.
If you're pointing out the larger class of behavior surrounding fmt.Printf("%T\n", func <params> {<expressions>})
, then I agree that it might be a difficulty, as with generics presently. I'm not sure. I'd suggest that if the function is supplied in that manner, the types inferred for the signature would be the first valid signature found for the types inferred during the expression. E.G. If there's a single parameter and single return, with the expression being a numeric operation, the most likely signature would be func(int) int
or func(float) float
. If an argument is supplied, being itself typed, that impinges on inferences, and the inferred signature can be expected with more certainty.
For the example you gave, though, I don't see any issue. For a closure that takes no argument and returns no value, provided there isn't a special case to disallow func <no params> {<no-return expression-list>}
, there isn't any reason why it shouldn't work. If the return
token is elided as assumed for the lightweight syntax, then the only possible expressions to fit the signature func()
must not produce any value. If the return
token is not elided, and is still explicit, there's no change to the kind of statement that might fit your example.
That is to say, "I don't see any particular difficulty posed by your example, but if I guess at the larger class of problem you reference, I think the problem is more interesting but equally benign."
Comment From: jba
func compute(f func()) { ... }
... If I change the signature tofunc compute(f any)
the code still works and clients don't have to be modified.
Since compute
is unexported, the "clients" are all in the same package, so changing the calls is easy.
But let's say it was exported as Compute
. Then this is a breaking change to your package's API, even though call sites don't have to change. A client package might have code like
var f func(func()) = pkg.Compute
which would no longer compile.
Comment From: ianlancetaylor
What aboutl lightweight function given to an any target?
It's not permitted.
Your example, which already works today, is not using a lightweight function literal.
Code like the following, which I think is what you are suggesting, will not compile.
func compute(f any) {}
func F() {
compute(func {})
}
Comment From: sammy-hughes
@ianlancetaylor, is there intention to support usage as in the snippet below? That is,
1. Will both A.f
and B
return a value having unnamed type 'int64'
2. Will both A.f
and B
have the named type <package>.exampleFunc
?
type exampleFunc func(t int64, v int64) int64
type exampleBox struct{f exampleFunc}
A := exampleBox{f: func a, b { a+b }}
B := exampleFunc(func a, b { a+b })
Comment From: DeedleFake
@sammy-hughes
A.f
was declared to be exampleFunc
, so it will be no matter what. The question is whether or not the expression exampleBox{f: func a, b { a + b }}
is allowed, which it seems likely that it should be.
Comment From: griesemer
The A
case can be viewed as an assignment to A.f
. The type of A.f
is exampleFunc
, the function literal matches that type and one can properly infer the function signature. If we would allow such a use (which seems possible), the type of A.f
remains exampleFunc
(of course). Calling that function returns a value of int64
(not an untyped value).
Similarly, for B
, the same applies assuming we allow such a conversion (which is plausible).
Comment From: rodcorsi
I think omitting the return
statement inside of block IMO only makes sense if we have block expressions (int a = { 1 + 5 }
), because of that, the syntax func a, b { a+b }
doesn't appear correct to me.
Omit return
statement with a syntax like func(a,b) a+b
or func(a,b): a+b
could be better
Comment From: sammy-hughes
@griesemer, cool! That tentative confirmation affirms that the new syntax likely works as replacement to every anonymous function I've ever written in prod code, which is flippin' fantastic! EDIT: ....well, the ones that just directly return, having a single expression-list, but that is most of them for me...
@Splizard, this definitely meets the need I saw being met in your function-type-constructor syntax. Is there any reason that doesn't work for you, the type-cast around a shorthand anonymous function?
@DeedleFake, I get that it felt like a "no-duh" question, but some "of course it will" expectations of generics in Go didn't pan out, not really fitting for Go's memory model or semantics, so I think it's fair that I really wanted to be able to confidently expect such semantics. I can actually imagine a few naive implementations where that wouldn't be the case, as well.
@rodcorsi, I think folk mostly have settled on the func parameter-list { expression-list }
syntax. Hey, maybe it does look unfamiliar, but from a perspective of "can that expression be unambiguously lexed by both me and machine?", it's absolutely suitable! For instance, your first example isn't currently valid, as far as I know. For the other two proposed grammars:
1. func( parameter-list ) expression-list
is ambiguous between a function signature and an anonymous function.
2. it isn't clear that func( parameter-list ): expression-list
is more expressive in some way than func parameter-list { expression-list }
, and especially since Go permits multiple return, a comma following the first expression is ambiguous between delimiting the anonymous function expression-list and marking the boundary between the anonymous function and another expression. This makes the former grammar less expressive than the latter.
Comment From: beoran
@sammy-hughes As i have explored above, the func parameter-list {} syntax is the only one that is backwards compatible and unambiguous, barring any new stabby lambda operators. So let's converge on that.
For this issue I would keep the return statement, omitting it may be a separate feature that can be implemented later if needed, since allowing return to be omitted is also backwards compatible.
Comment From: sammy-hughes
@griesemer, @ianlancetaylor, can I request a hot take on cases A, B, and C below? This is based on the assertion by @beoran, above:
// A is nice
A := func a, b { return a+b }
// A implies B. B is nice, if permitted
B := func a, b {
if a > 0 {
return a+b
}
return b
}
// If A, then C balances a negligible benefit against messy implications
C := func a, b { a+b }
// ****** arguments against C, as implications of C *******
// C implies D as `func(a,b T) (int, err)` which will be surprising for many
D := func a, b { fmt.Println(a+b) }
// D suggests some facility to assert a void return. I read E as an argument against D, and by extension, against C
E := func a, b { _,_ := fmt.Println(a+b) }
// If B and C, and if C is not a restricted to single-expression functions, then B and C imply F...F is more Rust than Go.
F := func a, b {
if a > 0 {
a+b
}
b
}
Comment From: zephyrtronium
@sammy-hughes
None of those are valid as proposed (hyperlinked for convenience, since it is now lost to GitHub's "65 more items" box). @ianlancetaylor elaborated in https://github.com/golang/go/issues/21498#issuecomment-1092208760 that the function type is inferred only from the site of use of the function literal, never from the contents of its body. In particular, there is no way to use this syntax with :=
.
Comment From: griesemer
@sammy-hughes Note that light-weight function literals could only be used in assignment (and perhaps conversion) context, but not in short variable declarations because there's no type given. None of your examples as written are permitted.
In its simple-most form (i.e., smallest change to spec), a light-weight function literal looks exactly like a normal function literal except that the signature only needs to list the names of the incoming arguments, separated with commas, without types or parentheses, and without return parameters. Such a function literal could be used whenever it is assigned to a variable/function parameter that has a matching function type, and perhaps when it's converted to a matching function type. Here are some examples of valid/invalid uses (in practice, in most cases the assignment will be passing the function literal to another function as an argument; the use of a variable declaration here is only to show the corresponding function type):
var f func(x, y int) int = func a, b { return a + b } // valid
var f func(x int) (int, int) = func a { return a + b, a - b} // valid
var f func(x int) (int, int) = func a { return a + b, a - b, a*b} // invalid: result count mismatch (2 vs 3)
var f func(x int) int = func a, b { return a + b } // invalid: argument count mismatch (1 vs 2)
var f func(x int) = func a { return a + 1 } // invalid: literal returns a result but function type doesn't specify a result
var f func() int = func {} // invalid: function type specifies a result but literal doesn't have one
var f func() int = func { return "foo" } // invalid: result type mismatch (int vs string)
If need be, we could go one step further and also allow the omission of the return
statement in a function body, if and only if that function body consists of just that return statement (with 0 or multiple return values). Using the same examples as above:
var f func(x, y int) int = func a, b { a + b } // valid
var f func(x int) (int, int) = func a { a + b, a - b} // valid
var f func(x int) (int, int) = func a { a + b, a - b, a*b} // invalid: result count mismatch (2 vs 3)
var f func(x int) int = func a, b { a + b } // invalid: argument count mismatch (1 vs 2)
var f func(x int) = func a { a + 1 } // invalid: literal returns a result but function type doesn't specify a result
var f func() int = func {} // invalid: function type specifies a result but literal doesn't have one
var f func() int = func { "foo" } // invalid: result type mismatch (int vs string)
Comment From: zikaeroh
If need be, we could go one step further and also allow the omission of the return statement in a function body, if and only if that function body consists of just that return statement (with 0 or multiple return values). Using the same examples as above:
Just to note a potential gotcha with this method:
var f func() = func { x.Close() }
If x.Close()
returns a value, say, error
, then this form will assume that the intent was to return the error, then fail on assignment because the intent was to have no return value. Other languages with looser assignment rules (say, TypeScript, or type-checked Python) typically allow assignment of functions with returns to those with void return types, which avoids this sort of problem, but Go doesn't.
A more real-world example would be passing a function to t.Cleanup
or something, where I can see f.Close()
in a t.Cleanup
being a thing, but it's not a perfect example given func() { x.Close() }
is already quite short, so there's probably a better example around.
Comment From: griesemer
@zikaeroh Yes, that is a good point which I glanced over. Note that Go already has a notion of results that "must be used", e.g. a stand-alone 1+2
is not permitted but a stand-alone f.Close()
is. I think we'd want to apply/generalize the same rule here as well: An expression with a value that must be used would require a return result, an expression with a value that may be used does not.
Comment From: zephyrtronium
go var f func() = func { x.Close() }
If
x.Close()
returns a value, say,error
, then this form will assume that the intent was to return the error, then fail on assignment because the intent was to have no return value.
I don't follow this claim. The context requires a value of type func()
, so the type of func { x.Close() }
should be func()
, not func() error
, regardless of the type of x.Close()
.
Comment From: zikaeroh
@zephyrtronium Yes, you're correct, I was just pointing out that an implementation of that "one step further" form would need extra logic to handle this case rather than use a purely syntactic rule, since it'd be a little awkward otherwise. It's a little unique given I don't think there's anything like it currently (where type inference changes the control flow entirely), but that's surely fine.
Comment From: beoran
@griesemer @zikaeroh
I think this goes to show that the "simplest" form is the best option. The case of x.Close doesn't cause any problems because there is no return statement, and then we can assume the function literal has no return values. It keeps the language simple and consistent, but with the benefit of not having to write out the types of the arguments and return values of the function literal.
So var f func(x, y int) int = func a, b { return a + b }
seems to be the best solution.
Comment From: gprossliner
I really like this discussion here, and the ability to use type inference would be very useful!
What I don't like about the current proposal var f func(x, y int) int = func a, b { return a + b }
is that the function body is visually bound to the last argument. As it would belong to it. It reminds me of the syntax to initialize a struct like person {name: name}
.
Sorry about asking again, but what was the rational behind sticking to func
? If we would use an operator instead, we would have no problems with ambiguous syntax. IIRC this was described as "Scala like": var f func(x, y int) int = (a, b) => a + b;
.
When sticking with func
, I really think the arguments should be grouped somehow, or that there is a separator between the arguments and the body. Parentheses would be ambiguous, so maybe brackets var f func(x, y int) int = func [a, b] { return a + b }
optional without return
like: var f func(x, y int) int = func [a, b] (a + b)
. The parentheses here represent an expression naturally.
func g(fn func(y, x int) int) { }
g(func a, b { return a + b }) // current proposal
g(func[a, b]{ return a + b }) // brackets block style
g(func[a, b](a+b)) // brackets expression style
g((a,b)=>a+b) // scala style
PS: Thanks @griesemer as one of the original authors to still participate here. Thank you for GO :-)
Comment From: DeedleFake
the function body is visually bound to the last argument
Can the same not be said about single, unnamed returns in regular function definitions, such as func Example() SomeType { ... }
? Personally, I don't find either situation particularly confusing or odd looking.
Speaking of which, will there be a way to specify named returns? Maybe put them in parentheses instead, so func a, b (v, ok) { ... }
?
Comment From: beoran
@gprossliner The growing consensus seems to be that the syntax should use the keyword func and not an operator. I am not against an operator, but all in all the func keyword seems easier to explain to new Go programmers. As for square brackets, they are for generics, so I think we should not use them in this case, leaving only the func a,b { return a+b} syntax again.
@DeedleFake , I think that named returns are not needed at first for function literals. Let's keep the proposal simple, named returns could be added later if practice shows their need.
Comment From: sammy-hughes
@griesemer, thank you for those clarifications.
The case of closing a channel and of printer functions from fmt are strong arguments, as @beoran is highlighting, that there is very little benefit from supporting an elision of the return
token.
Comment From: beoran
Another argument against omitting the return token is that in other proposals on error handling, most people here on this issue tracker seemed against implicit returns. Without implicit returns it is easy to see the difference between a function literal that has no return values and one that does have them.
Comment From: gprossliner
@beoran you are right about the brackets. I obviously still don't consider generics :-). So if a operator is from the table, and we wanna stick with func
, we will not have many more opportunities.
Comment From: sammy-hughes
To expand on @beoran's point, an implementation that elides the return
token means that an expression like func x {fmt.Println(x)}
might be read by a human as "function over x to void, pushing x
and a newline to standard out", while the true reading is "function over x to integer, error...". Another case is for a mutative function, such as func x, y {(*x).Y = y}
which cannot be expressed given elided return
token, excepting special handling.
An eliding implementation cannot express such cases where the dev intends to ignore the return value of a function call. I observe the folowing caveats:
1. A lightweight-syntax function could be compatible with a type as signature having compatible in-params, but void out-params, e.g. func x {fmt.Println(x)}
is compatible with both func(any) (int, error)
and func(any)
. This was suggested by @zephyrtronium in this comment
2. If it is considered bad practice to ignore the output, the objection is moot. E.G. if a user SHOULD handle the result of a call to fmt.Println
, there cannot be an objection.
3. It could be considered "idiomatic" to write an expression such as func x {close(x); return;}
, specifically to snuff the return. ~Alternatively, an explicit semicolon, such as in func x {fmt.Println(x);}
might be considered as equivalent~ (Sorry. That's just silly and extra confusing). Either case presents both initial confusion in searching for a spelling of the desired signature, and then later to other devs, when guessing at the motivation for the specific spelling.
@Deedlefake, I personally don't like any of the candidate grammars I can think of for named out-params. Further, as any such syntax will need to be unambiguous between in-params and out-params, any candidate grammar would be non-breaking if added as a later improvement
Comment From: DeedleFake
Another case is for a mutative function, such as
func x, y {(*x).Y = y}
which cannot be expressed given elidedreturn
token, excepting special handling.
Why can it not? The rule, as stated by @ianlancetaylor, was that a function would automatically return its body if that body consisted only of a single expression or comma-separated list of expressions. That function has an assignment, which is a statement, so it can't return anything. That's not a special case at all.
I think the void function issue can be avoided, no pun intended, by a simple extension to that rule: A function will return its body automatically if the implicit function signature has return values and its body consists solely of a single expression or comma-separated list of expressions.
An eliding implementation cannot express such cases where the dev intends to ignore the return value of a function call.
I don't understand this statement. If the function has no returns, don't return. This does not seem complicated to me. For example,
func WithReturn(a, b int, f func(int, int) int) {
fmt.Println(f(a, b))
}
func WithoutReturn(a, b int, f func(int, int)) {
f(a, b)
}
func main() {
WithReturn(1, 2, func a, b { a + b }) // Returns because type inferred from usage has a return.
WithoutReturn(1, 2, func a, b { fmt.Println(a + b) }) // Doesn't return because the usage doesn't.
// Fails to compile because the return types of the inside, namely (int, error), don't match the
// inferred return types from the usage.
WithReturn(1, 2, func a, b { fmt.Println(a + b) })
}
This seems extremely straightforwards to me. Am I missing some strange edge-case or something?
Comment From: zhuah
Personally, i still prefer the (a,b) => a + b
and (a,b) => {return a+b}
style syntax, parameter list should be contained in parentheses,
and we can only omit the return
keyword in first syntax, thus doesn't ambiguous the compiler.
Comment From: DeedleFake
thus doesn't ambiguous the compiler
As far as I know, the currently proposed syntax is not ambiguous for the compiler. The worry that some people have is that it could be ambiguous for a person reading the code.
Comment From: zhuah
If we want to support omit return
: func a, b {return a + b}
to be func a, b {a + b}
, then we may need to guess whether a + b
is a statement or an expression.
And also, the () =>
, () ->
, | | ->
style syntax is widely adopted in other languages, personally, it may better just use syntax people already know, instead of invent a new syntax.
Free to correct me if i'm wrong, thank you.
Comment From: ianlancetaylor
@zhuah The suggested new syntax is only permitted in cases where the type of the function is known from context, because it appears in an assignment statement or a function call. Therefore, we always know the result type. Knowing the result type lets us determine reliably whether a + b
is an expression in an implicit return
, or is a statement (in which case it will get an "unused value" error).
Comment From: griesemer
To make this discussion a bit more concrete, I ran a little experiment. Specifically, I adjusted the go/*
libraries to be able to parse and print two potential lightweight function literal notations:
func x, y, z { ... }
// func style(x, y, z) => { ... }
// arrow style
I also modified the gofmt
simplify mechanism (-s
flag) to rewrite eligible function literals into lightweight function literals. At the moment it considers function literals that are used in an assignment or as a function argument (note that gofmt
doesn't have type information and in rare cases it misidentifies candidates; e.g. arguments for testing.F.Fuzz
).
Additionally, if a lightweight function literal body consists of a single return statement, and if that return statement returns a single result, the simplifier removes the return
keyword. This is really independent to lightweight function literals but if applied only in the case of lightweight function literals it could help reduce boilerplate further without being too confusing. For instance:
func (x, y int) bool { return x < y }
could become one of:
func x, y { x < y }
(x, y) => { x < y }
(but if there were more than one result, the return
would have to be written).
Finally, I ran gofmt -w -s
(func style) and gofmt -w -s -a
(-a
for arrow style) over $GOROOT/src
. The results provide some idea of how Go code might look like if we had lightweight function literals:
CL 406076 (uses func {} notation, keeps all return
keywords)
CL 406397 (uses func {} notation, omits selected return
keywords)
CL 406395 (uses () => {} notation, keeps all return
keywords)
(These CLs were created last week, they may not be perfectly in sync with each other. Something to keep in mind for people trying to recreate the experiment.)
As of today, in the src directory there are - 4325 function literals; of these - 4119 (95.2%) function literals are likely eligible for a lightweight form.
(For this experiment, gofmt
does not descend into testdata
directories.)
Of the (rewritten) lightweight function literals - 529 (12.8% of 4119) have a function body consisting of a single return statement; of these - 468 (88.5% of 529) have a return statement returning 1 result, and - 61 (11.5% of 529) have a return statement returning 2 results. - 0 have a return statement returning 0 or > 2 results.
Some general observations:
- Almost all function literals could be written in lightweight form, making that form the de-facto form.
- Not having parentheses when using the func style notation can be confusing. It has been pointed out that the parentheses make it clear(er) that the function arguments belong to the function scope.
- The arrow style notation feels surprisingly readable to me. It is also familiar to people coming from other languages.
- Of the rewritten files, approx. 1/2 are test files (file names end in _test.go
).
- There are no syntactic ambiguities or other processing problems for a compiler with either of these notations.
To recreate the experiment or make modifications to gofmt
, apply the CL stack CL 406075, CL 406394, CL 406396, and CL 407376 (in order) on top of master. (I am not planning to maintain these CLs.)
Comment From: beoran
@griesemer Thanks for the experiment. It really goes to show how useful short function literals could be, no matter what the syntax.
I do feel that https://go-review.googlesource.com/c/go/+/406397, which omits return keywords is confusing to me. Often I can't really see clearly that a return was intended. So I would be against implicit returns.
As for the syntax, both func and stabby lambda seem ok. But is is true that the latter syntax exists in JavaScript, and quite a few JavaScripters are learning Go and for them it might be easier to learn like this. It is also even more brief than the func syntax.
Comment From: erudenko
I love swift like approach mentioned here: https://docs.swift.org/swift-book/LanguageGuide/Closures.html
for example, long-form:
reversedNames = names.sorted(by: { (s1: String, s2: String) -> Bool in
return s1 > s2
})
It could be transformed by inferring type context to
reversedNames = names.sorted(by: { s1, s2 in return s1 > s2 } )
and then using by an implicit return to shorter form:
reversedNames = names.sorted(by: { s1, s2 in s1 > s2 } )
and then by shorten argument names to:
reversedNames = names.sorted(by: { $0 > $1 } )
Which makes everything super short and simple for functional-style programming
Adding trailing closures could be even more incredible.
for example, if completion closure argument is the last argument of the function, I can write the code like that:
loadPicture(from: someServer) { picture in
someView.currentPicture = picture
} onFailure: {
print("Couldn't download the next picture.")
}
Comment From: beoran
@erudenko The Swift approach doesn't look very Go-like. I think we should focus on the two remaining proposed syntaxes, func and stabby.
Comment From: nate-anderson
@erudenko I don't like the Swift syntax because at first glance, your example looks more like a function definition than a function literal being passed to a function. That's also a huge departure from existing syntax, IMO.
Comment From: zhuah
Not having parentheses when using the func style notation can be confusing. It has been pointed out that the parentheses make it clear(er) that the function arguments belong to the function scope.
The arrow style notation feels surprisingly readable to me. It is also familiar to people coming from other languages.
Totally Agree.
Comment From: earthboundkid
I prefer the stabby version because as a reader, it’s clear when there’s an implicit return: when there are no brackets around the function body. It’s also familiar to me from JS.
Comment From: DeedleFake
I'm not personally a big fan of the arrow syntax, though if it keeps the {}
both with and without an implicit return then it's less of issue for me. I don't like having to add and remove the {}
along with the return
if I'm changing from one to the other.
That being said, I do completely agree that the func
syntax, though fine in isolation, doesn't really look nice in actual usage. For example,
slices.SortFunc(s, func a, b { a.Name < b.Name })
Despite the func
in there, the ,
in between a
and b
just kind of looks like it's separating two arguments to slices.Sort()
. I'm sure that I could get used to it, but why bother when there are better syntaxes available? I think that it just needs to be inside of actual delimiters so that I can scan it easily as a sub-list.
Another option, just to throw it out there, is a simplified version of the Kotlin syntax. It puts the arguments inside of the {}
, so the above would become
slices.SortFunc(s, { a, b -> a.Name < b.Name })
Ruby's syntax is similar, but it keeps the arguments and the arrow outside:
slices.SortFunc(s, ->(a, b) { a.Name < b.Name })
I'm not saying that any of these syntaxes are the right thing for Go, but my point is that you could do something just a bit different, whatever does work best for Go, and people will get used to it. Don't feel pressured to pick =>
just because it's very similar to JavaScript.
On a side note, like most languages that added shorthand function syntaxes after JavaScript did, I think that I prefer the thin arrow (->
) over the fat one (=>
), but I get why that might not be desirable in Go due to the similarity with <-
.
Edit: Actually, the more that I look at it the more that I kind of like the Ruby one. It keeps the arguments inside of parantheses, like a regular function declaration, uses {}
around the body and only around the body, and doesn't have an ambiguity because it prefixes with ->
instead of func
. It's not too different from (a, b) => { a.Name < b.Name }
, but I think prefixing with the arrow is more readable and more Go-like.
Comment From: marwan-at-work
In all the years I've written Go, I never once wrote a function (anonymous or otherwise) and thought to myself this is tiresome or could use reduction of syntax. Even more so today than before because officially supported tooling like gopls seamlessly auto-complete function signatures for programmers. For example, here's me writing an anonymous function for an errgroup:
https://user-images.githubusercontent.com/16294261/169444753-4f633595-b6ff-48e8-af69-9ec82932dd65.mov
Therefore, writability has thankfully gotten much easier even when the Go culture has largely favored readability given the idea of clear is better than clever.
A function is a function, whether it's a declaration, a variable assignment, or anonymous. The fact that all of them look like a function is because they are.
Comment From: DeedleFake
I'm seeing this sentiment about this being a decrease in readability for the sake of writeability and therefore a bad change quite a lot, and, as someone who has used Go as my general language of preference since it was doing weekly releases, it's rather surprising. To those who think that, I have a serious question: If explicit typing is inherently better for readability and readability always trumps writeability, are you O.K. with combo declaration-assignment type inference, such as a := doSonething()
and var a = doSomething()
, or do you always write your declaration-assignments as var a SomeType = doSomething()
? If you're fine with inference there, how is this any different?
I'm not entirely sure where this idea came from. Yes, readability is important, but adding more information doesn't always decrease it, and, even if it did, I don't think that Go has ever been about readability over writeability 100% of the time, but more about a nice clean balance between the two with a bit of erring in favor of readability.
Comment From: Skarlso
Readability is more important than writeability for the sole reason that 70% of the time you read the code more than write it. While this looks really neat, it doesn't make me parse the code better at a glance.
When I look at the code and see something like add := func(a, b int) int {return a+b}
I instantly parse this and move on.
Whereas I would see something line add := (a, b int) => { a+b }
it would take me a second to understand what I'm seeing and what ()
is and why {}
is and what the type will be and where the return is. Not to mention that it's like 0.5% of the time I write a small function which does a small thing. When I write such a function it's usually at least two lines.
So the compound effect of reading something that takes 1s to parse or an instant really adds up in the long run.
Comment From: aarzilli
Whereas I would see something line
add := (a, b int) => { a+b }
I'm not a fan of this proposal because I think small functions should be discouraged but this isn't a valid argument. That code is a syntax error because of the type in the argument list and, even if that wasn't there, the type of the short function syntax needs to be inferrable from the context, so it would fail to typecheck.
Comment From: Skarlso
Even worse. :D So this is apparently something that is accepted? (a, b) => { a + b }
? That is not something I would like to read and try to figure out on a daily bases. This is copied from the above examples.
Comment From: sbinet
sure, readibility is important. but you'll get used to the lightweight way of declaring anonymous functions.
the first real programming language I was taught at the university was Turbo Pascal. learning C++ was a bit annoying at first because the order of var/type was different (than Turbo Pascal). and learning Go was a bit confusing at first because the order of var/type was different (than C++). now it feels familiar (again) to have the variable's name and the variable's type.
the plasticity of the human brain is really something.
Comment From: beoran
@aarzilli That ship has sailed. As you can see from @griesemer's experiment, even the go compiler and runtime are full of small functions, and a shorthand for those would be beneficial and probably even more readable in the long run.
Comment From: aarzilli
@beoran I said "small functions should be discouraged", I didn't say they should be prohibited. If you look at that CL many of the functions that get rewritten are in fact not small and the benefit of the new syntax is small for them.
Comment From: deanveloper
I am curious what people's aversions are to small functions. I genuinely don't see what is hard to read about code like var add = (a, b int) => a + b
. Granted, I've written a lot of JavaScript, so this kind of thing is normal to see for me as it is extremely common in JS and React (ie const onClose = (ev) => setSomeValue(ev.someData)
. But if people could enlighten me about what's hard to read instead of just stating it as fact, that would be helpful. It seems like the reason is just because the syntax is new and eyes aren't used to it yet, but that will obviously change over time.
I would also like to mention, something to consider is that adding types to the argument list of a shorthand function is occasionally useful when resolving generic types. I'm not proficient enough in generic types in Go to attest to it here, but in other languages (ie Java), adding a type to the argument of a Lambda Function can help add context for type inference. Although Go already has anonymous functions, so if it's needed in Go, I guess we could say "just use func(...) syntax if you need types"
Comment From: aarzilli
I am curious what people's aversions are to small functions.
They generally come up in the context higher order iteration (things like map, filter, foreach, etc). Those are bad because:
- break, continue doesn't work with them
- return doesn't do what it should do
- they are very inefficient unless the compiler does a lot of work to rewrite them
Usually the argument goes that this might be less efficient but it is more readable. My contention is that this is usually not true, usually the explicit loop is easier to understand than a chain of higher order operations.
There's obviously situations where this is warranted, for example https://go-review.googlesource.com/c/go/+/406395/2/src/cmd/compile/internal/test/divconst_test.go is an obvious big win. But it is very unfortunate that it will encourage people to import bad habits from other programming languages. Javascript is actually a good example, the langauge itself is actually not that bad compared to the prevalence of bad habits in its community.
Comment From: Skarlso
I agree to this. And yes, https://go-review.googlesource.com/c/go/+/406395/2/src/cmd/compile/internal/test/divconst_test.go is a big win, I will grant it that. :)
Comment From: deanveloper
My contention is that this is usually not true, usually the explicit loop is easier to understand than a chain of higher order operations.
Can you elaborate on this? I'd love some examples of possible. Using these functions is often something that I like personally.
Another very common use-case for shorthand functions is for simple remappings from arguments of external types to a local ones, for instance func (x otherpack.SomeType, y otherpack.OtherType) { return myFunc(x.ID, y.Name)
, which can be shortened to (x, y) => myFunc(x.ID, y.Name)
.
Comment From: aarzilli
My contention is that this is usually not true, usually the explicit loop is easier to understand than a chain of higher order operations.
Can you elaborate on this? I'd love some examples of possible. Using these functions is often something that I like personally.
There's been several implementations of the lodash set of functions in Go. I'm not going to post any here but the body of those higher order iterators is only a few lines long. They usually only save you a couple of operations, for example:
r := Map((x) => {
return x + 1
}, Filter((x) => {
return x % 2 == 0
}, in))
is the same as this:
r := []int{}
for _, x := range in {
if x % 2 == 0 {
continue
}
r = append(r, x+1)
}
It's basically the same thing. Depending how you write it it can be fewer longer lines, but the complexity doesn't change, they are simple syntactic rewrites most of the time.
But from a newbie point of view you have to learn what Filter and Map are. Since these higher order iterators aren't fully general (i.e. there are loops that can not be represented with any combination of them) they have a tendency to multiply without bound. The haskell prelude for example contains the following: foldr, foldl, foldr1, foldl1, elem, maximum, minimum, sum, product, until, map, filter, head, last, tail, init, reverse, and, or, any, all, concat, iterate, repeat, replicate, cycle, take, drop, takeWhile, dropWhile, span, break, splitAt, lookup, zip, zip3, zipWith, unzip, unzip3 (deliberately skipped all the monad stuff).
Another very common use-case for shorthand functions is for simple remappings from arguments of external types to a local ones, for instance
func (x otherpack.SomeType, y otherpack.OtherType) { return myFunc(x.ID, y.Name)
, which can be shortened to(x, y) => myFunc(x.ID, y.Name)
.
I don't see any examples of this in Go source code. Not sure where this would come up.
Comment From: beoran
@aarzilli This proposal is for adding a shorthand for function literals, which are already in wide use. Most of them seem to be not for functional language simulation but as callbacks. Seeing how prevalent callbacks are, allowing them to be written more easily seems like a good idea. This proposal would in no way turn Go into Haskell.
Comment From: urandom
@marwan-at-work
In all the years I've written Go, I never once wrote a function (anonymous or otherwise) and thought to myself this is tiresome or could use reduction of syntax. Even more so today than before because officially supported tooling like gopls seamlessly auto-complete function signatures for programmers.
I completely agree. Writing function literals has never been a problem. Even if it takes longer to write them, you don't do it that often to justify having a shorter syntax.
Where a shorter syntax brings improvements is in readability. Having a callback-heavy piece of code suffers in readability due to the larger visual noise associated with the function literals. A shorter syntax would greatly improve this. I see the detriment of not seeing the types of arguments, however I would argue that this is a problem only with an unknown API, and is overcome rather easily, by diving deeper in the code. There might be evidence of this because we don't see a lot of complaints regarding type inferred variable instantiations, where similarly you have a variable with an unkown type being defined by a possible unknown functon call.
Comment From: NatoBoram
I like the idea of simply dropping type parameters in anonymous callbacks.
After all, you really don't care about what the type is, you just want to receive whatever it sends you and move on. If you have doubts about the type, the language server will tell you. Plus, func
gets auto-completion, so you don't need a shorter syntax, really.
compute(func(a, b) { return a + b })
s.Write(ctx, func(p) {
return p.SetData([]byte("Hello, "))
})
g.Go(func() {
return nil
})
Not only you don't care about its type, but you also can't care about it. Both input and return types are defined elsewhere and can't be changed unless you actively want to hinder yourself. You could put interfaces there, but... why?
Comment From: Skarlso
After all, you really don't care about what the type is,
If I wouldn't care about the type I wouldn't use a TYPED language! I care about the type in my typed language, thank you.
Comment From: atdiar
It's funny because afaic, I think that adding another syntax for functions is not beginner friendly. But I also have callback heavy code that would benefit from this, notably from function arguments type inference.
Although arrow functions look like their mathematical counterpart, when ES6 javascript got introduced, for some reason I wasn't too keen on it. For a beginner, every piece of syntactic sugar is more to remember.
So I don't know. This really is a tradeoff.
Comment From: switchupcb
That ship has sailed. As you can see from griesemer's experiment, even the go compiler and runtime are full of small functions, and a shorthand for those would be beneficial and probably even more readable in the long run. — @beoran
Fundamentally, justifying a shorthand for anonymous functions justifies a shorthand for error handling; which is also beneficial when an experiment is ran. However, historical decisions — from those error handling proposals — would not support this proposal because saving space (with a shorthand) isn't enough to justify a language change.
Personally, I don't have an issue with (x, y) => { return x < y }
. However, I do agree that non-lightweight function literals are easier to read.
Comment From: beoran
@switchupcb I can be mistaken but my impression was that the main concern of check error handling was that it it would introduce a hidden return-maybe statement. That is why I think keeping the return is important. But I agree that (x, y) => { return x < y } is an ok syntax for this.
Comment From: switchupcb
@beoran There are 100s of error-proposals.
- https://github.com/golang/go/labels/error-handling
Most get closed due to 3 reasons.
- Syntax that is "cryptic" (i.e _
, ?
, !
)
- Weird Complexity (i.e check, try, macros)
- Not enough benefits for the cost (of saving space).
This proposal may classify under number 3, but also 1 if someone argues that the inability to see parameter and return types are deemed cryptic.
Comment From: earthboundkid
Right now to make a recursive closure, you have to double declare, like
var f func(*html.Node)
f = func(n *html.Node) {
if n.Type == html.ElementNode && n.Data == "a" {
// Do something with n...
}
for c := n.FirstChild; c != nil; c = c.NextSibling {
f(c)
}
}
Being able to write
var f func(*html.Node)
f = (n) => {
// ...
would be a win.
For the stabby version, I'm assuming the grammar would work to drop the brackets around immediate return. The downside is that it could only support a single expression, but the upside is that the return would be "explicit" because of the missing brackets.
I don't like having to add and remove the {} along with the return if I'm changing from one to the other.
That is an annoyance in JavaScript (especially when you have to wrap returns in () to return an object), but it's pretty minor and worth it IMO.
Comment From: amnonbc
Please don't add this. I see this everywhere in Node code. It encourages pathological labda abuse, anonymous functions nestled deeply inside one another, a dozen levels of indentation, and unreadable 4000 line functions. Lets keep things clean, simple and idiomatic.
As the Zen of Python decrees: There should be one-- and preferably only one --obvious way to do it.
Comment From: earthboundkid
Most get closed due to 3 reasons.
- Syntax that is "cryptic" (i.e
_
,?
,!
)- Weird Complexity (i.e check, try, macros)
- Not enough benefits for the cost (of saving space).
My understanding is that the try proposal was dropped because it broke code coverage counts. I.e. coverage for try foo()
wouldn't be able to tell if the sad path was tried or not. That was the major factor that sank it. Once you internalize that requirement, then it's pretty clear why none of the other proposals went anywhere: they just end up being a second way to write if
. This is especially true if you try to figure out a way to allow for optional error wrapping.
This is very different. It only affects cases where the type information is totally redundant, and lets you omit the types. I would be against this if it were doing more complicated inference, but it's just a QoL improvement.
Comment From: earthboundkid
There should be one-- and preferably only one --obvious way to do it.
Python vs. Go aside, the beauty here is that gofmt can be upgraded to automatically rewrite the function to the one correct way.
Comment From: switchupcb
they just end up being a second way to write
if
. — @carlmjohnson
This proposal is just a second way to write an anonymous function literal.
Comment From: earthboundkid
This proposal is just a second way to write an anonymous function literal.
Right, but it is significantly shorter.
For the try proposals, even though it seems like there's a lot of redundant typing in if err != nil { return err }
, once you try to work out a way to allow error wrapping and preserve code coverage, there's no way to actually make it shorter, just a different order or different symbols. Even try { } catch err { return err }
is longer than if err != nil { return err }
.
Comment From: pelemarse
Stop taking ideas from other languages and plug them into a go programming language. The beauty of the Go programming language is simplicity. Why a hundred ways to do the same? This makes it harder to collaborate on the code, to read the code, more time wasted on which method to choose, and the result remains the same. LESS IS MORE!
Comment From: ChrisHines
Similar to the results of @griesemer's experimental data, I find test code a common place for function literals. That seems true in part because of t.Run
taking a function parameter, but also because function literals are a useful way to provide mock behaviors or to parameterize some behavior in table driven tests.
Some test code I recently wrote has some particularly long function literal signatures because they are abstracting two testing dimensions. It may be interesting to see how they change with this proposal.
After looking at the CL's from the experimental data, I also find the (a, b) => { ... }
syntax surprisingly readable, more so than the func a, b { ... }
syntax. So I will use the "stabby" style below.
For context, the code under test here is a package for logging errors returned from database/sql
queries in a standardized way. The specific logging package doesn't matter, so that is just Logger
here. Also, don't worry about why we have this code, we do, the point is the function literals it contains.
type txTestCase struct {
name string
expectBeforeSQL func(mock sqlmock.SqlmockCommon)
expectAfterSQL func(mock sqlmock.SqlmockCommon)
runSQL func(t *testing.T, ctx context.Context, log Logger, db *sql.DB, doQuery func(ctx context.Context, log Logger, qc QueryContexter) error) error
}
// WithTx executes doQuery within the scope of a single transaction.
func WithTx(ctx context.Context, log Logger, bt BeginTxer, doQuery func(context.Context, Logger, QueryContexter) error) error {
...
}
Here's an example of a txTestCase
in standard Go.
txWithError = txTestCase{
name: "TxWithError",
expectBeforeSQL: func(mock sqlmock.SqlmockCommon) { mock.ExpectBegin() },
expectAfterSQL: func(mock sqlmock.SqlmockCommon) { mock.ExpectRollback() },
runSQL: func(t *testing.T, ctx context.Context, log Logger, db *sql.DB, doQuery func(ctx context.Context, log Logger, qc QueryContexter) error) error {
var queryError error
err := WithTx(ctx, log, db, func(ctx context.Context, log Logger, qc QueryContexter) error {
queryError = doQuery(ctx, log, qc)
return queryError
})
if err != nil && !errors.Is(err, context.DeadlineExceeded) && !errors.Is(err, queryError) {
t.Fatalf("unexpected transaction error: %v", err)
}
return err
},
}
If I understand this proposal correctly it would allow rewriting the above test case as follows.
txWithError = txTestCase{
name: "TxWithError",
expectBeforeSQL: (mock) => { mock.ExpectBegin() },
expectAfterSQL: (mock) => { mock.ExpectRollback() },
runSQL: (t, ctx, log, db, doQuery) => {
var queryError error
err := WithTx(ctx, log, db, (ctx, log, qc) => {
queryError = doQuery(ctx, log, qc)
return queryError
})
if err != nil && !errors.Is(err, context.DeadlineExceeded) && !errors.Is(err, queryError) {
t.Fatalf("unexpected transaction error: %v", err)
}
return err
},
}
- Did I do that right?
- Do we expect the type of
doQuery
, a function literal parameter of another function literal, to get inferred? - The code is certainly a lot less noisy, it even fits in a GitHub comment without horizontal scroll bars (for me anyway).
- Is too much type information lost for the human reader?
Comment From: switchupcb
there's no way to actually make it shorter — @carlmjohnson
There are tens of error proposals that make the 3-line statement significantly shorter, such as those involving implicit return function calls (NOT try). This proposal may or may not involve implicit returns, and does remove type information from the parameters and results of a function. As you have stated,
it's just a QoL improvement. — @carlmjohnson
The point being that — at the current time — making something shorter (and showing its "benefits" via experiments) isn't enough to fundamentally justify the inclusion of a new feature to the language. Especially when that feature involves new syntax and removes explicitness.
Comment From: earthboundkid
There are tens of error proposals that make the 3-line statement significantly shorter, such as those involving implicit return function calls
Right but then those fail the two requirements of allowing wrapping and code coverage. That's the priority list: can it allow wrapping, get correct code coverage, and be shorter. There is no proposal that does all three. Most are just shorter and pass on 1 and 2.
The requirements here are: have clear and unambiguous type inference, have one way to do things, and be shorter. The current proposal meets all three by requiring explicit typing elsewhere (1), having gofmt rewrite to the optimal form (2), and 3 is from omitting the types.
Comment From: sirkon
The use case is primarily these Filter(…).Map(…)
I suppose? Because it is not bother me to write (IDE helps here) and this is rather what I like when reading to see full function definition instead of some birdie language.
List comprehensions don't look like a bad idea in these circumstances I am afraid. At least they will be a match for the performance of regular loops.
Comment From: switchupcb
There is no proposal that does all three. — @carlmjohnson
You're correct that error proposals involving functions fail to pass coverage tools that don't account for a new language feature. However, any backwards compatible proposal involving wrap-supported keywords, syntax, or if
-shorthand succeed in passing coverage tools: Since code coverage tools already (?) account for if err != nil
.
This proposal meets the requirements you set forth, but those requirements don't include fundamentals. We have agreed that this proposal is only a QoL improvement. However, we disagree on how it meets fundamentals: 1. Is it cryptic by removing explicitness (through the use of new syntax)? 2. Is it worth the cost of inclusion?
The context of 2 being that error-handling occurs more in codebases than anonymous functions.
Edit: Regarding gofmt
https://github.com/golang/go/issues/33113#issuecomment-511970012
Comment From: pdk
To pile on with a vote. I like the idea of omitting types in function declarations when those types can be inferred. I believe this follows perfectly in line with go's :=
operator. Strong type enforcement with type inference works. I think it's good that there be a single way to define functions: func
. It's short enough, and not too long. We don't need a second syntax for functions. (We don't need 3 ways to say "loop".) In my experience it's just having to put in all the parameter and return types that is annoying, because it's usually pretty clear from context what the types should be. Also, if there is a situation where types cannot be inferred, it's simple to add in the types with func
, rather than having to rewrite the entire expression in another syntax.
Comment From: griesemer
@ChrisHines Re your https://github.com/golang/go/issues/21498#issuecomment-1133046748
Did I do that right?
Looks right to me.
Do we expect the type of doQuery, a function literal parameter of another function literal, to get inferred?
Yes, doQuery's function type is known in full from the type of the runSQL field.
The code is certainly a lot less noisy, it even fits in a GitHub comment without horizontal scroll bars (for me anyway).
Ack.
Is too much type information lost for the human reader?
I think that is the question we need to answer.
Comment From: griesemer
@pdk 's observation seems spot-on: a lightweight function literal is a short form of a function literal the same way a short variable declaration is a short form for a variable declaration. In both cases the absent types are directly known (essentially copied, there's really not much "inference" going on) from the assignment context. In both cases we have a choice between the long and the short form, and one might chose the long form where readability is suffering otherwise. And in both cases the short form is (or could be, for short function literals) the prevalent form in code.
To push the analogy a bit further: similar to how we go from, say
var x int = 0
to
x := 0
(notice the drop of the keyword var
and the type in favor of the token :=
), we might go from:
func(x int) { ... }
to
(x) => { ... }
(again, notice the drop of the keyword func
and the type in favor of the token =>
).
Comment From: jimmyfrasche
As far as the complaints about =>
being overused in js, I'd note that there are some additional js-specific reasons for that which don't apply here:
=>
has better semantics thanfunction
as it bindsthis
to the current lexical scope, making code much easier to reason about- since it's a dynamic language, you can always use
=>
overfunction
In this proposal, - the semantics are the same: it's just a way to elide boilerplate - short functions would be limited to expressions where the type is already known (and hence your IDE can always report the elided types)
Comment From: pdk
Examples matter. :)
I was actually trying to say that I prefer going from
f := func(a string, b int64, c map[string]MyStruct) (OtherStruct, error) { ... }
to
f := func(a, b, c) { ... }
Basically, without introducing more syntax, just support more type inference.
Comment From: griesemer
@pdk Unfortunately we cannot simply leave the types away:
f := func(a, b, c) { ... }
is a perfectly valid Go function literal that takes three unnamed parameters of types a
, b
, and c
. We do need another difference to identify this as a short function literal, e.g., by leaving away the parentheses, or by choosing a different notation (such as =>
). Of course there are many possibilities.
Comment From: magiconair
@griesemer I would argue that the cognitive load on parsing x := 0
is significantly lower than parsing (x, y) => {...}
since {...}
can be arbitrarily complex. And that is the catch with this proposal. I get the same feeling as with the one-line error handling attempt that this will not increase readability since {...}
is by definition a complex expression. Yes, we have multiple ways of defining a variable but in almost all cases this is just defining a simple type.
@pdk 's observation seems spot-on: a lightweight function literal is a short form of a function literal the same way a short variable declaration is a short form for a variable declaration.
Comment From: zigo101
Re-declaration is already one confusion source in Go programming. Do we need another?
Comment From: fzipp
by leaving away the parentheses, or by choosing a different notation
I'd prefer to keep the func
keyword and the parentheses. My choice would be a colon, similar to how the colon in :=
adds type inference / shortness to =
.
func(a, b, c): { return a + b*c }
func(a, b, c): a + b*c
Comment From: magiconair
I looked at https://go-review.googlesource.com/c/go/+/406395 and this is one of the rewrites. I don't see the win here but we're losing the type information. Yes, the compiler can infer them but it helps the developer as well, doesn't it? How is this better?
Comment From: marwan-at-work
I think the difference between x := 0
and (x) => { ... }
is that the programmer knows what x is in the former example but not in the latter one. Of course, you can do x := someFunc()
and we'd lose the ability to know the type of x but I'd argue that that's unfortunate and we should have less of it and not more.
Comment From: DeedleFake
@magiconair
I would argue that the cognitive load on parsing x := 0 is significantly lower than parsing (x, y) => {...} since {...} can be arbitrarily complex.
The complexity of { ... }
is irrelevant to the type information, though. This proposal is for the shorthand function literals to infer type info only from the outside in, not the other way around, so the type info comes only from the usage of the literal, not the contents of the body. That's why a := () => { 2 }
would be illegal, even though the type of the function can theoretically be entirely figured out. The anonymous function in slices.SortFunc(s, (a, b) => { a.Name < b.Name })
would be func(T, T) bool
because that's what slices.SortFunc()
expects, not because of the contents of { ... }
.
Comment From: natefinch
There are two main differences between x := someFunc()
and someFunc((x, y) => {x+y})
:
The first is the level of indirection. The return type is part of the "outer-most" information about someFunc. For the short assignment form, this is just one level of indirection. x is an int if someFunc returns an int.
There are two levels of indirection with the short function declaration. You have to read the declaration of someFunc and then the type of the argument and what types it takes as an argument. That's a lot more cognitive overhead. And actually it's worse than that, because you also have to infer the return type that is being returned. In the above, are x and y ints or floats or strings? You don't know. It could be even worse if it was someFunc((x,y) => {x.Parse(y)})
The second difference is the inherent complexity of lines of code that use this. Variable assignment is the most simple concept of programming. Passing an inline-declared function literal is a lot more complex.
Making simple code more implicit leverages the simplicity of that code - we have some wiggle room to add a little cognitive overhead to save some visual clutter. Making complex code more implicit is taking code that is already complicated and making it more difficult to understand. That's making hard code worse.
I actually like that the current syntax is slightly unwieldy. Passing around functions is cognitively complicated, and I don't mind having some slight deterrent to doing it willy-nilly.
In the CLs above by @griesemer - almost every single one seems worse. If I didn't have the before code, I would have a really hard time knowing what that code was supposed to be doing, mostly due to lack of types in the arguments.
Finally, the short form pushes devs to use single letter variable names for brevity, which just further obscures what the code is doing.
Comment From: ChrisHines
@natefinch
Finally, the short form pushes devs to use single letter variable names for brevity, which just further obscures what the code is doing.
You might be right, but I don't think we should jump to that conclusion too quickly. If the types take up less space on the line maybe we have more screen budget for parameter names. I certainly notice how much easier it is just to see all the parameter names in my example above. I don't think it creates any pressure to make them shorter.
Maybe people will try harder to fit the whole function on a single line and that will encourage shorter parameter names. I don't know how bad that is for one line functions, though, since the variables have a tiny scope.
I think it is hard to predict how our collective behavior would evolve with this proposal in place.
Comment From: switchupcb
I don't think we should jump to that conclusion too quickly.
I think we should.
Comment From: magiconair
@DeedleFake what I meant was also what @natefinch just stated that mentally parsing a variable declaration is much simpler than a function declaration since I don't have to read it since there is way less information in it. It's a counter argument to the shorthand function syntax being similar to a shorthand variable declaration.
Most of the examples given for the sort.Slices
-like functions look reasonable but when you look at the CL of what actually got replaced there is a lot of information omitted which you have to re-construct when reading the code. I don't see the benefit here just to make the sort
one-liners easier to write.
Again, how is this example here a useful simplification? How is this easier to read? What problem does this solve?
Assume for a moment that I am not aware that I am reading a snippet from a _test.go
file. In the first case this becomes immediately obvious. In the second case not so much. It just occurred to me that we exchange a lot of code snippets via screenshots on Teams for example. This context would get lost.
Comment From: magiconair
@ChrisHines it would do one thing for sure: It would initiate the discussion within and between teams which style is acceptable and when. Or with gofmt
enforced styling there wouldn't be much choice - or it would lead to configurable gofmt
forks. Either of these options seem very unattractive. The lack of choice is the guarantor for not having to have these discussions in the first place.
I think it is hard to predict how our collective behavior would evolve with this proposal in place.
Comment From: ChrisHines
@magiconair I agree with you regarding the syntactic question, but I think the choice (or lack of choice) between using the current syntax or the short form is a different choice from the names of the function parameters. My last point was that the short function literal syntax does does not necessarily lead to short parameter names.
Comment From: ianlancetaylor
@magiconair Re: https://github.com/golang/go/issues/21498#issuecomment-1133318476
I think this is one of the examples where the short form is clearly helpful. Yes, in the absence of any contextual it provides less information. But for myself, just looking at the use of b.Run
and b.RunParallel
I know that this is almost certainly benchmark code. The explicit types testing.B
and testing.PB
are unnecessary boilerplate. And, frankly, even when present they don't make the code much clearer.
I'm not saying that the short form is always clearer to read. But I think that in this particular example, it is.
Comment From: hherman1
What if short function syntax was only allowed for single statement functions?
i.e this is allowed:
(x) => x + 1
But this is not:
(x) => {
Y, err := f(x)
…
}
The benefit of this restriction is that it resolves ambiguity and you still get most of the benefit. Short form functions are mostly useful for very short transformations anyways, and then there’s no question of when you should use one syntax vs the other. The short syntax is probably almost always better for short functions anyways, and the long syntax is usually better or not-that-much worse for complex functions.
@griesemer I’m interested in your opinion of this.
Comment From: atdiar
The only thing I wonder is why the examples are mostly test code, unless I'm mistaken. I think we should have library examples which force callback usage by developpers. I think I'd rather optimize for that rather than tests. I do have a use-case myself of such library (for event-handling and mutation observing à la javascript) and I had actually thought, while designing it, that type inference would be nice.
Comment From: griesemer
@hherman1 (x) => x + 1
seems like a win over func (x int) int { return x + 1 }
but judging from the code in the std library, this is not really common Go code. The closest are comparators of the form (i, j) => s[i] < s[j]
or the like (using your suggested notation without {}
), but even of those there are much fewer than I expected. So only allowing short function literals for those cases may not be worth it.
More generally, in code where the same form of function literal is used repeatedly (example), not having to repeat the signature over and over again seems like a win to me, somewhat irrespective of the function's body size. In code where a certain function signatures is common, almost no matter how complex or simple the signature, it's probably a win if the details can be left away, because they don't change. A reader will need to familiarize themselves with them once and that's it. This is true for instance for test or benchmark runs in test code which always expect a *testing.T
or *testing.B
argument. There's really not much gained here by repeating those types over and over again. (It's noteworthy that most function literals where in _test.go
files which are notorious for repetitive but mostly simple code.)
So my answer is "it depends". I would chose one or the other form based on specifics of the surrounding code. Being able to leave away repeat boilerplate allows the relevant code to become more visible and thus (in my mind) improves readability.
Comment From: DeedleFake
@atdiar
I've made pull requests to introduce the arrow syntax for two of my current projects that have made pretty heavy use of callbacks, far more so than is common in the Go standard library, I think. You can find them here and here. I think that a few things were made slightly less clean by it, but most either had essentially no change or were improved, and in a few cases I think that the improvements were quite significant.
Edit: An example of, in my opinion, a fairly significant improvement.
Edit 2: Another significant improvement.
Edit 3: Here's a potentially bad change, though you could easily just not change this particular type of case.
Comment From: jeffreydwalter
This is a garbage proposal which solves no real world problem. For over 10 years the current syntax has worked in all cases. Please stop adding unnecessary complexity to a beautifully simple language. Generics is bad enough; but at least it has some real-world utility beyond "it saves me some typing". If you don't like the language, go program in something else, pun intended.
Comment From: mojixcoder
Actually today I searched any vs interface{}. Just don't make it JS. Having so many ways to do one thing just makes code inconsistence and harder to maintain by teams with developers with different tastes. Simplicity is not always having less lines of code.
Comment From: DeedleFake
@jeffreydwalter
What 'real-world utility' beyond decreasing typing does any high-level language feature really provide? You could argue that about essentially everything. From that point of view, the very existence of high-level languages is simply saving typing over just writing assembly directly, is it not? After all, all the compiler really does is convert that high-level language to assembly, so why not just write that assembly yourself?
Edit: I wanted to clarify a bit. Obviously some language features are overkill. Some language features hurt a language far more than they help. But a language feature being new, while certainly not an argument for it, is hardly an argument against it, either, and a language feature being new doesn't necessarily mean that it'll add complexity, either.
Generics also technically have no real utility besides saving typing. Everything that they do can also be done in a type-safe manner by rewriting absolutely everything to handle any given type. All generics really does is do that rewriting for you.
Comment From: ianlancetaylor
@jeffreydwalter Please follow the Go Community Code of Conduct. Please be respectful. Thanks.
Comment From: griesemer
Also, just to be clear: no decisions have been made here nor is there any pressure to make any decisions. The point of this discussion is exactly to find out the merits and disadvantages of this proposal and what exactly should be done, if there's something to be done. Thanks.
Comment From: jeffreydwalter
@DeedleFake of course it saves you typing, but it also generally makes it easier to grok and faster to accomplish a programming task, not by saving you a few characters but by abstracting concepts. In this case, changing a perfectly clear function declaration like: func() { ... }
to a more cryptic syntax like: () => { ... }
(which only really saves 2 chars btw) doesn't actually accomplish anything other than appealing to a subset of the population who is used to that syntax from other languages. #DontJavascriptGo
Edit in response to @DeedleFake's edit above: I completely agree and strongly feel that generics in Go were a mistake. Capitulation by the core team to this exact process.
Comment From: DeedleFake
changing a perfectly clear function declaration like:
func() { ... }
to a more cryptic syntax like:() => { ... }
(which only really saves 2 chars btw) doesn't actually accomplish anything
I completely agree. That particular change doesn't do anything useful. But how about changing func(a, b *ipnstate.PeerStatus) bool { ... }
to (a, b) => { ... }
? I think that improves readability, as there's less mostly-useless boilerplate to process and immediately discard. Or even better, how about changing
view.onRequestMoveListener = surface.TopLevel().OnRequestMove(func(t wlr.XDGTopLevel, client wlr.SeatClient, serial uint32) {
server.startMove(&view)
})
to
view.onRequestMoveListener = surface.TopLevel().OnRequestMove((t, client, serial) => { server.startMove(&view) })
Even changing the three lines to one still results in a line that's shorter than the boilerplate causes the first line to be on its own in the original.
Comment From: jeffreydwalter
@DeedleFake
Why do we need types at all in Go?!? Let's just make a dynamically typed language!
Comment From: deanveloper
@jeffreydwalter I mean, we already use type inference a lot in Go. I don't see what's wrong in this case either. Nearly every modern language uses type inference, and has a shorthand for anonymous functions which use type inference for arguments/return values. Not sure what you're getting at here.
Edit - Would also be nice to converse without the weird rhetorical punctuation and hashtags. They just come off as uncivil and rude
Comment From: jeffreydwalter
@deanveloper
weird rhetorical punctuation and hashtags
🙄 got it! Sorry if I offended you with my hashtag ❄️ . 😂
Over the years I've seen this mentality of, "we need X because EVERY other modern language has it", ruin several other languages (PHP, Javascript, c++, etc.). It's a distraction and I hate to see Go get ruined like that. That's all I'm trying to say here.
Comment From: DeedleFake
I hate to see Go get ruined like that.
I don't want Go to get ruined like that, either, but I don't think that this change is a step towards that. I'm not against pulling features from other languages if I think that the features will make Go better. I'm only against it when it's being done simply because another language has it and therefore so must Go. I do not believe that to be the case here, however.
Comment From: jeffreydwalter
I too am all for improving Go. Adding a new more terse way of declaring a func is not it ihmo. It's a slippery slope.
Comment From: seankhliao
In current go code, variable names are often short, but this is still readable when it's a function argument which has types to expand on that name, and when it's used in a short declaration, assuming a descriptive function call. These lightweight functions remove the reference point to support the short names, moving them to a potentially far away place (declaration of the context), increasing the friction of reading code. This might be fine for very common uses (testing, benchmark runs) or a codebase you're already familiar with, but as someone who primarily has to read code often in unfamiliar codebase, I'm not a fan of losing this type information up front.
On the proposed syntaxes, I feel a bit uneasy on the invisibleness of return (types), especially in cases of multiple return values.
Comment From: freeformz
my2c
If something like this does happen, at least for me, I'd like gofmt to include a space at the beginning as well as at the end of the anonymous function to add some visual padding ...
So a line this would actually be:
cg.Add(a.peers.Listen( (peers) => {
And a line like this would be:
quitAction.ConnectActivate( (p) => { a.Quit() } )
For my, perhaps poor eyes, ensuring there is a space at the beginning and end makes it easier to visually parse/call out the anonymous function.
I can understand why others would be -1 on this suggestion though and perhaps over time I'd learn to see the ((
s more easily.
Comment From: jeffreydwalter
☝️ yet another example of why this proposal is problematic.
Comment From: beoran
@jeffreydwalter Some slopes are indeed sIippery, but I don't think this is the case for this proposal. If accepted, what do you think would the other negative effects could be?
I think the point that this proposal seems to be most applicable for testing is a actually a great benefit. It could encourage people to write more tests, since the annoying boilerplate is gone.
Comment From: jeffreydwalter
I think people that don't write tests aren't going to magically start because you added a second way to declare a function. 🤷♂️🤣
Comment From: zhuah
Why insist on "one way", if there is a new better way in some cases? Go can do anything with current syntax, does it mean we shouldn't adds any new language feature in the future?
Language can also be think as a library for developer, we should also improve the API if possible.
Comment From: zhuah
Error handling, sum types, string literals, there are so many people complain with those problems.
Comment From: FiloSottile
I did a quick skim of the changes under src/crypto/
of CL 406395 (because that's the code I am familiar with) and found it less palatable than I expected, at least as a default for non-trivial bodies.
A lot of the changes are just func()
to () =>
(arguments to sync.Once.Do
, time.AfterFunc
, testing.AllocsPerRun
) which does not feel more readable or clearer to me, and saves only one character. Anecdotally, I remember not being sure what () =>
meant the first time I encountered it in Javascript.
In the other changes, I deeply felt the loss of visible argument and return value type information.
For example, turning
forEachSAN(sanCert.getSANExtension(), func(tag int, data []byte) error {
into
forEachSAN(sanCert.getSANExtension(), (tag, data) => {
requires going to the definition of forEachSAN
to figure out what tag
and data
and the return value are, or going to the site where they are used or a value is returned and guessing. Or using the IDE features, if you are in one.
I think this is a meaningful information locality loss, which hurts readability.
vscode-go (maybe through gopls) somewhat recently added support for autocompleting function literals. After typing forEachSAN(sanCert.getSANExtension(),
it will suggest func(tag int, data []byte) error {}
. I love love love that feature, and I wonder how much it makes this language change less valuable at writing time.
Generally, I am always in an IDE when writing, but not when reading (because regrettably writing tools are more developed than reading tools), so I like a solution that relies on the IDE at write-time (function literal autocompletion) more than a solution that relies on the IDE at read-time (hover for the type information).
Comment From: leaxoy
Another suggestion, just omit return
and brackets {}
for one line expression.
For currying function4 with four arguments. The signature is:
func (f Fn4[A, B, C, D, R]) Curry() func(A) func(B) func(C) func(D) R
Current solution:
return func(a A) func(B) func(C) func(D) R {
return func(b B) func(C) func(D) R {
return func(c C) func(D) R {
return func(d D) R {
return f(a, b, c, d)
}
}
}
}
With return and brackets:
return (a) => { return (b) => { return (c) => { return (d) => { return f(a, b, c, d) } } } }
Omit return:
return (a) => { (b) => { (c) => { (d) => { f(a, b, c, d) } } } }
Omit both:
return (a) => (b) => (c) => (d) => f(a, b, c, d)
Or omit some =>
between lambda chain, like below:
return (a) (b) (c) (d) => f(a, b, c, d)
For multi line expression, return
and {}
can't omitted.
return (a) => (b) => (c) => (d) => { doSomething(); return f(a, b, c, d) }
Comment From: hherman1
Suppose we still required type annotations but allowed the omission of func
and return
? Also, would be nice to allow multiple returns
e.g (x int) => x/2, x+1
@griesemer the example you linked in response to my prior comment seems like it would be satisfied entirely by my proposal if you allow multiple returns.
Comment From: aarzilli
@hherman1 that syntax is ambiguous: f((x) => x/2, x+1)
how many arguments are passed to f?
Comment From: esimov
The arrow function is a bit r(R)usty to me. I consider of using an anonymous function like the proposal below more closer to the Go syntax rather than using an arrow function which is another layer of abstraction.
f := func(a, b, c) { ... }
instead of
(a, b, c) => { ... }
Comment From: deanveloper
@esimov that is already valid Go syntax, where a
b
and c
are types
For instance,
var x = func(int, bool) { ... }
Comment From: nathj07
I think this will reduce clarity in code. Go has incredible glancability and we should work to keep it that way. I fear this sort of syntax will add confusion to code bases making it more difficult to share code and understand libraries
Comment From: deanveloper
@nathj07 why do you say that? we have type inference in Go today, why is this type inference different?
Comment From: yazver
Why not:
(x, y){x > y}
Comment From: devuo
After reading the proposal and comments, I went from thinking that this could be a convenient thing to have and therefore "heh, why not?" to this is going to make the reading of code authored by others unnecessarily hard, as @seankhliao and others correctly pointed out in this thread.
While I like the terseness of the arrow function and the suggested typing inference, I can also fully understand how it does not fit with the go that we have come to know and like, and how this proposal if adopted can both negatively impact the go ecosystem and teams in general (unnecessary bike shedding, inconsistency, readability, etc.).
I think this proposal will create more issues than what it's worth.
Comment From: hherman1
Another idea:
- types are still required
- Multiple return values allowed
- Return keyword skippable
- Single statement form only
Eg
(x string) (int, error) => load(x)
is allowed
I think this is interesting because on the plus side it:
- retains type signatures for readability
- Is not a general purpose alternative to the func keyword
however, it’s only marginally shorter than using the func syntax, so it’s not clear to me it would be worth adding.
Curious what others think
Comment From: nomad-software
The rhetoric promoting this change would allow a ternary operator for conditional expressions too.
func Foo(b bool) {
return b ? "foo" : "bar"
}
Look it's so simple and easy to parse.
Where do we draw the line to say many ways of doing the same thing is bad?
When the three of us got started, it was pure research. The three of us got together and decided that we hated C++. We started off with the idea that all three of us had to be talked into every feature in the language, so there was no extraneous garbage put into the language for any reason. - Ken Thompson
Clear is better than clever. - Rob Pike
I vote No to "lightweight function literals".
Comment From: batara666
Clear is better than clever.
Comment From: ncubrian
I vote No to "lightweight function literals" and lambda expression.
Comment From: changkun
The objective of this proposal may not be clear enough. I would consider this proposal aims to improve typing experience and readability.
In terms of a programmer's typing experience, filling in a complete function signature may indeed be a lot of keystrokes. However, this issue has been gradually improved with the current toolchain support, such as gopls
.
In the above figure, gopls
enable auto-completion and can fill the entire function signature by typing one character f
. In this case, the func
keyword plays a key role. If we adopt the scalar syntax, the auto-completion may not be smart enough to figure out if the user means to type a bracket (
, or want to start with a function.
It is true that there are Go users who do not use auto-completion, and it would be interesting to see the actual data.
Regarding readability, The lightweight syntax may reduce the existing readability of parameter types, as well as the return type. Consider this example:
func (x, y int) bool { return x < y }
could become one of:
func x, y { x < y }
(x, y) => { x < y }
We might generally ask: What are the type of x and y? and what is the returned type? For the implementer, it is clear with the hint from the required function signature. But for its reader, it would be less intuitive to understand this information at a first sight.
The above observation align with https://github.com/golang/go/issues/21498#issuecomment-1133509432.
Comment From: nathj07
@nathj07 why do you say that? we have type inference in Go today, why is this type inference different?
@deanveloper this type inference seems to me to be an explicit reduction in clarity. What we have so far is convenient and doesn't reduce readability. Fro the proposal, and the examples given, I don't see a reason to add this complexity to the language and therefore the code that would use it. This is a somewhat personal position rather than a strictly technical one, but it feels.like a deviation from standard Go best practices of favouring explicit clarity
Comment From: dolmen
TL;DR
Currently creating an anonymous is limited by the possibility of referencing named types in the function signature.
I think this behaviour must be preserved even with a new function syntax that allows omitting type: it still must not be possible to create an anonymous function that references types private to another package.
Concrete example
Here is a foo
package that use the functional options pattern.
package foo
type config struct {
Name string
}
type Option func(*config)
func Name(name string) Option {
return func(cfg *config) {
cfg.Name = name
}
}
type Foo struct {
config
}
func New(opts ...Option) *Foo {
var foo Foo
for _, opt := range opts {
opt(&foo.config)
}
return &foo
}
As config
is private to the foo
package, nobody outside the foo
package can create a function that can be converted to Option
(except maybe with reflect
tricks).
Here is an example with the existing func
syntax:
package main
func main() {
// ok
_ = foo.New(foo.Name("xx"))
// doesn't compile
_ = foo.New(func(*foo.config) {})
// doesn't compile
var _ foo.Option = func(*foo.config) {}
}
Any implementation with a new func
syntax should preserve the behaviour. The following code should be rejected by the compiler as well because the anonymous function references the private foo.config
in its signature even if not explicitely mentioned in the source.
// should not compile despites foo.config not being visible in the source
_ = foo.New(func x { x.Name = "bar" })
_ = foo.New((x) => { x.Name = "bar" })
// should not compile
var _ foo.Option = func x { x.Name = "bar" }
var _ foo.Option = (x) => { x.Name = "bar" }
Comment From: gazerro
It is usual to read:
x := f()
and we don't think it should be rewritten by making the type explicit
var x int = f()
nor does Go encourage the explicit type. For example the following code is not valid in Go
var x int, y string = f()
we should write
var x int
var y string
x, y = f()
So I wonder why then Go forces you to specify the types of parameters when they can be inferred? Why not making the type explicit in a parameter is less readable than not making it explicit in a variable declaration?
Comment From: magiconair
@gazerro because this is not about type inference but about a more compact syntax. The disagreement is that it reduces readability too much and introduces ambiguity without providing a clear enough benefit to justify this.
Comment From: matttproud
A couple of observations about the proposed change:
-
Multiple Morphologies for Similar Things: One observation working with teams onboarding to Go is that learners get tripped up when encountering nearly-duplicative patterns that achieve nearly the same effect:
v := &T{}
versusv := new(T)
. (I know these aren't always exactly equivalent, but often they are functionally speaking so.) Such duplication leads to a fair bit of needless debating between users, so the proposed shortened form versus existing inline functions could easily lead to such confusion and friction in yet another area. -
Confusion about Whether Method and Function Values Can Still Be Used: Similar to the previous, if we introduce a new syntax, I have doubts to the extent that new Go language learners would realize they could use classical method and function values in place of the anonymous syntax. I see a lot of folks do something akin to this already, and I can imagine a new anonymous syntax would make such a problem worse:
``` type T struct { F func() error }
func f() error { / omitted / }
func g() { var v T v.F = func() error { return f() } } ``` when this would be preferable
func g() {
var v T
v.F = f
}
-
Difficult to Read and Maintain: We maintain a huge monorepo for our Go projects, and it cannot at this moment take advantage of external tooling (e.g.,
gopls
) due to alternative package layout. I only state this for background. Having said this, we often execute large scale changes (LSC) using two mechanisms in Go: -
using classical stream-based editors like
sed
andawk
, etc. This covers a lot of bases. - writing ad hoc transformers using
package dst
for non-trivial rewrites This works well thanks to the simplicity of the language specification.
We almost never need example-based refactoring tools. Either dirt-simple or somewhat advanced suffices. For the case of stream-based editors, my gut instinct tells me that would make large-scale code maintainance more difficult due to the lack of a keyword to anchor such editing patterns on. It's not merely looking for func
and some pattern but now looking for stop words in addition to func
to mean something anonymous. That would blow up the complexity in cases of trying to perform refactorings that require finding or changing something involving a specific function signature.
And even in the case of using something like package dst
, I fear we would then have to match on more than just FuncDecl
and FuncLit
, so complexity rises here.
From my view, I'd prefer not to have Go introduce a shortened form due to these increased costs. It will hurt learners and maintainers. There are probably more cases than above. I don't say this out of fundamental negativity or change aversion but rather concern for our users.
Comment From: evanmcclure
This proposal and thread bores me.
Comment From: tv42
I know it would break go1compat, but I really wish we could decide that func(int, string) {}
is now illegal and must be expressed as func(_ int, _ string) {}
, and then allow func(a, b, c) {...}
to do type inference. That's the only alternative mentioned so far that looks like Go to me, and isn't in conflict with practically any Go code I've ever read.
Comment From: deanveloper
@tv42 An alternative could be to use _
as a placeholder for the type instead of the variable. I like your solution better though.
func foo() {
var s1 []somepack.User
var s2 []somepack.AdminUser
slices.EqualFunc(s1, s2, func(e1 somepack.User, e2 somepack.AdminUser) bool { return e1.userID == e2.userID })
// becomes
slices.EqualFunc(s1, s2, func(e1 _, e2 _) _ { return e1.userID == e2.userID })
}
Comment From: allochi
IMHO, and I don't mean to undermine anyone, but from a person who actually refrain from using the operator :=
as much as possible and uses var x = ...
, I would prefer not to have the short anonymous function syntax.
I may regret not having it sometimes, but surly I don't miss it most of the time, and I find it against the explicitness of Go where writing clear and communicative code is for multiple readers than one writer.
Between the first and the second examples below, I prefer the first, call it Stockholm Syndrome after years of wring Go code, I still prefer the explicit and type communicative style.
// First
slices.SortFunc(slice, func (a int, b int) bool { return a < b })
// Second
slices.SortFunc(slice, (a, b) => { a < b })
When I find myself with an anonymous function larger than a line or two, I usually move it out, give it a name that communicate its purpose, couple of years later I know why I wrote it. Most of the time I wish if JavaScript was more like Go not the other way around for that same reason.
Besides, do you really want to read code like this?
http.HandleFunc("/endpoint1", (w, r) => {
...
...
})
http.HandleFunc("/endpoint2", func(w http.ResponseWriter, r *http.Request) {
...
...
})
http.HandleFunc("/endpoint3", (w, r) => {
...
...
})
http.HandleFunc("/endpoint4", func(w http.ResponseWriter, r *http.Request) {
...
...
})
I don't, this is why (in a way) I write mine like this
http.HandleFunc("/endpoint1",endpoint1)
http.HandleFunc("/endpoint2",endpoint2)
http.HandleFunc("/endpoint3",endpoint3)
http.HandleFunc("/endpoint4",endpoint4)
func endpoint1(w http.ResponseWriter, r *http.Request) { ... }
func endpoint2(w http.ResponseWriter, r *http.Request) { ... }
func endpoint3(w http.ResponseWriter, r *http.Request) { ... }
func endpoint4(w http.ResponseWriter, r *http.Request) { ... }
I share my opinion here and I feel I'm a little unfair, as I do like the type inference when initializing an array of struct
, but only because the type is communicated in the array type
type Option struct {
Name string
Active bool
}
var options = []Option{
{"Option1", true},
{"Option2", false},
{"Option3", true},
{"Option4", false},
}
Now, if this proposal finds its way into Go, I only wish that we have a format that looks more like Go than another language, without the =>
, dropping the return keyword or have a weird syntax, maybe keep func
and parameter names, just infer the types
slices.SortFunc(slice, func(a, b) { return a < b })
http.HandleFunc("/endpoint3", func(w, r) {
...
...
})
Finally, thanks for the Go team for giving us one of best languages and the opportunity to share our opinions about its future.
Comment From: DeedleFake
@jeffreydwalter
I completely agree and strongly feel that generics in Go were a mistake. Capitulation by the core team to this exact process.
I disagree 100%. Generics has been nothing but useful, and while admittedly they haven't really hit particularly widespread usage yet, I have yet to run into a situation where it made things less clear. If anything, it cleared things up by eliminating clutter from repeated types and functions that do the exact same thing as each other.
Your assertion that their addition was 'capitulation' seems completely off-base to me. From everything that the Go team has said on the subject, they've wanted generics for as long as and as much as anyone else, but they just wanted to make sure that they could add them in a way that fit well with Go. Even Rob Pike, who has outwardly been the most steadfast proponent of slow, thoughtful changes of all the developers who have been on the Go team and whose famous less-is-more approach is something that I am a huge fan of, approved of the idea of generics and mentioned during a talk a while back that he liked the possibility that a chans
package could be created. Generics were not something that was forced onto the developers at all, and neither will this proposal be if it is added.
Edit: Found the Rob Pike talk that the quotes in the blog post are from.
Comment From: zhuah
@jeffreydwalter
I completely agree and strongly feel that generics in Go were a mistake. Capitulation by the core team to this exact process.
My private library heavily relies on generics to provide a user friendly API, instead of tons of interface{}
, and i even adopt generics in my code before Go1.18 was released.
Comment From: burdiyan
Just recently I was digging into some Rust code, and was pondering on “why on earth different syntax for lambda-style function? Why two pipes and not parens like in JS or Scala?”, and so on.
I was appreciating that in Go we have only one way to express a function.
But, at the same time I often feel annoyed when I have to pass functions with long signature definitions by value. Gladly, Copilot autocomplete I started using recently helps a bit with that.
Maybe a way to solve this problem is to somehow create better autocomplete, rather than shorter syntax?
I love having full function declarations everywhere, but sometimes I hate writing them.
Comment From: gazerro
For some it would be enough to remove only the types, others would like an abbreviated form because in some circumstances the code is more readable, others believe that introducing a shortened form to be able to elide types may lead to less readable code.
Considering this, I propose to use ||
instead of ()
for the input parameters to indicate that the types are inferred without other changes to the current syntax
func |a, b| { return a < b }
This form is not less readable that the current form, a part from the elided types, and it does not have the readability problems of the func a, b { return a < b }
form when used in an expression list.
If the function has at least one return value and the body contains only one return
statement, then it is allowed to omit func
and return
and write
|a, b| { a < b }
This form is not too smart because it resembles the full form and is limited in its use.
Examples
http.HandleFunc("/foo", func |w, r| {
...
})
slices.SortFunc(slice, |a, b| { a < b })
Comment From: atdiar
@gazerro in general, it's better to introduce less sigils because it is confusing for non-initiated/beginners.
Someone has to know what they semantically represent.
It's akin to creating a math theory with its own new notation. Even non-initiated professionals get confused.
Especially when symbols have overloaded semantics.
So I don't think it would be beginner friendly here again, even though in the present context of this proposal, I understand your intent.
Comment From: gazerro
@atdiar
@gazerro in general, it's better to introduce less sigils because it is confusing for non-initiated/beginners.
In general I agree with you, but in this case the semantic is different from the ()
form so a different syntax is necessary. The func a, b { return a < b }
form does not introduce a new sigil but even with this syntax someone has to know what they semantically represent.
Comment From: ncruces
I like the arrow one, but I'd only use it for (and wouldn't mind having it limited to) single expression lambdas.
That's still very useful, and reduces the noise significantly, when you call a very short function (like (a, b) => { a < b }
), or when you just want to adjust arguments (or curry) before calling an existing function (like (a, b) => { a.x(b, true) }
)
The auto replacement CL doesn't really impress me much because many replacements are nothing like this.
Comment From: DmitriyMV
I don't think the argument about code being more "cryptic" stands. First of all - nobody is taking away the ability to use the old syntax where it's important. Second of all - with generics you can already write code where type declarations are not important. Third - context matters:
Consider the following:
func main() {
slc := []apackage.MyLongType{{"B"}, {"D"}, {"A"}, {"X"}, {"E"}, {"Z"}}
slices.SortFunc(slc, func(x, y apackage.MyLongType) bool { return x.Name < y.Name })
fmt.Println(slc)
}
with newly slice package. I don't think types here bear any interest, since we already know what we are sorting. But it does add clutter. Alternative:
func main() {
slc := []apackage.MyLongType{{"B"}, {"D"}, {"A"}, {"X"}, {"E"}, {"Z"}}
slices.SortFunc(slc, (x,y) => { return x.Name < y.Name })
fmt.Println(slc)
}
looks much cleaner to me, since my eyes can quickly skip to the relevant parts. With the advent of generics we are going to see more, and not less of functions being passed into functions - most of the new data-structure types would have to accept some sort of comparator to work. Please also remember that reading types is also a cognitive load - your eye has to read things before your mind will throw information away. Thats why I believe short function declaration add more good than harm in the long term, since we can use them when it's good and use the old syntax where parameter type info is important.
FWIW I like the () => {}
proposal. I don't think its going to be overused (like we feared with generics and before that with channels), since there are tons of features Go already have which can be abused (combination of generics and reflection extends this field tenfold), but so far, we as community, have found a ways to limit feature usage.
Comment From: DeedleFake
I highly, highly suggest that all of the people claiming that removal of explicit types is bad, or that it's expected to write code with an IDE, watch this talk by Rob Pike from 2010. I don't know what language you think you're arguing for, but these things that you're arguing have never been part of Go's design philosophy. Quite the opposite, in fact. Go wants to be readable, yes, but it also wants to be fun and easy to write, with literal direct comparisons to the ease-of-writing of dynamically-typed languages.
And as pointed out above, a lot of the arguments against readability are giving very out-of-context examples. Of course if all you see is stuff(n, (a, b) => { moreStuff(a + b) }
it's going to be confusing. You have no clue what stuff
, n
, or moreStuff
are. But the same is true of n := stuff(a, b, moreStuff)
. I don't think it's a particularly big deal to require the reader to know what a function that they're looking at a call of does. That's how programming works. That's kind of normal.
Comment From: ghost
Personally, I think it's better not to increase it. It will become very complicated. Only one solution is very good. What do you think?
Comment From: beoran
Seeing that this would be a very simple modification of the Go compiler and tools, perhaps we could get this as a GOEPERIMENT ? Then we can try it out and see what the real impact is?
Comment From: allochi
Since we are sharing ideas, and looking for a way to write and read less, this is another imagination for the lightweight anonymous function syntax.
From the documentation HandlerFunc
is declared to be
type HandlerFunc func(ResponseWriter, *Request)
if we enforce the parameter name at declaration like so (if we don't we enforce the full long version)
type HandlerFunc func(w ResponseWriter, r *Request)
we could providing anonymous function by only a block of code, using the parameter names (w
and r
) in the deceleration
http.HandleFunc("/decode", {
var product Product
json.NewDecoder(r.Body).Decode(&product)
...
json.NewEncoder(w).Encode(peter)
})
Breakage Adding identifiers to parameters would not break Go current implementation
Readability
we have less characters to read compared to suggestions above, besides, how would a list of unidentified parameters serve the readers? they still need to refer back to the documentation to make sense of them and what type they are, the example below clarify my point
Connect(srv *Service, func (address string, port uint, name string, user string, password string) *Connection {
...
}
// uncommunicative parameter names
Connect(s, (a, b, c, d, e) => {
dial(a, b)
...
})
// communicative parameter names
Connect(srv, {
dial(address, port)
...
})
Similarity It goes inline with struct initialization, where both identifiers and types are inferred, as an example
type Database struct {
Address string
Port uint
Name string
User string
Password string
}
var db = Database{"127.0.0.1", 7845, "products", "root", "something"}
I tried to scan as much as my time allowed to see if anyone suggested the same approach, I didn't see that, if I missed it, my sincere apology.
Comment From: biij5698
I firmly oppose it!
Comment From: biij5698
Please keep the golang simple!
Comment From: earthboundkid
This issue has been shared on social media, so more people are commenting than usual. Please refrain from adding “me too” comments and try not to add a point that has already been made by someone else, although that can be difficult because GitHub hides old comments.
Comment From: zhuah
Adding this syntax doesn't means that we must use it everywhere. Lambda expression is widely adopted in programming languages, almost all modern languages support this compact syntax, and i didn't see so much complains about it, instead, people are tend to support it.
For myself, i prefer the new compact syntax for both readability and writability, it looks more clean and current syntax looks too long and too noisy, especially in callback-heavy use cases.
When read someone's code, i want the code to be clean and the type information isn't so important for me at first glance,
just like val := someFunc(a, b, c)
, it doesn't matter what the type of val
is, and i will not complain the author
why not tell me the type of val
like var val someType = someFunc(a, b, c)
. If i need to dive into the code, i will pull down the code and browse it in an IDE, rather than inspect the code just by my eyes.
Comment From: rickiey
I firmly oppose it! Please keep the golang simple! You can use javascript ... if you like it
Comment From: mattn
Go should be explicit. and be easy to read. This change make people confusing to know the types. Go provides readability in exchange for the disadvantage of more description by explicitly describing types.
FYI, at least, this proposal should be separated to two or three.
- Omit
return
or brackets. - Omit types.
Comment From: b97tsk
@dolmen You can already do some hack right now: Go Playground.
Comment From: zhuah
A typical example from @eliasnaur's gioui library, https://github.com/gioui/gio-example/blob/main/kitchen/kitchen.go:
type (
D = layout.Dimensions
C = layout.Context
)
layout.Rigid(func(gtx C) D {
return in.Layout(gtx, material.IconButton(th, iconButton, icon, "Add Icon Button").Layout)
}),
layout.Rigid(func(gtx C) D {
return in.Layout(gtx, iconAndTextButton{theme: th, icon: icon, word: "Icon", button: iconTextButton}.Layout)
}),
layout.Rigid(func(gtx C) D {
return in.Layout(gtx, func(gtx C) D {
for button.Clicked() {
green = !green
}
return material.Button(th, button, "Click me!").Layout(gtx)
})
}),
layout.Rigid(func(gtx C) D {
return in.Layout(gtx, func(gtx C) D {
l := "Green"
if !green {
l = "Blue"
}
btn := material.Button(th, greenButton, l)
if green {
btn.Background = color.NRGBA{A: 0xff, R: 0x9e, G: 0x9d, B: 0x24}
}
return btn.Layout(gtx)
})
}),
layout.Rigid(func(gtx C) D {
return in.Layout(gtx, func(gtx C) D {
return material.Clickable(gtx, flatBtn, func(gtx C) D {
return layout.UniformInset(unit.Dp(12)).Layout(gtx, func(gtx C) D {
flatBtnText := material.Body1(th, "Flat")
if gtx.Queue == nil {
flatBtnText.Color.A = 150
}
return layout.Center.Layout(gtx, flatBtnText.Layout)
})
})
})
}),
as we can see, there is so many func(gtx C) D {
callback functions, and it even looks worse without type alias:
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return in.Layout(gtx, material.IconButton(th, iconButton, icon, "Add Icon Button").Layout)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return in.Layout(gtx, iconAndTextButton{theme: th, icon: icon, word: "Icon", button: iconTextButton}.Layout)
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return in.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
for button.Clicked() {
green = !green
}
return material.Button(th, button, "Click me!").Layout(gtx)
})
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return in.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
l := "Green"
if !green {
l = "Blue"
}
btn := material.Button(th, greenButton, l)
if green {
btn.Background = color.NRGBA{A: 0xff, R: 0x9e, G: 0x9d, B: 0x24}
}
return btn.Layout(gtx)
})
}),
layout.Rigid(func(gtx layout.Context) layout.Dimensions {
return in.Layout(gtx, func(gtx layout.Context) layout.Dimensions {
return material.Clickable(gtx, flatBtn, func(gtx layout.Context) layout.Dimensions {
return layout.UniformInset(unit.Dp(12)).Layout(gtx, func(gtx layout.Context) layout.Dimensions {
flatBtnText := material.Body1(th, "Flat")
if gtx.Queue == nil {
flatBtnText.Color.A = 150
}
return layout.Center.Layout(gtx, flatBtnText.Layout)
})
})
})
}),
since almost all users of this library knows that what gtx
means in such case, it may seems more clean with the new compact arrow syntax: gtx => {}
:
layout.Rigid(gtx => {
return in.Layout(gtx, material.IconButton(th, iconButton, icon, "Add Icon Button").Layout)
}),
layout.Rigid(gtx => {
return in.Layout(gtx, iconAndTextButton{theme: th, icon: icon, word: "Icon", button: iconTextButton}.Layout)
}),
layout.Rigid(gtx => {
return in.Layout(gtx, gtx => {
for button.Clicked() {
green = !green
}
return material.Button(th, button, "Click me!").Layout(gtx)
})
}),
layout.Rigid(gtx => {
return in.Layout(gtx, gtx => {
l := "Green"
if !green {
l = "Blue"
}
btn := material.Button(th, greenButton, l)
if green {
btn.Background = color.NRGBA{A: 0xff, R: 0x9e, G: 0x9d, B: 0x24}
}
return btn.Layout(gtx)
})
}),
layout.Rigid(gtx => {
return in.Layout(gtx, gtx => {
return material.Clickable(gtx, flatBtn, gtx => {
return layout.UniformInset(unit.Dp(12)).Layout(gtx, gtx => {
flatBtnText := material.Body1(th, "Flat")
if gtx.Queue == nil {
flatBtnText.Color.A = 150
}
return layout.Center.Layout(gtx, flatBtnText.Layout)
})
})
})
}),
In such cases, i REALLY want to exclude types from my code as they are totally noise.
Comment From: anjmao
I think the biggest benefit for anonymous function syntax is type inference.
func findPendingNodes(in []corev1.Node) []corev1.Node {
return lo.Filter(in, func(n corev1.Node, i int) bool {
return n.Status.Phase == corev1.NodePending
})
}
// ...
var pendingNodes []corev1.Node = findPendingNodes(nodes)
vs
func findPendingNodes(in []corev1.Node) []corev1.Node {
return lo.Filter(in, (n, i) => n.Status.Phase == corev1.NodePending)
}
// ...
pendingNodes := findPendingNodes(nodes)
Remember that in Go we already have explicit and implicit declarations. Does this make it less readable?
var pendingNodes []corev1.Node = findPendingNodes(nodes)
// vs
pendingNodes := findPendingNodes(nodes)
Comment From: zigo101
Type inference is good. The main problem of this proposal is cognition cost. C++ has millions of small features, each of them looks good solely.
Comment From: zhuah
@go101
The main problem of this proposal is cognition cost.
C#, Rust, Javascript, Java, Python, C++, etc.., they all support the arrow syntax like a=> {}
, are only Go programmers have cognition cost? Simplicity is not a reason to prevent a language to innovating itself.
And i don't understand why there is a downvote on my comment, i just show a case that type information is useless.
Comment From: zigo101
Yes, I indeed think the loads of cognition for the syntax in C#, Rust, (modern) Javascript, Java, Python and C++ are ALL high.
And i don't understand why there is a downvote on my https://github.com/golang/go/issues/21498#issuecomment-1134271931, i just show a case that type information is useless.
Because there is a difficulty for me to read your modified code.
Comment From: leaxoy
@go101
The main problem of this proposal is cognition cost.
C#, Rust, Javascript, Java, Python, C++, etc.., they all support the arrow syntax like
a=> {}
, are only Go programmers have cognition cost? Simplicity is not a reason to prevent a language to innovating itself.And i don't understand why there is a downvote on my comment, i just show a case that type information is useless.
Totally agree, I don't understand why many people opposed this proposal, and other languages has lots of experience on lambda, is golang want special for special for some political factors.
As I seen, this community does not allow the existence of other opinions.
Comment From: zigo101
I don't understand why many people opposed this proposal, and other languages has lots of experience on lambda, is golang want special for special for some political factors.
Because Go functions are already lambdas.
Comment From: allochi
@leaxoy
As I seen, this community does not allow the existence of other opinions.
This is unfair thing to say, this whole proposal is open for everyone to contribute and share their opinion since 2017
Comment From: zhuah
@go101 gtx
is the parameter, =>
indicates that this is an arrow function, {}
is the function body, that's all, nothing difficult, we all may feel strange to a new syntax at the first glance, but we need to understand it.
This proposal is not to just for introduce features from other language, but to solve a real problem: type is not needed in some cases.
People suggest func a, b {}
syntax firstly, but the arrow syntax (a, b) => {}
seems better as you can see in comment by @griesemer .
Comment From: leaxoy
I don't understand why many people opposed this proposal, and other languages has lots of experience on lambda, is golang want special for special for some political factors.
Because Go functions are already lambdas.
Yes, it is. Other language can omit type
and return
and brackets {}
, if possible, go should also can. Introduce new syntax will bring some complexity, but compare to benefit, it's worth trying like other languages.
And for programmers, continuous learning is a good habit.
Comment From: zhuah
I respect that people vote for code review to opposite this proposal, but not for just "keep Go simple", otherwise, all new language feature proposals should be rejected.
Comment From: leaxoy
share their opinion since 2017
Partially agree. But there are also many counter-examples here.
Proposals from internal like Google
, an example: https://github.com/golang/go/issues/51317, looks they are vips.
Meanwhile proposals like literal pointer
with code &1
has been proposed many times, but been closed or ignored, until Go Team Member
propose it.
Comment From: jayden-qiu
I think it's best to keep it simple
Comment From: allochi
@leaxoy
Proposal #51317 is really good news if it happens.
But let me put it this way regarding the current subject, I like Go explicitness, I have other problems with Go, but explicitness is not one of them, if we add lightweight functions to the language, I just wish it to look like Go code, just like what happened with type parameters, I don't want a borrowed style from JS or Rust, I don't like the =>
and I don't like the implicit return
, a simple func(a, b) {return a > b}
would work for me.
Would I use the lightweight syntax if it looks Go style, sure, but currently, I don't miss it.
Comment From: billinghamj
imo, a borrowed style from JS, assuming it is entirely consistent, would be quite positive - no need to reinvent the wheel if there's syntax which works great and matches already
Comment From: allochi
@billinghamj
from your words
no need to reinvent the wheel if there's syntax which works great and matches already
We do have that now, a syntax that works great and matches Go already
Comment From: leaxoy
@leaxoy
Proposal #51317 is really good news if it happens.
But let me put it this way regarding the current subject, I like Go explicitness, I have other problems with Go, but explicitness is not one of them, if we add lightweight functions to the language, I just wish it to look like Go code, just like what happened with type parameters, I don't want a borrowed style from JS or Rust, I don't like the
=>
and I don't like the implicitreturn
, a simplefunc(a, b) {return a > b}
would work for me.Would I use the lightweight syntax if it looks Go style, sure, but currently, I don't miss it.
For manual manage memory, lots of people don't think so, if want manual manage memory, a language without gc
is more fit than go, like C/C++/Zig/Rust
etc, optimize memory reclamation maybe the right way. Manual manage memory will introduce complexity far more than this(simple lambda).
And a variety of options may not be a good direction.
Comment From: allochi
@leaxoy
from your words
And a variety of options may not be a good direction
Comment From: leaxoy
C++ has millions of small features, each of them looks good solely.
Yeah, so and other proposals like this. The main problem is whether go them treat internal and external equality.
Comment From: earthboundkid
This issue has been shared on social media, so more people are commenting than usual. Please refrain from adding “me too” comments and try not to add a point that has already been made by someone else, although that can be difficult because GitHub hides old comments.
Many of the comments contain points already made. If you want Go to "keep it simple" or you're happy to reuse the fat arrow operator from JS, please just thumbs up a previous comment making that same point. Unfortunately, a lot of good comments that brought up issues not otherwise covered are now hidden from view by Github because of all the repetition of similar points.
Here is a link to a comment with @griesemer's CL's for a prototype change: https://github.com/golang/go/issues/21498#issuecomment-1132271548
Some points made that have been made on the pro-change side:
- Omits redundant type information, similar to
:=
- Very useful for tests where the same callback pattern is used repeatedly
- Some generic code works better with callbacks, and the callbacks read better without types, such as
slices.Sort(ss, (a, b) => { a.Thingie < b.Thingie })
- The fat arrow form in particular is familiar from other languages
Some points made that have been made on the anti-change side:
- There's already a good enough function syntax, we don't need two ways to do it
- Advances in autocomplete make this less necessary than it was before
- When reading code without an IDE/gopls, the type information may be hard to get
- In some cases, omitting the type information makes the code confusing, so it should not be done in all eligible cases
- Documentation for the language will need to be updated everywhere as well as tooling
- The form
func a, b { }
without parentheses looks unbalanced, and the difference is easy to overlook and hard to search for - The form
=>
may or may not turn up in a web search depending on whether the web search strips out symbols - It may be confusing to new users when the short form is legal or not, and whether there is any difference besides the length
- You can't write a callback which takes a private type in an external package today, but the error message for
otherpackage.Configure((cfg) => { /* ... */ })
would be very confusing. A user may think, I never wroteotherpackage.config
, why am I getting an error message about it?
If you want to make a point, please try to make one not listed above, thanks!
Comment From: nd
People who find the arrow syntax easier to read can get it in goland via plugin.
Comment From: DeedleFake
Correction:
People who find the arrow syntax easier to read and use Goland can get it via a plugin.
Thank you for the suggestion, but I do not use Goland, and I do not want to. I am extremely appreciative of the fact that I can write Go without an IDE, unlike Java, and I have no intention of using one for it anytime soon.
Comment From: daheige
Many languages provide a lightweight syntax for specifying anonymous functions, in which the function type is derived from the surrounding context.
Consider a slightly contrived example from the Go tour (https://tour.golang.org/moretypes/24):
```go func compute(fn func(float64, float64) float64) float64 { return fn(3, 4) }
var _ = compute(func(a, b float64) float64 { return a + b }) ```
Many languages permit eliding the parameter and return types of the anonymous function in this case, since they may be derived from the context. For example:
scala // Scala compute((x: Double, y: Double) => x + y) compute((x, y) => x + y) // Parameter types elided. compute(_ + _) // Or even shorter.
rust // Rust compute(|x: f64, y: f64| -> f64 { x + y }) compute(|x, y| { x + y }) // Parameter and return types elided.
I propose considering adding such a form to Go 2. I am not proposing any specific syntax. In terms of the language specification, this may be thought of as a form of untyped function literal that is assignable to any compatible variable of function type. Literals of this form would have no default type and could not be used on the right hand side of a
:=
in the same way thatx := nil
is an error.Uses 1: Cap'n Proto
Remote calls using Cap'n Proto take an function parameter which is passed a request message to populate. From https://github.com/capnproto/go-capnproto2/wiki/Getting-Started:
go s.Write(ctx, func(p hashes.Hash_write_Params) error { err := p.SetData([]byte("Hello, ")) return err })
Using the Rust syntax (just as an example):
go s.Write(ctx, |p| { err := p.SetData([]byte("Hello, ")) return err })
Uses 2: errgroup
The errgroup package (http://godoc.org/golang.org/x/sync/errgroup) manages a group of goroutines:
go g.Go(func() error { // perform work return nil })
Using the Scala syntax:
go g.Go(() => { // perform work return nil })
(Since the function signature is quite small in this case, this might arguably be a case where the lightweight syntax is less clear.)
I am strongly opposed to this proposal. This is taking the path of JS arrow function. I don't think it's necessary to do so. Just keep a style. Otherwise, some developers use and write indiscriminately everywhere, which will bring greater pit and more complexity to code maintainability.
Doing these grammar candy is just like showing off skills. Fancy things can not solve the problems of engineering, scale and efficiency in software engineering, but increase the mental burden of developers. When doing code review, we have to think about what this function is trying to express. It's not as good as the previous versions below go 1.18. Just keep a style.
Comment From: daheige
Please no, clear is better than clever. I find these shortcut syntaxes impossibly obtuse. …
Yes, clarity is better than KitKat, this is the classic point of unix programming art, just keep go as its original function.
Comment From: zhuah
@daheige
This is taking the path of JS arrow function. I don't think it's necessary to do so. Just keep a style. Otherwise, some developers use and write indiscriminately everywhere, which will bring greater pit and more complexity to code maintainability.
Doing these grammar candy is just like showing off skills. Fancy things can not solve the problems of engineering, scale and efficiency in software engineering, but increase the mental burden of developers. When doing code review, we have to think about what this function is trying to express. It's not as good as the previous versions below go 1.18. Just keep a style.
It's not a proposal for invent new wheel for declare functions, but to explore could we ignore types in function signature in some use cases. arrow function is just one way to do it.
Comment From: fewebahr
since almost all users of this library knows that what gtx means in such case, it may seems more clean with the new compact arrow syntax: gtx => {}:
@zhuah even in your example case, it is incorrect to assume that a majority of developers know the type of gtx
-- let alone "almost all". It is even more dangerous to make this assumption for all Go code in existence.
Real production code will be read, understood, and adapted many many more times than it is written. Terseness may slightly benefit the author of code-- but the cost to those working with the code on a go-forward basis is extremely high.
i REALLY want to exclude types from my code as they are totally noise
Type information is never "noise" in a strongly-typed language. Type information is critical context for developers and also for the compiler. At the very least a type declaration can be used to click "go to definition".
If developers wish to write code without type declarations, there are plenty of dynamically-typed languages out there like Python or Javascript.
More subjectively, I believe that Go's greatest strengths are readability and shallow learning curve. The syntax for Go is inflexible, to be sure- but it is also simple.
Comment From: fewebahr
I respect that people vote for code review to opposite this proposal, but not for just "keep Go simple", otherwise, all new language feature proposals should be rejected.
@zhuah this is a textbook straw man argument. Go has accepted many language proposals over the years, and many more will follow. It is possible to to meaningfully improve the language while keeping it simple.
Comment From: daheige
I think this is breaking go, not a good exploration, just keep it as it is, you don't experience the pain of arrow functions, it makes the project messy, and the readability and maintainability are worse.
------------------ Original ------------------ From: zhuah @.> Date: Tue,May 24,2022 10:56 AM To: golang/go @.> Cc: heige @.>, Mention @.> Subject: Re: [golang/go] proposal: Go 2: Lightweight anonymous function syntax(#21498)
@daheige
This is taking the path of JS arrow function. I don't think it's necessary to do so. Just keep a style. Otherwise, some developers use and write indiscriminately everywhere, which will bring greater pit and more complexity to code maintainability.
Doing these grammar candy is just like showing off skills. Fancy things can not solve the problems of engineering, scale and efficiency in software engineering, but increase the mental burden of developers. When doing code review, we have to think about what this function is trying to express. It's not as good as the previous versions below go 1.18. Just keep a style.
It's not a proposal for invent new wheel for declare functions, but to explore could we ignore types in function signature in some use cases. arrow function is just one way to do it.
— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you were mentioned.Message ID: @.***>
Comment From: daheige
Yes, just keep it simple, I think it's a show of kinky tricks for those fancy usages in other languages, and definitely say no to that! don't import this, don't import this,again!
------------------ Original ------------------ From: RobertGrantEllis @.> Date: Tue,May 24,2022 11:17 AM To: golang/go @.> Cc: heige @.>, Mention @.> Subject: Re: [golang/go] proposal: Go 2: Lightweight anonymous function syntax(#21498)
since almost all users of this library knows that what gtx means in such case, it may seems more clean with the new compact arrow syntax: gtx => {}:
@zhuah even in your example case, it is incorrect to assume that a majority of developers know the type of gtx -- let alone "almost all". It is even more dangerous to make this assumption for all Go code in existence.
Real production code will be read, understood, and adapted many many more times than it is written. Terseness may slightly benefit the author of code-- but the cost to those working with the code on a go-forward basis is extremely high.
i REALLY want to exclude types from my code as they are totally noise
Type information is never "noise" in a strongly-typed language. Type information is critical context for developers and also for the compiler. At the very least a type declaration can be used to click "go to definition".
If developers wish to write code without type declarations, there are plenty of dynamically-typed languages out there like Python or Javascript.
— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you were mentioned.Message ID: @.***>
Comment From: fewebahr
Adding this syntax doesn't means that we must use it everywhere.
@zhuah That's true- but adding the syntax means it will be used in many places.
Lambda expression is widely adopted in programming languages
Other programming languages are not Go, and many of them are harder to read and work with.
i prefer the new compact syntax for both readability and writability
The "compact" syntax is substantially less readable because (a) type information is critical context, and (b) multiple ways to write the same code also materially reduces readability.
especially in callback-heavy use cases.
Callback-heavy use cases are inherently not go-centric, and in fact the language discourages callbacks. You will see very few uses of callbacks in the standard library. There are alternative coding patterns that solve asynchronous problems.
it doesn't matter what the type of val is
Yes it does. If the code assigns val
, then it also uses val
(the compiler even enforces this). If val
is used, then the type matters.
Comment From: fewebahr
but you'll get used to the lightweight way of declaring anonymous functions.
@sbinet I agree with you that the plasticity of the human brain is amazing! And I agree that we'll get used to it if they are added. But that is not a reason to allow them in the language.
But there are reasons to keep the status quo: as you say, readability is important. Adopting these patterns will hurt readability- and real production code is read/adapted/understood (many times) way more than it is written (once).
Comment From: daheige
After seeing the changes in the go language in recent years, I did accept some proposals from other languages, but I am relatively opposed to this proposal. I am a senior js developer and also a senior go developer. The bells and whistles of usage proposals are really sad. Since js introduced this kind of arrow function, when some projects are developed and maintained by developers, the readability and maintainability of this writing method in the project is really bad. So I think it's better not to step into the various problems caused by the js language arrow function, the past is there. It is recommended to keep the original go func definition style, just one style is enough, don't play with these fancy syntax sugar tricks, I really hate these proposals.
------------------ Original ------------------ From: RobertGrantEllis @.> Date: Tue,May 24,2022 11:24 AM To: golang/go @.> Cc: heige @.>, Mention @.> Subject: Re: [golang/go] proposal: Go 2: Lightweight anonymous function syntax(#21498)
I respect that people vote for code review to opposite this proposal, but not for just "keep Go simple", otherwise, all new language feature proposals should be rejected.
This is a textbook straw man argument. Go has accepted many language proposals over the years, and many more will follow. It is possible to to meaningfully improve the language while keeping it simple.
— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you were mentioned.Message ID: @.***>
Comment From: fewebahr
shorthand for those would be beneficial and probably even more readable in the long run.
@beoran The experiment that @griesemer ran made clear that lots of Go code could use the shorthand form- but I don't see how it would necessarily be "beneficial" or "more readable". In fact, @griesemer's conclusions call out explicitly that certain parts of the syntax appear less readable.
Comment From: zhuah
@RobertGrantEllis Sorry for "it doesn't matter what the type of val is", i mean do people know the type of val
in val := someFunc(xxx)
at the first glance?
func (gtx C) D
is a very common case in gioui, it may appears many many times in gioui app, even the author of this library @eliasnaur needs to use type alias to simplify the function signature:
type (
D = layout.Dimensions
C = layout.Context
)
and sorry for the "almost all" representation, it should be "users are familiar with gioui".
It's a bit like peoples who don't uses generics saying generics is a mistake, regardless of so many complains of interface{}
.
Type inference is used so much in Go code, and we all uses "val := someFunc(args)" rather than "var val someType = someFunc(args)", no one says that we must use the second syntax for readability.
Comment From: zhuah
I support this proposal for solving my problem, not for "JS have arrow function, so Go should also have it".
Comment From: zhuah
Other programming languages are not Go, and many of them are harder to read and work with.
Sorry, i don't think so. And Rust, C#, Java, they are also strong-typed languages, we Go programmers are not special compared to programmers in other languages.
Comment From: daheige
Just keep it as it is. Clarity is better than fancy, and better than ingenuity.
------------------ Original ------------------ From: RobertGrantEllis @.> Date: Tue,May 24,2022 11:54 AM To: golang/go @.> Cc: heige @.>, Mention @.> Subject: Re: [golang/go] proposal: Go 2: Lightweight anonymous function syntax (#21498)
shorthand for those would be beneficial and probably even more readable in the long run.
@beoran The experiment that @griesemer ran made clear that lots of Go code could use the shorthand form- but I don't see how it would necessarily be "beneficial" or "more readable". In fact, @griesemer's conclusions call out explicitly that certain parts of the syntax appear less readable.
— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you were mentioned.Message ID: @.***>
Comment From: daheige
Too much flexibility leads to a variety of code styles. When doing code review and maintenance, it brings more burden to developers. We have to understand what this code means, spend more time and energy, and can not bring substantive benefits. Therefore, I am very opposed to this proposal.
------------------ Original ------------------ From: zhuah @.> Date: Tue,May 24,2022 0:16 PM To: golang/go @.> Cc: heige @.>, Mention @.> Subject: Re: [golang/go] proposal: Go 2: Lightweight anonymous function syntax (#21498)
Other programming languages are not Go, and many of them are harder to read and work with.
Sorry, i don't think so. And Rust, C#, Java, they are also strong-typed languages.
— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you were mentioned.Message ID: @.***>
Comment From: DeedleFake
@daheige
Since js introduced this kind of arrow function, when some projects are developed and maintained by developers, the readability and maintainability of this writing method in the project is really bad.
This proposal has almost nothing in common with JS arrow functions besides the superficial syntactic similarities. JS has no types, and, as such, there is literally zero difference having to do with type inference between regular anonymous functions and arrow functions in JS. JS's arrow functions exist purely to work around the awkwardness that is this
in regular functions in JS, something which does not exist in Go and is not being proposed by anyone.
This proposal is purely about adding type inference to anonymous functions in Go. That's it. Some people suggested something like arrow functions for the syntax, but other than that there is no connection whatsoever between this proposal and JS's arrow functions.
Comment From: daheige
I see what you mean, but I don't think such a syntactic sugar mechanism is needed anyway, thanks.
------------------ Original ------------------ From: DeedleFake @.> Date: Tue,May 24,2022 1:22 PM To: golang/go @.> Cc: heige @.>, Mention @.> Subject: Re: [golang/go] proposal: Go 2: Lightweight anonymous function syntax(#21498)
@daheige
Since js introduced this kind of arrow function, when some projects are developed and maintained by developers, the readability and maintainability of this writing method in the project is really bad.
This proposal has almost nothing in common with JS arrow functions besides the superficial syntactic similarities. JS has no types, and, as such, there is literally zero difference having to do with type inference between regular anonymous functions and arrow functions in JS. JS's arrow functions exist purely to work around the awkwardness that is this in regular functions in JS, something which does not exist in Go and is not being proposed by anyone.
This proposal is purely about adding type inference to anonymous functions in Go. That's it. Some people suggested something like arrow functions for the syntax, but other than that there is no connection whatsoever between this proposal and JS's arrow functions.
— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you were mentioned.Message ID: @.***>
Comment From: fmyxyz
It will lead to a reduction in readability, used to show off skills
Comment From: daheige
Thank you so much for what you said! Of course, such a mechanism also exists in the rust language. If go also introduces this, on the one hand, it is destroying go and adding a new style. On the other hand, for the go compiler, it may do more things. It is guaranteed that this mechanism can be compiled normally. Secondly, for go developers, in fact, everyone is used to the original style, and there is no need to introduce another style or learn this mechanism. The last point is that when doing code review, too much flexibility will increase the developer's time and energy, and will not bring substantial benefits, at least not in my opinion.
------------------ Original ------------------ From: DeedleFake @.> Date: Tue,May 24,2022 1:22 PM To: golang/go @.> Cc: heige @.>, Mention @.> Subject: Re: [golang/go] proposal: Go 2: Lightweight anonymous function syntax(#21498)
@daheige
Since js introduced this kind of arrow function, when some projects are developed and maintained by developers, the readability and maintainability of this writing method in the project is really bad.
This proposal has almost nothing in common with JS arrow functions besides the superficial syntactic similarities. JS has no types, and, as such, there is literally zero difference having to do with type inference between regular anonymous functions and arrow functions in JS. JS's arrow functions exist purely to work around the awkwardness that is this in regular functions in JS, something which does not exist in Go and is not being proposed by anyone.
This proposal is purely about adding type inference to anonymous functions in Go. That's it. Some people suggested something like arrow functions for the syntax, but other than that there is no connection whatsoever between this proposal and JS's arrow functions.
— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you were mentioned.Message ID: @.***>
Comment From: zikaeroh
This proposal has almost nothing in common with JS arrow functions besides the superficial syntactic similarities. JS has no types, and, as such, there is literally zero difference having to do with type inference between regular anonymous functions and arrow functions in JS. JS's arrow functions exist purely to work around the awkwardness that is
this
in regular functions in JS, something which does not exist in Go and is not being proposed by anyone.
This isn't totally the case. Per on the State of JS (an equivalent to the yearly Go survey), most JS devs are actually using TypeScript, which is typed. Arrow functions there are used very, very often in places such as parameters to other functions (like forEach
, map
, others), where inference is desirable for the exact same reasons laid out in this proposal (not having to write the types again for one reason or another). Further, people using VS Code or VS are just using TypeScript without the type-related diagnostics, so all of the same inference rules apply to regular JS too (the code in use has type declarations, which is very often).
Comment From: ChrisHines
So far my opinion has been essentially 50-50 on this proposal. I am a fan of function literals and use them regularly, but after further consideration of all the arguments and code examples and thinking about my own code that could use this feature, I've come down on the no side, 👎 . The strongest arguments against this proposal for me are:
- Adding a second function literal syntax is too big a cost relative to the benefits I am seeing
- The lack of information about return parameters in the signature significantly damages readability in my opinion. I think knowing whether a function returns anything prior to reading the body is important.
- Documentation, books, and training material for the language will either become stale or need to be updated everywhere. When updated they have to explain more styles and when they are allowed or should/should not be used.
- It pushes more work on tool maintainers to make updates
Comment From: daheige
LGTM!
------------------ Original ------------------ From: Chris Hines @.> Date: Tue,May 24,2022 1:40 PM To: golang/go @.> Cc: heige @.>, Mention @.> Subject: Re: [golang/go] proposal: Go 2: Lightweight anonymous function syntax(#21498)
So far my opinion has been essentially 50-50 on this proposal. I am a fan of function literals and use them regularly, but after further consideration of all the arguments and code examples and thinking about my own code that could use this feature, I've come down on the no side, 👎 . The strongest arguments against this proposal for me are:
Adding a second function literal syntax is too big a cost relative to the benefits I am seeing
The lack of information about return parameters in the signature significantly damages readability in my opinion. I think knowing whether a function returns anything prior to reading the body is important.
Documentation, books, and training material for the language will either become stale or need to be updated everywhere. When updated they have to explain more styles and when they are allowed or should/should not be used.
It pushes more work on tool maintainers to make updates
— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you were mentioned.Message ID: @.***>
Comment From: zhuah
func (a, b _) _ {}
, this is the Go-like type inference for function literals, no second style, perfect.
Comment From: hitzhangjie
IMHO, I prefer current syntax taken readability into consideration.
For the new syntax, if want to know the types (can't see it from the signature), then have to move focus to that parameter to show type info. It's inefficient and annoying.
I prefer simplicity of Go features over less lines of Go code.
Comment From: mrg0lden
This thread is becoming me-too-ish, I think whatever both sides think can be expressed using votes instead. 👀
I also like @zhuah suggestion because it shows that something is being returned and both parameters have the same type, which helps with readability. I think if the usage of this syntax was strictly limited to n expressions per function and only to functions passed directly as arguments it may help the readability concerns even more.
Comment From: Graham-Beer
I'm struggling to understand why we would want this in Go. Seems to go against everything Go stands for. Having clean, readable code is far more valuable than having abbreviated syntax, in my eyes. To enforce this with go fmt
doesn't feel like a positive move, removing the choice from the user/customer/company. Forcing the Go user use shortened syntax will, I think, lose people who love Go for it's readability.
Imagine been woken up at 3am to debug some Go code, only to see it all with unreadable syntax. All the coffee in the world won't help with that!!
Comment From: daheige
I totally agree with your point of view, it really should be kept as it is, and these proposals should not be introduced. There is nothing useful for readability and maintainability.
------------------ Original ------------------ From: Graham Beer @.> Date: Tue,May 24,2022 4:31 PM To: golang/go @.> Cc: heige @.>, Mention @.> Subject: Re: [golang/go] proposal: Go 2: Lightweight anonymous function syntax (#21498)
I'm struggling to understand why we would want this in Go. Seems to go against everything Go stands for. Having clean, readable code is far more valuable than having abbreviated syntax, in my eyes. To enforce this with go fmt doesn't feel like a positive move, removing the choice from the user/customer/company. Forcing the Go user use shortened syntax will, I think, lose people who love Go for it's readability. Imagine been woken up at 3am to debug some Go code, only to see it all with unreadable syntax. All the coffee in the world won't help with that!!
— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you were mentioned.Message ID: @.***>
Comment From: DmitriyMV
Once again:
Please refrain from adding “me too” comments and try not to add a point that has already been made by someone else, although that can be difficult because GitHub hides old comments.
There is no reason to repeat old points.
Comment From: ncruces
go fmt
doesn't need to have an opinion on this, just like it doesn't on :=
vs var
which are also often interchangeable.
Comment From: zhuah
@griesemer maybe the proposal title should be updated to mention "type inference" for function literals.
Comment From: beoran
@RobertGrantEllis This proposal would be beneficial in the case where the types of the function literals are just boilerplate, such as in tests, or for GUI libraries, etc.
If the types are important, then I wouldn't use the type inferred function literals.
Comment From: fewebahr
i mean do people know the type of val in val := someFunc(xxx) at the first glance?
@zhuah Assignment and invocation are completely different than function signatures, whose sole purpose are to express parameters and return types. Also, implicit typing as you've described above should, in my opinion, be discouraged except in certain cases. For example, in my opinion, this syntax is fine if someFunc
is defined inside the invoking function or perhaps immediately above or below it. When I'm reading unfamiliar code, I shouldn't have to jump around to find types- I should only have to jump around for details of the function itself.
func (gtx C) D is a very common case in gioui
Maybe- that's one library, and I'm personally unfamiliar with it. If anyone needs to understand this library in future, it will be harder to understand if the shorthand syntax is used. More importantly, the shorthand syntax could be used broadly across code everywhere-- whether appropriate or not.
Saying that this one library could use the syntax is a long shot from making the case that it would be superior as a result. And, even if we successfully make the case that this library could benefit, it's a giant leap to then imply that all other code would be superior as a result.
This shorthand will make the code less readable because less vital information is expressed in the function signature-- and the sole purpose of a function signature is to express that information. Terseness helps the author just a bit and hurts everyone else a lot more.
even the author of this library @eliasnaur needs to use type alias to simplify the function signature
"Needs to" is an overstatement. The author @eliasnaur chose to use type aliases. Type aliases are already a feature of the language and provide many benefits. Usage of type aliases does not necessarily indicate any preference for terseness, let alone terseness-at-all-costs. Perhaps it's appropriate to let the author themselves chime in to express their intention, rather than assuming it?
and sorry for the "almost all" representation, it should be "users are familiar with gioui".
Code exists so that humans can understand and work with it. Lots of developers work with (and will work with) gioui, and only a subset will be familiar with it. The same goes with any code you write. It is well established that code is read and adapted many times more often than it is written- in a professional all developers should write code that helps those in future-- even if it means typing a couple of extra characters.
It's a bit like peoples who don't uses generics saying generics is a mistake, regardless of so many complains of interface{}. Respectfully, I disagree. Generics are a tool in Go. They provide new functionality that a developer can use as-needed to dramatically improve re-usability in some cases. They do come at the cost of readability, but the utility and value is well-established, even among the haters. This proposal does not make additional functionality available to the language- it merely creates another way of doing the same thing, which provably makes all code less readable and harder to learn.
Type inference is used so much in Go code, and we all uses "val := someFunc(args)" rather than "var val someType = someFunc(args)", no one says that we must use the second syntax for readability.
Critically, type inference is never used in function signatures, because function signatures exist to express parameters and return values, including their types. Also, see my comments above: IMHO var val someType = someFunc(args)
is dramatically more readable and should be used in cases where someFunc
has different residency (i.e. when the developer would need to context-switch to understand the function signature).
Comment From: fewebahr
I support this proposal for solving my problem, not for "JS have arrow function, so Go should also have it".
@zhuah What is the problem you're trying to solve, and how does this solve it? If the problem is you don't like writing so many characters, then I would argue that's less a problem and more a feature. I would also point out again that terseness is a choice other languages have made, and they are available if you value terseness.
Comment From: fewebahr
Sorry, i don't think so. And Rust, C#, Java, they are also strong-typed languages, we Go programmers are not special compared to programmers in other languages.
You're right that Go programmers are not special-- but Go is undoubtedly special. IMHO Go's greatest strength is how simple, rigid and expressive the language is-- that's why it's easy to learn (and importantly train others on).
If you think Go should have more syntactic sugar because other languages do, then Go will ultimately become just the same as those other languages. Developers that want features offered by other languages can choose to use those instead.
Comment From: fewebahr
Thank you so much for what you said! Of course, such a mechanism also exists in the rust language. If go also introduces this, on the one hand, it is destroying go and adding a new style. On the other hand, for the go compiler, it may do more things. It is guaranteed that this mechanism can be compiled normally. Secondly, for go developers, in fact, everyone is used to the original style, and there is no need to introduce another style or learn this mechanism. The last point is that when doing code review, too much flexibility will increase the developer's time and energy, and will not bring substantial benefits, at least not in my opinion.
@daheige this -----^
Comment From: fewebahr
@ChrisHines Completely agree. I'll point out again that code in a professional environment is written once, then read and adapted countless times. Languages should be optimized for readability, because in the lifetime of a line of code, that's what happens.
Comment From: fewebahr
func (a, b _) _ {}, this is the Go-like type inference for function literals, no second style, perfect.
@zhuah this is a second style. It leaves out type information and forces those reading and working with the code to work harder to understand what it's doing. This is provably less functional and less readable.
Comment From: fewebahr
Forcing the Go user use shortened syntax will, I think, lose people who love Go for it's readability.
@Graham-Beer This. Exactly this. Go is great because it's goal -from day 1- was readability. Go doesn't need to be like every other language, and it shouldn't be. Go code is supportable, and this feature offers literally no functionality and makes the code less supportable. I honestly don't understand why anybody is lobbying for this.
Comment From: fewebahr
would be beneficial in the case where the types of the function literals are just boilerplate
@beoran I don't think it would be beneficial- it would just be more terse-- and less readable. Also, I urge you to more strictly define what "boilerplate" means in your statement. The word "boilerplate" implies useless, meaningless content- and in a strongly-typed languages, types are most certainly meaningful.
Go is readable and supportable because it is opinionated, expressive, and simple. And in general there is only one way to do things.
Comment From: DeedleFake
@RobertGrantEllis
terseness is a choice other languages have made
Terseness is also, rather explicitly, a choice of Go. Go's design explicitly wanted to make writing it as easy as a scripting language, and also explicitly believed that too much information and repetition hurts readability. Just watch any talk from Rob Pike where he talks about the design and goals of the language. Here's one.
There are a bunch of people in here making arguments so heavily in favor of verbosity that it makes it feel like they want to be writing early 2000s Java, not Go. You even stated above that you think that a full var a T = f()
is a good convention and preferable any time f
is declared far away, which is just nuts to me. That is not Go and never has been. If you think that's a good idea, great, but please stop claiming that you are arguing in favor of Go's existing design philosophy. You are not.
Edit: For those who don't want to and/or can't watch the talk, here are a few slides of particular interest:
Go is not about constant repetition and verbosity, never has been, and shouldn't be. One of its inspirations was literally too much verbosity in other statically-typed languages and the difficulty in both reading and writing them that results. As Rob said:
Comment From: timothy-king
I may be in a distinct minority on this, but I personally like the func(a, b, c _) _
suggestion proposed in
https://github.com/golang/go/issues/21498#issuecomment-324726831. It saves the fewest characters, but it changes the syntax the least (*ast.FunctLit would still works IIUC) and grants the author flexibility to choose when to elide the types.
I think it would be nice to see the experiment https://github.com/golang/go/issues/21498#issuecomment-1132271548 repeated for this syntax to compare.
Comment From: jimmyfrasche
To clarify: while gofmt was modified to rewrite a large Go codebase to see how it would look, this proposal does not include gofmt automatically rewriting function literals.
Comment From: DeedleFake
I may be in a distinct minority on this, but I personally like the
func(a, b, c _) _
suggestion proposed in https://github.com/golang/go/issues/21498#issuecomment-324726831.
The syntax is ugly, but I agree with the reasoning and I think it would definitely be best for the syntax to be close to the existing ones. Plus, if something like https://github.com/golang/go/issues/34515#issuecomment-545515326 is accepted, it would be consistent with it. _
as a type would essentially become a marker for inference that works outside-in instead of inside-out.
Comment From: griesemer
Using
func(a _) {}
is slightly problematic because that's valid (ordinary) function literal syntax with a type checking error (use of _
as value or type). It can be made to work but it's something to be aware of.
Comment From: jxsl13
please just no. The current anonymous functions are clear and are used everywhere in the same way. no special syntactical sugar, no nothing.
Comment From: Allyedge
I honestly prefer func
over symbols.
Some languages like Elixir are more readable with more symbols, because that's a feature of those languages.
Go focuses on simplicity, and in my personal opinion, when looking at some code, seeing and understanding func
is a lot easier, especially for beginners.
Go code is also very easy to read in almost every project because it doesn't have too much syntax sugar.
JavaScript on the other hand, is sometimes hard to read because everyone uses the syntax they like to use, and not everyone can read it as easily as the person who writes the code.
Comment From: daheige
Yes, keep one style, do one thing, and do one thing well, this is also a point repeatedly mentioned in the art of unix programming.
------------------ Original ------------------ From: RobertGrantEllis @.> Date: Tue,May 24,2022 10:14 PM To: golang/go @.> Cc: heige @.>, Mention @.> Subject: Re: [golang/go] proposal: Go 2: Lightweight anonymous function syntax(#21498)
would be beneficial in the case where the types of the function literals are just boilerplate
@beoran I don't think it would be beneficial- it would just be more terse-- and less readable. Also, I urge you to more strictly define what "boilerplate" means in your statement. The word "boilerplate" implies useless, meaningless content- and in a strongly-typed languages, types are most certainly meaningful.
Go is readable and supportable because it is opinionated, expressive, and simple. And in general there is only one way to do things.
— Reply to this email directly, view it on GitHub, or unsubscribe. You are receiving this because you were mentioned.Message ID: @.***>
Comment From: zhuah
@daheige don't be superstitious about "one style" or "one way", it's just guidance, not restriction.
- Linux have
select
,poll
,epoll
, all designed for handling I/O events - Go stdlib provide two way to create a file, os.Open and os.Create, the later is just a sugar for previous:
func Create(name string) (*File, error) {
return OpenFile(name, O_RDWR|O_CREATE|O_TRUNC, 0666)
}
- Go have
i++
andi += 1
while python have onlyi += 1
if err != nil
is currently the only way do error handling in Go, but we are still looking for a better way
Edit: and as i said before, this proposal is neither invent a new style nor a syntax sugar, but to allow type inference for function literals.
Comment From: jefferyjob
Strongly opposed. The most attractive thing about golang is its simplicity. If there are so many syntax sugars, what is the difference between golang and redundant Java. I hope the author of golang can continue to keep his original intention.
Comment From: griesemer
With a few minor exceptions, most recent comments have added no new useful information to this thread.
Repeatedly stating that one is opposed or in favor of this proposal based on a personal belief or mantra, without concrete examples in Go that make the point clear, is simply not going to sway anybody who is of different opinion. It certainly won't sway the Go Team, one way or the other. So please refrain from doing so.
If you do want to add something, what we are interested in at this point is examples of real-world Go code that would benefit/suffer from the proposal. It's pretty clear that there's code where removing boilerplate (incl. types!) will improve readability. It's also pretty clear that there's plenty of code where leaving away types will reduce readability. Still, the more such concrete examples we have (we only investigated the std library), the better. We may have overlooked something.
I have provided the tools to make the respective experiments if you are so inclined (yes, it requires a bit of work).
If you just want to voice your personal opinion, please use emoji voting on the initial comment.
Thanks.
Comment From: nomad-software
I know you've asked people not to 'clutter' this thread but I think the following important points are worth re-iterating.
Repeatedly stating that one is opposed or in favor of this proposal based on a personal belief or mantra, without concrete examples in Go that make the point clear, is simply not going to sway anybody who is of different opinion.
From what the Go team has said, this is how Go was originally envisioned and designed. Three original authors had to agree (based on personal experience) on all design aspects or it didn't make it in.
If you do want to add something, what we are interested in at this point is examples of real-world Go code that would benefit/suffer from the proposal.
The problem is that it's not a code issue. It's a more philosophical issue about the design (and future) of the language as a whole. Adding features because you can, is not why we all love Go.
Comment From: changkun
I applied the tool on an (experimental) code base that I wrote. See https://github.com/polyred/polyred/commit/70588ff1e125ba718634f8b17d47e63cf68b7dcf for more details and statistics.
Here are some of my observations and cases where the syntax really hurts readability, I elaborate on a few examples that are difficult to identify the returning value in this case.
It might be difficult to realize that the Col
is returning in this case:
-r.DrawFragments(buf1, func(f *primitive.Fragment) color.RGBA {
- return buf2.Get(f.X, f.Y).Col
-})
+r.DrawFragments(buf1, (f) => {
+ buf2.Get(f.X, f.Y).Col
+})
Similarly:
-r.DrawFragments(buf, func(frag *primitive.Fragment) color.RGBA {
- return color.RGBA{uint8(frag.X), uint8(frag.X), uint8(frag.Y), uint8(frag.Y)}
-})
+r.DrawFragments(buf, (frag) => {
+ color.RGBA{uint8(frag.X), uint8(frag.X), uint8(frag.Y), uint8(frag.Y)}
+})
Also, we have no idea that shade
is returning a value to be used:
-r.DrawFragments(buf, func(frag *primitive.Fragment) color.RGBA {
- return r.shade(frag, uniforms)
-})
+r.DrawFragments(buf, (frag) => {
+ r.shade(frag, uniforms)
+})
We may expect to write code that is expecting to return a zero value other than using the returned value of the last statement, and it would cause unexpected code behavior that is difficult to realize.
Furthermore, what is this isolated true
?
-scene.IterObjects(s, func(o object.Object[float32], modelMatrix math.Mat4[float32]) bool {
- return true
-})
+scene.IterObjects(s, (o, modelMatrix) => {
+ true
+})
The majority of rewrites in this tested code base are the discussed testing.T
, testing.B
, and empty func()
cases.
For the testing
package, it is surely common knowledge that is established in the standard library where almost every developer has the knowledge to understand the context of the test function. However, I'd argue that in a domain-specific codebase, only developers who know the APIs well are capable of understanding what the code is exactly doing. When doing code reviewing, it may not be aware of the implicit behavior (at least yet not quickly aware) that the returning value of the last statement is used as a return value, especially the shade
example in above.
Comment From: fzipp
It might be difficult to realize that the
Col
is returning in this case: [...] Also, we have no idea thatshade
is returning a value to be used: [...] We may expect to write code that is expecting to return a zero value other than using the returned value of the last statement, and it would cause unexpected code behavior that is difficult to realize.
Usually shorthand functions have one of two forms:
- Without curly braces followed by a single expression (not a statement). These always have a return value.
- With curly braces (a block) and statements. These only have a return value if there is a return statement.
The prototype's combination of curly braces and implicit returns is a bit unusual.
Comment From: beoran
@nomad-software has an interesting point. In no other programming language community I know would there be almost as many people against adding this feature as there are in favor of it. While code examples are great, it seems like many people in the Go community like its minimalism. Even though I like this feature, seeing how controversial it is, perhaps we should let it rest for a year or so and see how the mood is then.
Comment From: billinghamj
Setting aside whether people like the JS-style map/filter/reduce methods, they are a great example where this is necessary
This can be very difficult to write at the moment, and is a situation where language server hinting does not help:
return slicefn.Reduce(maps.Keys(foo), func(acc map[string]string, bar string) map[string]string {
acc[bar] = fmt.Sprintf("something-%s", bar)
return acc
}, map[string]string{})
The typing is only known once you write the last parameter, so you don't get any automatic completion (and if you do write it wrong, which I've found to be common, the error message is... lengthy)
This is quite a simple example, but we have several individual calls to this function, where the typing alone is 100+ chars
With anonymous function syntax, it would be much simpler:
return slicefn.Reduce(maps.Keys(foo), (acc, bar) => {
acc[bar] = fmt.Sprintf("something-%s", bar)
return acc
}, map[string]string{})
For one-liners when using generics with map/filter/reduce etc, this can often reduce the line length by more than half - making it dramatically easier to read
In the longest example in our codebase, the line length would be reduced from 192 to 62 - less than a third(!)
slicefn.ReduceE(intercomconfig.Platforms, func(acc map[intercomconfig.Platform]*app.AppPlatform, platform intercomconfig.Platform) (_ map[intercomconfig.Platform]*app.AppPlatform, err error) {
slicefn.ReduceE(intercomconfig.Platforms, (acc, platform) => {
Of course for anyone who isn't keen on this syntax, you do always retain the option of not using it. For those who do need it, the benefits are significant. If you're concerned about the potential readability impact in your codebase, why not just prevent it with your linting rules?
I'd note that the big benefit is, by far, the removal of the explicit typing. With that covered, any of the syntax options would be great. I like the idea of not reinventing the wheel and matching JS, but including the func
keyword could be beneficial to make it crystal clear you're working with a function
@beoran to your point on giving it some time - this issue has been open since 2017!
Comment From: DmitriyMV
@changkun did you try to to go with () => { ... return .. }
syntax, and if so, what was your experience?
Comment From: DeedleFake
@nomad-software
From what the Go team has said, this is how Go was originally envisioned and designed. Three original authors had to agree (based on personal experience) on all design aspects or it didn't make it in.
That is not remotely viable to expand to a group the size of those who use Go. If everyone has to agree before something is added, nothing will ever get added because you can always find at least one person on essentially anything who doesn't agree with it.
Adding features because you can, is not why we all love Go.
I completely agree. I do not, however, agree with the implication that that's the entirety of reasoning in favor of this.
@fzipp
The prototype's combination of curly braces and implicit returns is a bit unusual.
But not unheard of. Both Rust and Ruby also do it, though Rust has that bizarre little trick with semicolons that it does to try to distinguish it a little. That being said, I agree with @changkun's assessment that the lack of returns is strange in several of the examples. In some of them I think it's a matter of not being used to it, but in some I think it's not really worth the trouble of removing the return
keyword.
I wonder if there's a better syntax besides dropping the {}
that would still make it clear that it's returning. In the meantime, those examples have convinced me that dropping return
without any other indications isn't a good idea.
Edit: Hmmm... All of the confusion, for me at least, is from the function calls. Maybe it would be fine to implicitly return only expressions that are currently illegal as a standalone statement? That would allow for simple stuff like (a, b) => { a + b }
, but disallow (a, b) => { add(a, b) }
. I think those are less confusing because it's obvious that they have no side-effects, and therefore the only possible reason to have them there is to return them.
Comment From: jimmyfrasche
Everything is an expression languages don't need to make a distinction because everything is an expression.
Not a huge fan of js but this is a case where js does it right: If the RHS is an expression, the result of the expression is returned; if it's a block it's an ordinary block. So (x, y) => x + y
is shorthand for (x, y) => { return x + y }
. The different syntactic forms provide the necessary context.
Comment From: DeedleFake
Everything is an expression languages don't need to make a distinction because everything is an expression.
Go already makes the exact distinction that I'm talking about.
func main() {
1 + 2
}
won't compile, but
func main() {
add(1, 2)
}
will.
I'm not a fan of the usage of {}
to distinguish. For one thing it's actually an ambiguity in JavaScript, which is why you need (a, b) => ({ ... })
to return an object literal, though that's not likely in Go unless something like the type-elided struct literals proposal gets accepted. The other issue is that it makes refactoring from one to the other significantly more annoying.
That being said, I'd rather have the readability boost that a lack of {}
provides over the nicety of having a consistent syntax for both. Or just always require return
and worry about it later.
Comment From: davidleitw
Please don't try to make Go obscure, the current situation is enough to solve the problem
Comment From: Jimmy2099
There are so many languages that provide this syntax, can you not use the go language to program, can you change a programming language, why add a new syntax, do you have to write many lines of code every day? Does saving a few words have a big impact on you? There are already so many grammars in rust and c++. My suggestion is that you should use c++ and rust. Or you should consider Fork go language git repository and maintain your own grammar.
Comment From: switchupcb
If you do want to add something, what we are interested in at this point is examples of real-world Go code that would benefit/suffer from the proposal. It's pretty clear that there's code where removing boilerplate (incl. types!) will improve readability. It's also pretty clear that there's plenty of code where leaving away types will reduce readability. Still, the more such concrete examples we have (we only investigated the std library), the better. We may have overlooked something.
I have provided https://github.com/golang/go/issues/21498#issuecomment-1132271548 to make the respective experiments if you are so inclined (yes, it requires a bit of work). — @griesemer
Y'all heard him.
Get to it.
Comment From: ianlancetaylor
Based on the experiment above (https://github.com/golang/go/issues/21498#issuecomment-1132271548), the func
without parentheses, as in func x, y { return x + y }
does not seem like a good choice in actual code. In particular, when this appears in a function call argument list as it often does, the fact that x, y
is not itself parenthesized suggests that that comma is separating arguments to the function, which it is not.
The arrow notation, (x, y) => { x + y }
is definitely shorter, but doesn't feel much like the rest of the language. Several people have pointed out that omitting the result type and also omitting the return
keyword makes it unclear that anything is being returned. So, with this notation, the return
keyword should be required as usual, as in (x, y) => { return x + y }
.
Maybe there is a more suitable notation that preserves both the func
keyword and parentheses but allows us to omit the types. Note that simply func(x, y) { return x + y }
doesn't work, as that is already valid Go, requiring x
and y
to be types.
Comment From: fzipp
Maybe there is a more suitable notation that preserves both the func keyword and parentheses but allows us to omit the types.
@ianlancetaylor
I already suggested :
, similar to how :=
turns =
into a type inferring and shorter notation:
func(x, y): { return x + y }
And in case we want to support a single expression form:
func(x, y): x + y
Comment From: Graham-Beer
Maybe there is a more suitable notation that preserves both the func keyword and parentheses but allows us to omit the types.
@ianlancetaylor I already suggested
:
, similar to how:=
turns=
into a type inferring and shorter notation:
func(x, y): { return x + y }
And in case we want to support a single expression form:
func(x, y): x + y
I don't mind the syntax of :
however I'm struggle to see why we are looking to remove the type. Go is a static language, that is one of its selling points. Go has a strong identity, why lose that? If you want to write functions like this, why not just use a dynamic language instead?
Comment From: Jimmy2099
Maybe there is a more suitable notation that preserves both the func keyword and parentheses but allows us to omit the types.
@ianlancetaylor I already suggested
:
, similar to how:=
turns=
into a type inferring and shorter notation:func(x, y): { return x + y }
And in case we want to support a single expression form:
func(x, y): x + y
I don't mind the syntax of
:
however I'm struggle to see why we are looking to remove the type. Go is a static language, that is one of its selling points. Go has a strong identity, why lose that? If you want to write functions like this, why not just use a dynamic language instead?
i think the syntax could be better,why not try to change
func(x, y): x + y
to
!(x,y):x+y
then gopher can save 3 more letter from typing
Comment From: sbinet
from a parsing POV, I guess :func(x,y) { ... }
would be better than func(x,y): { ... }
Comment From: fzipp
i think the syntax could be better,why not try to change
func(x, y): x + y
to
!(x,y):x+y
then gopher can save 3 more letter from typing
Ian's expressed desire was to find a way to preserve the func
to keep it looking like Go.
Comment From: SilverRainZ
Base on https://github.com/golang/go/issues/21498#issuecomment-1136174513, how about the syntax like func _(x, y) { x + y }
, it looks like a function declaration with an underscore as name, and can be used as expression.
Comment From: mibk
Maybe there is a more suitable notation that preserves both the
func
keyword and parentheses but allows us to omit the types.
I just wanted to point out that the short variable declarations also omits the var
keyword, so it seems to me we don't have to insist on preserving it.
Also I think places where we would most benefit from having a lightweight anonymous function syntax are those where we usually write simple one liners. In those use cases I wouldn't mind omitting even the return
keyword.
When the function body is larger, the requirement to use the standard anonymous function (with parameter types), as was already pointed out by many, actually helps to make the code more readable.
Comment From: seanmrnda
I honestly prefer
func
over symbols.Some languages like Elixir are more readable with more symbols, because that's a feature of those languages.
Go focuses on simplicity, and in my personal opinion, when looking at some code, seeing and understanding
func
is a lot easier, especially for beginners.Go code is also very easy to read in almost every project because it doesn't have too much syntax sugar.
JavaScript on the other hand, is sometimes hard to read because everyone uses the syntax they like to use, and not everyone can read it as easily as the person who writes the code.
I totally agree with this. func
keyword should be preserved. It would be very intuitive for the beginners.
Comment From: c3y28
When I need lazy evaluation
in order to reduce cpu occupying, ie. in logging facility, I expect this kind of Syntactic sugar
very much, since we may have thousands of kind of evaluations to be writen in logs.
Comment From: Rudiksz
Maybe there is a more suitable notation that preserves both the
func
keyword and parentheses but allows us to omit the types. Note that simplyfunc(x, y) { return x + y }
doesn't work, as that is already valid Go, requiringx
andy
to be types.
Maybe I missed it, but is there some technical reason func
has to stay, or is it just syntactic preference?
To paraphrase Rob Pike's saying, I came to Go because it helped me not have to "foo" so much. If it's possible at all, we shouldn't have to func
so much either.
Comment From: Rudiksz
It might be difficult to realize that the
Col
is returning in this case:
As somebody who uses arrow functions in other languages (not JS), it's perfectly obvious what it is happening in all your examples. Even the true one. There's nothing mysterious about arrow functions.
All the arguments about readability eventually boil down to the fact that this syntax would be new to Go, and some programmers don't think the extra learning effort is worth the benefits.
Comment From: DeedleFake
As somebody who uses arrow functions in other languages (not JS), it's perfectly obvious what it is happening in all your examples. Even the true one. There's nothing mysterious about arrow functions.
I agree 100% on the true
example. I don't find that one remotely confusing, either. The ones that I find a bit more awkward, however, are the ones that return the result of a function call. Something like (a, b) => { add(a, b) }
. true
on its own is not a valid statement in Go, so it's pretty obvious that it's doing something else with it, such as returning. add(a, b)
, on the other hand, is a valid statement, so I could see that being more confusing, especially in more complicated cases. I don't think it'll be massively confusing, but if a better alternative could be found that makes it more obvious, I'd be fine with that, though I still don't like having both () => ...
and () => { ... }
.
Comment From: ianlancetaylor
Maybe I missed it, but is there some technical reason func has to stay, or is it just syntactic preference?
It's syntactic preference after looking at the results of @griesemer 's CLs.
Comment From: Rudiksz
I still don't like having
both () => ...
and() => { ... }
The distinction shouldn't be between those two, because they are just stylistic difference.
The distinction should be between these two semantically different cases:
- for single line functions that return a single value, which apparently are 11% of the "rewriteable" functions in stdlib
(a, b) => a + b
- function blocks that need to do stuff, and/or return multiple values
``` (a,b) => { c := doXWith(a) d := doYWithB(b) return c + d }
My arbitrary opinion is that 11% occurrence of a pattern already warrants its own syntax, and maybe if we had the syntax this percentage would increase even more.
It's not like there's no prior art either. This problem has been solved quite effectively by other languages, both non-typed (JS) and strongly typed (Dart). In Dart `() => true` and `() => { return true;}` are valid, but not `() => {true}` or `() => return true`. While in this example the two valid forms are equivalent, the compiler is extremely clear when you try to use the wrong syntax in the wrong context.
The extra syntax actually helps with capturing the "programmer's intent".
**Comment From: jeffreydwalter**
At the end of the day one of the most compelling reason to use go is the simplicity and consistency of it's syntax. Go having a single style via `go fmt` has proved to be a boon. Having multiple ways to declare a func detracts from that. Go use the other languages that have solved this "problem".
**Comment From: davidleitw**
> At the end of the day one of the most compelling reason to use go is the simplicity and consistency of it's syntax. Go having a single style via `go fmt` has proved to be a boon. Having multiple ways to declare a func detracts that. Go use the other languages that have solved this "problem".
Agree, I think it is inappropriate to add new syntax just to add insignificant functionality
**Comment From: deanveloper**
@jeffreydwalter @davidleitw
And yet, Go has multiple ways to allocate slices/maps, multiple ways to define variables, multiple ways to loop over a slice, and multiple names for {`int32`, `uint8`, `interface{}`}. There are far more examples of "more than one syntax for similar ideas".
**Comment From: seanmrnda**
> @jeffreydwalter @davidleitw
>
> And yet, Go has multiple ways to allocate slices/maps, multiple ways to define variables, multiple ways to loop over a slice, and multiple names for {`int32`, `uint8`, `interface{}`}. There are far more examples of "more than one syntax for similar ideas".
Indeed, but `func` should not be ditched in anonymous function syntax.
**Comment From: jeffreydwalter**
> @jeffreydwalter @davidleitw
>
> And yet, Go has multiple ways to allocate slices/maps, multiple ways to define variables, multiple ways to loop over a slice, and multiple names for {`int32`, `uint8`, `interface{}`}. There are far more examples of "more than one syntax for similar ideas".
Yeah, and after almost 10 years I still pay the cognitive overhead of having to decide which syntax to use everytime I declare a variable.
Just because a language has X, doesn't mean it was a good decision, and is NOT a justification for making a new error.
**Comment From: zephyrtronium**
These exact statements have been made several dozen times now in this issue. It is not useful to say this makes Go more like some other language, or to say people should use a different language if they want different features. It is also not useful to say that Go has other examples of related things to try to correct people. The only function of making those comments is to waste the time of anyone following this issue for legitimate updates. I am apparently less patient with people who waste my time than others in that category, but I am certainly not the only one affected.
Things that are useful to say will generally include evidence. If you want to say that some type-inferred function literal syntax will improve the readability of your code, then provide code samples. If you want to say that a different syntax for the idea meets the goals of this proposal more effectively, then (this is at least fairly interesting on its own, but it's still more convincing if you) provide code samples. If you want to say such syntax will lead to inscrutable code, then provide code samples.
For a hint on the latter, maybe try things involving function literals with arguments and return parameters that can also be function literals – most kinds of syntax become less intelligible when deeply nested.
**Comment From: deanveloper**
This isn't particularly relevant to the current conversation, but I think it's relevant and (hopefully) new information. I have heard the "I have written in Go for _x_ years and anonymous functions were never a pain point for me" argument several times, and I have thought a lot about this because I actually agree with it, or at least used to. They were never a pain point for me until relatively recently, and I was never really sure why. But, my current suspicion is because of the introduction of generics. I think this wasn't as much of a pain-point in a pre-generics world, because the lack of static typechecking meant that it was harder to use type-safe generic functions, and therefore we saw fewer painful anonymous function signatures. However, in a post-generics world, I've now noticed that anonymous functions can be a pain to write. For instance, with `slices.EqualFunc`:
```go
s1 := make([]MyProductUser)
s2 := make([]otherproduct.OtherProductUser)
hasSameUsers := slices.EqualFunc(s1, s2, func (myUser MyProductUser, otherUser otherproduct.OtherProductUser) bool { return myUser.OtherProductID == otherUser.ID })
We would have never seen something like this in a pre-generics world though. Instead, we may have seen a pattern similar to sort.Slice
, which would take a func (i int) bool
:
s1 := make([]MyProductUser)
s2 := make([]otherproduct.OtherProductUser)
// definition of equalFunc is `func equalFunc(s1, s2 any, func (index int) bool) bool`
hasSameUsers := equalFunc(s1, s2, func (idx int) bool { return s1[idx].OtherProductID == s2[idx].ID })
However, this isn't fully type-safe. Specifically, s1
and s2
are not required (by the compiler) to be slices, and there is no guarantee that s1
and s2
are both used in the function call. Both of these things are guaranteed in slices.EqualFunc
, so it is much preferred over our makeshift equalFunc
in the previous example. However, the anonymous function in EqualFunc
today has a ton of boilerplate (as illustrated earlier), almost entirely caused by the anonymous function's signature. Inferring the types of the arguments to the anonymous function would be extremely beneficial and the return type could be inferred as well. I'll use Rust syntax (|arglist| ...
) in order to be agnostic to the discussion between func arglist { ... }
and (arglist) => { ... }
syntaxes. Anyway, with lightweight anonymous functions, we can see just how much lambda functions help calls to things like EqualFunc
:
s1 := make([]MyProductUser)
s2 := make([]otherproduct.OtherProductUser)
hasSameUsers := slices.EqualFunc(s1, s2, |myUser, ytUser| myUser.OtherProductID == otherUser.ID)
Previously, we didn't really have this issue because functions like EqualFunc
didn't really exist - we instead wrote separate functions that typically used closures/interfaces (ie container/heap
), or used reflection (ie sort.Slice
, or our makeshift equalsFunc
above) instead. But now that we have a cleaner way to take a function as an argument to query/update a generic data structure, anonymous functions seem to be a lot more common, which may be why we have only seen this come up recently.
Comment From: gophun
go hasSameUsers := slices.EqualFunc(s1, s2, func (myUser MyProductUser, otherUser otherproduct.OtherProductUser) bool { return myUser.OtherProductID == otherUser.ID })
Those are some lengthy Java Enterprise-esque names for parameters, packages and types, and of course you wouldn't write the function body on the same line:
hasSameUsers := slices.EqualFunc(s1, s2, func(u my.User, o other.User) bool {
return u.OtherProductID == o.ID
})
Comment From: DeedleFake
@gophun
Sometimes you don't have much choice, though. Here's an awkward one from one of my projects, for example. The type names are coming from a package someone else wrote. The only real alternative to using the lengthy type names is to alias them, which would be ridiculous and make things weird for someone trying to read it. Here's another from the same project. With anonymous functions, this one could be reduced to just { p1, p2 -> p1.HostName < p2.HostName }
.
Comment From: earthboundkid
An alternative solution might be to allow _
as an inferred type:
s1 := make([]MyProductUser)
s2 := make([]otherproduct.OtherProductUser)
hasSameUsers := slices.EqualFunc(s1, s2, func (myUser _, otherUser _) bool { return myUser.OtherProductID == otherUser.ID })
Comment From: DeedleFake
@carlmjohnson
I believe this was proposed above. It makes sense, and it also allows for a natural extension to partially inferred generics, such as someFunc[int, _, string](/* Arguments from which the second can be inferred. */)
. My biggest issue with it is that it looks kind of ugly, especially if you infer the return type: func(user1, user2 _) _ { ... }
, but other than that it seems like one of the best ways to do it. It's inferred, but it's still explicit, which feels pretty Go-like to me.
Comment From: deanveloper
@gophun Edit - I just read that @deedlefake basically said the same thing as this, so I'm hiding this myself. Keeping it here in case it's still valuable, expand if you want.
You might think my type names are long, but there is still truth to them. I wouldn't go as far as to say they're "java-enterprisey", maybe they are a bit verbose though. But, it's important to consider that you can't control the names that other packages use, so you can't simply change it to other.User
. If another package decides to use a long type name, you're forced to use it too (unless you make a type alias, I guess)
For instance, just look at already existing services. AWS has credentials.SharedCredentialsProvider
, GCP has a package named accesscontextmanager
and defined types in that package are even longer. I can't control these type names, and I'd love to not have to read them anywhere in my code (at all, if possible).
Comment From: rodcorsi
Taking this example, today I often see anonymous functions using short names for arguments, I think the reason for this is, as the type is necessary, using short names doesn't reduce de legibility of the code.
// Long names with types
hasSameUsers := slices.EqualFunc(s1, s2, func(myUser MyProductUser, otherUser otherproduct.OtherProductUser) bool {
return myUser.OtherProductID == otherUser.ID
})
// Short names with types
hasSameUsers := slices.EqualFunc(s1, s2, func(m MyProductUser, o otherproduct.OtherProductUser) bool {
return m.OtherProductID == o.ID
})
But if we have some kind of short syntax, the use of a short name could reduce the legibility, to me in this case long names are the correct choice
// Short names without types
hasSameUsers := slices.EqualFunc(s1, s2, func(m, o): {
return m.OtherProductID == o.ID
})
// Long names without types
hasSameUsers := slices.EqualFunc(s1, s2, func(myUser, otherUser): {
return myUser.OtherProductID == otherUser.ID
})
Using long names for slices brings some context, and it seems ok to use short names as arguments
hasSameUsers := slices.EqualFunc(myProductUsers, otherProductUsers, func(m, o): {
return m.OtherProductID == o.ID
})
Comment From: beoran
@deanveloper Now that you mention it, since a func(a, b) { return a == b } is not possible, perhaps a func |a,b| { return a==b } syntax could be possible? Placing the function arguments between || is common in Ruby and Rust, as you mention, so it would not be that hard to learn I think. Maybe we could run an experiment with this syntax?
Comment From: deanveloper
@beoran When I first saw the syntax in Rust, my initial impression was "well, that's a weird syntax". It doesn't seem to be analogous to anything else in the language, and (not knowing Ruby), it didn't seem to share syntax with other languages. Perhaps it's fine if other languages use it, however, it is a strange syntax compared to the rest of the language. I personally prefer if we kept func
somehow, but I'm not exactly married to it either. Would be good if we had a document of every language's shorthand anonymous functions syntax. Maybe I'll get working on that now.
Comment From: beoran
@deanveloper Great idea! Rosettacode seems like a good place to collect syntaxes: http://www.rosettacode.org/wiki/First-class_functions or http://www.rosettacode.org/wiki/Higher-order_functions
Comment From: DeedleFake
@beoran @deanveloper
Technically, Ruby uses the { |a, b| a + b }
syntax for blocks, which are a weird Ruby-specific thing, and ->(a, b) { a + b }
for anonymous functions.
Here are a few more syntaxes:
Language names are bold if they're statically typed.
Language | Syntax | Notes |
---|---|---|
Python | lambda a, b: a + b |
|
Kotlin | { a, b -> a + b } |
Niladic functions have no arrow, such as { 3 } . |
Dart | (a, b) => a + b or (a, b) { return a + b } |
|
JavaScript | (a, b) => a + b or (a, b) => { return a + b } |
|
Java | (a, b) -> a + b or (a, b) -> { return a + b } |
|
C# | (a, b) => a + b or (a, b) => { return a + b } or delegate(int a, int b) { return a + b } |
The delegate syntax was the only available syntax before C# 3.0. All variants also allow explicit typing, but delegate requires it. |
Rust | \|a, b\| { a + b } |
|
Haskell | \a b -> a + b |
The backslash is apparently supposed to look like a λ. |
Elixir | fun (a, b) -> a + b or &(&1 + &2) |
|
D | int delegate (int a, int b) { return a + b } or delegate (a, b) { return a + b } or (a, b) => a + b |
The shorthand version allows typing, too. |
Swift | { (a: Int, b: Int) -> Int in return a + b } or { a, b in a + b } or { $0 + $1 } |
I'm not 100% sure about this one. The syntax is bizarre. Anyone in here who knows Swift, feel free to correct me. |
Also: https://en.wikipedia.org/wiki/Anonymous_function
Comment From: griesemer
A couple of observations:
1. The JavaScript notation is closest (or the same) to what we've experimented with (=>
notation). Thus, this notation is widely known among programmers, with JavaScript probably the most widely use PL in the world.
2. If we want to keep the func
keyword one could borrow from Elixir and write, e.g.: func (a, b) => a + b
or func (a, b) => { return a + b }
. Basically the same as JavaScript with the additional func
keyword. The =>
is needed to distinguish the function signature from an ordinary signature with types.
As an aside, the "weird" Ruby notation comes of course from Smalltalk where one would write [ :a :b | a + b ]
Comment From: beoran
@griesemer func (a, b) => { return a + b } seems like a good compromise between the func notation and the JavaScript notation.
However, seeing that the keyword "delegate" is used in several languages, another Go-like approach would be to add a built in function named delegate which can then be used like this: delegate(a, b, a+b). The last argument is the returned expression, the other ones are the arguments.
Comment From: c3y28
It might be difficult to realize that the
Col
is returning in this case:As somebody who uses arrow functions in other languages (not JS), it's perfectly obvious what it is happening in all your examples. Even the true one. There's nothing mysterious about arrow functions.
All the arguments about readability eventually boil down to the fact that this syntax would be new to Go, and some programmers don't think the extra learning effort is worth the benefits.
Agree. All the concerns about readability will fade away after a short-term learning. Those who don't like this Syntactic sugar
may eventually prefer this styles when they want to lazy evaluation
on the function or they use a famous programe pattern map-filter-reduce
. short anonymous function definition is helpful to write more readable code especially the business logic is very very complex(ie it requres multiple map functions and multiple filters before reducing)
Comment From: fzipp
- If we want to keep the
func
keyword one could borrow from Elixir and write, e.g.:func (a, b) => a + b
orfunc (a, b) => { return a + b }
. Basically the same as JavaScript with the additionalfunc
keyword. The=>
is needed to distinguish the function signature from an ordinary signature with types.
Great, if func(a, b) => a + b
is possible from a parsing standpoint, then func(a, b): a + b
is possible, too.
Comment From: DmitriyMV
func(a, b): a + b
It looks a lot like map initialization where left part is key and right part is a value.
Compare
func(a,b) => {
c := a + b
return c, nil
}
to
func(a,b): {
c := a + b
return c, nil
}
To my tastes its too similar to typeless map inialization, that is:
"foo": {
Foo: "bar",
Bar: 1,
},
"bar": {
Foo: "baz",
Bar: 2,
},
"baz": {
Foo: "qux",
Bar: 3,
},
Comment From: fzipp
It looks a lot like map initialization where left part is key and right part is a value.
While I don't hate the extra =>
I don't love it either. I find it a bit too ASCII-arty. In my opinion the func
keyword makes it clear enough. Keywords are known to the programmer, they are unambiguous and unchangeable. Some people even highlight them in their editors. And I haven't heard of Python programmers mistaking lambda
functions for dictionary keys.
Comment From: deanveloper
Not that Go needs to support this, but figured it'd be a fun addition: Kotlin and Swift both have the idea of "trailing lambdas", which are lambdas that are the last argument to the function. They are very powerful and allow for the creation of DSL-like APIs. Essentially, the concept allows you to put the lambda outside of the parentheses for the function call, if the lambda is the final argument to the function. For instance (using Kotlin syntax):
// (note: "(Int) -> Unit" is the type which represents a function that takes an Int and returns the Unit ("void") type)
// This function
fun foo(a: Int, b: (Int) -> Unit);
// "Normally" called like this
foo(5, { someInt ->
println("called with $someInt")
})
// Can equivalently be called like
foo(5) { someInt ->
println("called with $someInt")
}
// Or this way, "it" is the default name for the first parameter of a lambda
foo(5) {
println("called with $it")
}
I'm not really advocating for this feature in Go per se, but figured that it would be a useful thing to bring up so that we can possibly learn from it.
Comment From: DeedleFake
Kotlin also allows you to omit the parentheses entirely if the closure is the only argument:
doStuff { a ->
stuff(a)
}
I'm not so sure this kind of cutesy syntactic trick makes sense in Go, though.
Comment From: earthboundkid
Ruby also works that way.
For my part, I was in favor of this feature before I listed out the pros and cons, about fifty “load more comments” ago. ;-) Having really considered it, I think the burden of explaining to users that there’s a new function syntax but it only works when the types can be inferred is too hard. For example, the Go Time Podcast discussed this issue recently and from the conversation, it wasn’t clear to me as a listener if they were aware that the new syntax would only work when types could be inferred and I don’t think a listener who didn’t know that would have gotten that impression. It’s too confusing and subtle as a distinction. So I think it would be better to find ways of adapting the current syntax to infer types. For example, besides using underscore, there could be a go.mod rule that if the version is 1.20 or higher than func(a, b) has inferred types instead of types a and b.
Comment From: griesemer
For example, besides using underscore, there could be a go.mod rule that if the version is 1.20 or higher than func(a, b) has inferred types instead of types a and b.
That means that existing (pre-1.20) might not work if it's simply copied, w/o suitable adjustments or go.mod adjustments. That seems too brittle. We've been very careful to maintain backward-compatibility under almost all circumstances; we've gone through great lengths to ensure it even for such fundamental changes as generics. We shouldn't give that up for syntactic sugar.
The issue at stake here is really a suitable notation that we find palatable after some getting used to (every new notation needs some getting used to).
I'm not concerned about users not being able to understand that the light-weight function syntax "only works when the type types can be inferred". At a first approximation this is always true when a function is passed as an argument to another function (which is the vast majority of use cases). So the rule of thumb is trivial: the light-weight function notation can be used when passing a function to another function. If that extra decision is one decision too many, then simply always use the lightweight function literal notation when passing a function to another function.
As an aside, "type inference" here implies some heavyweight compiler mechanism: the reality is much simpler. The function type is simply copied from the parameter list of the callee (or the LHS expression of an assignment, as the case may be), and then the argument names are suitable replaced by the argument names provided in the lightweight function literal. There's no magic here. The primary benefit of lightweight function literals is removing boilerplate, exactly because the types are simply repeated.
Comment From: aarzilli
@griesemer
As an aside, "type inference" here implies some heavyweight compiler mechanism: the reality is much simpler. The function type is simply copied from the parameter list of the callee
I think it's interesting to note how many examples posted in this thread wouldn't work with this. A short list of examples selected from older posts:
https://github.com/golang/go/issues/21498#issuecomment-771214926 https://github.com/golang/go/issues/21498#issuecomment-885902408 https://github.com/golang/go/issues/21498#issuecomment-1091880619
Leaving aside my general dislike for the idea, I think the type inference question might have been handwaved away too much.
Comment From: DeedleFake
@aarzilli
I'm not sure what you mean. Unless I'm horribly misunderstanding something here, that sure seems like it works with all of those.
Comment From: aarzilli
Say you have
func Map[Tin, Tout any](in []Tin, f func(Tin) Tout) []Tout
Map([]int{1, 2, 3}, (x) => { /* body of the function omitted from this example */ })
where does the return type come from? If the type is just copied from the formal arguments it's func(int) Tout
which is incomplete.
Comment From: DeedleFake
In that case, I would expect the compiler to complain about an inability to infer Tout
. But the error is in the call to Map()
, not the usage of the anonymous function. If it does the copy before the generic inference, if that's possible, there should be no problem, I think. In other words, it would determine the function signature to be func(Tin) Tout
via the copy, and then fill those in via the generic type inference, which would then fail.
Comment From: earthboundkid
That means that existing (pre-1.20) might not work if it's simply copied, w/o suitable adjustments or go.mod adjustments. That seems too brittle. We've been very careful to maintain backward-compatibility under almost all circumstances; we've gone through great lengths to ensure it even for such fundamental changes as generics. We shouldn't give that up for syntactic sugar.
I would be surprised to learn that very much code actually uses the func (atype, btype) format. Perhaps someone can quantify it with a corpus analysis. If the change were made, it would need to come with a rewriter triggered by go mod edit -version=1.whatever
. That might still catch out anyone who manually changed their go.mod version, but at that point, it’s an easy fix for what I think is a small number of users.
Comment From: aarzilli
In that case, I would expect the compiler to complain about an inability to infer
Tout
Right, which was my point, that people are writing examples of code that won't work.
Comment From: DeedleFake
@aarzilli
At least the first example that you linked to doesn't use generics. It should work fine, no type inference required.
Comment From: aarzilli
At least the first example that you linked to doesn't use generics. It should work fine, no type inference required.
Fair enough, edited.
Comment From: beoran
@carlmjohnson In Magefiles, the func(type) syntax is used often, so it should stay usable in the future. It is a very handy syntax to simulate namespaces within a single package. That's why we have to find a different syntax for this.
Comment From: tv42
simulate namespaces within a single package
As far I understand, the Mage trick is more about methods added to dummy types, with the receiver value not being captured. I don't think that has to hinder this.
type MyGrouping mg.Namespace
func (MyGrouping) MyTaskName() error {
...
}
Comment From: earthboundkid
Yes, the change would only need to apply to closures/callbacks where the types are already known from contexts and the other interpretation would be a type error.
Comment From: zigo101
If this proposal is for type inference purpose, I hope it also applies to the following alike cases:
func foo() {
var x func(int, int) (int, error)
x = func(a,b) => {
c := a + b
return c, nil
}
...
}
Comment From: griesemer
@go101 Yes it is, as has been said a few times. This is also "assignment context".
Comment From: gazerro
In the discussion so far it has been assumed that since func(x)
already has a meaning in Go then this syntax cannot be used, however it does not seem to me that there is ambiguity for the compiler. The type checker knows if x
in func(x)
is a type or not.
In this example
var f func(a int)
f = func(x) { }
According to the current specification, x
must be a type and must be the type int
, otherwise the compiler fails with an error.
We can change this spec in this way:
If x
is a type, the type must be int
. If x
is not a type, func(x) { }
is a function literal with inferred types and it is validated accordingly.
Examples:
var f func(a int)
f = func(int) { } // int is a type so the compiler checks that it is the same type as the 'a' parameter
f = func(v) { } // v is not a type so the compiler infers the type from the type of the 'a' parameter
f = func(v) int { } // an error occurs: "v is not a type" or "inferred function literals cannot have return types"
Note that we already have cases when a name can be a type or a value. In the x.m
expression, x
can be a type or a value with diffent meanings.
If we accept this spec, we may also accept the following
sort.Slice(s, func(i, j) s[i] < s[j])
// can be used instead of:
sort.Slice(s, func(i, j int) bool { return s[i] < s[j] })
Comment From: hherman1
@gazerro it was a goal of the generics proposal to keep code parsable without evaluating types. I suspect that constraint applies here too.
Comment From: DeedleFake
Actually, he might be onto something. Since this is only in an assignment context, the compiler might be able to copy the types without evaluating them. In other words, given the fact that you're assigning to func(int) int
, anything other than func(int) int
would be illegal in current Go, so if there are only types given and they're not the right ones, assume it to be new identifiers instead. Then do normal type checking later.
In other words, given var f func(int) int
, if I try to do f = func(x)
, automatically expand that to f = func(x int) int
by copying the old types, then do type checking normally. All existing code of that kind would be illegal, unless it exactly matched with possible aliasing, in which case it'll get expanded to f = func(int int) (int int)
. In that case, you can add a flag that says that it's been expanded, and then unexpand it during the type checking phase if the identifiers match like that.
I don't know how feasible any of this is, but it actually sounds vaguely plausible to me.
Edit: To clarify, what I'm saying is that an assignment involving a function literal with at least its argument or return list involving only types is put into an ambiguous state that can then be tracked and cleared up during type checking, Unlike the generics problem with x, y = a < b, c > (d)
, its not actually resolving a syntactic ambiguity with completely separate grammars, Rather, it's just resolving an ambiguity in terms of what type of identifier is being declared, if any.
That being said, this could result in more confusion, though I doubt it would be too bad. For example, something like doSomething(a, func(x, y) { ... })
might not look obvious if x
and y
are actually types or are arguments, but with the body it would be pretty obvious, such as doSomething(a, func(x, y) { return x + y }
, because you can't use a type like that.
Comment From: gazerro
@hherman1 I think this goal remains. In what cases, with the new spec, does the parser need to know the types to parse the code?
Comment From: gazerro
@DeedleFake
That being said, this could result in more confusion, though I doubt it would be too bad. For example, something like
doSomething(a, func(x, y) { ... })
might not look obvious ifx
andy
are actually types or are arguments, but with the body it would be pretty obvious, such asdoSomething(a, func(x, y) { return x + y }
, because you can't use a type like that.
I think the cases where you read doSomething(a, func(x, y) { ... })
in code and x
and y
are types are very rare. And in case it is not evident when reading the type names and the function body that they are types, the code can be written as doSomething(a, func(_, _) { ... })
with the new spec.
Comment From: japhib
@DeedleFake For completeness/correctness, I just wanted to point out the first Elixir example you gave earlier is wrong.
You wrote this:
fun (a, b) -> a + b
but it should be this:
fn a, b -> a + b end
Comment From: Hypnotriod
Is this topic dead?
I guess most of the people will use arrow syntax to pass anonymous callback/predicate functions as a parameter. So why not to add this feature at least? As a receiver function will fully specify the signature of such function, so such syntax is a bit redundant:
mapAThing(func (thing TypeGenericThing) TypeSpecificThing {
return toSpecificThing(thing, someOptionsFromContext)
})
in comparison with:
mapAThing(thing => toSpecificThing(thing, someOptionsFromContext))
Comment From: DeedleFake
Is this topic dead?
I sure hope not. The more I use generics and packages such as slices
that use generics, the more I want this.
Comment From: earthboundkid
I supported this proposal before I did the full pros/cons list. Now having seen all the cons, I support instead finding a way to make func(a, b, c)
mean func(a A, b B, c C) R
instead of func(_ a, _ b, _ c)
in cases where the type system requires an A, B, C → R function. ISTM, it's backwards compatible (since the code was broken before), and it's not more confusing than a second function syntax which can only be used sometimes.
Comment From: amtdx
mapAThing(thing => toSpecificThing(thing, someOptionsFromContext))
I would vote for ->
over =>
, which is harder to type in non-US keyboards.
I would also vote for the syntax being limited to arguments -> single auto-returned statement
rather than anything more complex at this point.
Most languages have a syntax for method / function definition and another for lambdas. The popular exception is Javascript, but even it has gone in that direction. The reason there's two syntaxes is that one is a full definition while the other is a mere compact literal for callers.
Comment From: adambudziak
I'd like to suggest a narrower approach that I hope will be considered less vulgar to the language: make a slightly shorter syntax for cases when the function only has one statement and it's a return
.
With recent development in generics I found myself implementing simple conversion functions for different things. Cases like converting a domain-level struct into an db-level struct or API-level struct.
The code I write very often looks somewhat like this:
apiXs := common.MustConvert(domainXs, func(x domain.X) api.X {
return api.X{
fieldA: x.FieldA,
ys: common.MustConvert(x.Ys, func(y domain.Y) api.Y {
return api.Y{
fieldB: y.FieldB,
zs: common.MustConvert(y.Zs, func(z domain.Z) api.Z {
return api.Z{
Field1: z.Field1,
Field2: z.Field2,
}
}),
}
}),
}
})
so, it basically just converts one thing to another, nothing interesting here. But I feel it's quite hard to read due to all the nesting boilerplate.
The thing I'd like to propose is to let to write
func(z domain.Z) => api.Z { Field1: z.Field1, Field2: z.Field2 }
instead of
func(z domain.Z) api.Z { return api.Z {Field1: z.Field1, Field2: z.Field2} }
I have no strong opinion about =>
, I just feel there must be some extra token to make this syntax distinguishable from the normal one. Maybe it can be dropped, I'm no expert.
I know it doesn't seem significant, but look at the example above with the simplified syntax:
apiXs := common.MustConvert(domainXs, func(x domain.X) => api.X {
fieldA: x.FieldA,
ys: common.MustConvert(x.Ys, func(y domain.Y) => api.Y {
fieldB: y.FieldB,
zs: common.MustConvert(y.Zs, func(z domain.Z) => api.Z {
Field1: z.Field1,
Field2: z.Field2,
}),
}),
})
Generally, I think this syntax would greatly improve many of the cases where we just want to combine a few simple functions; if the function is supposed to do anything else but return
, it needs to use the normal syntax.
like
Map(func(x int) => x * 2, Filter(xs, func(x int) => x > 5))
instead of
Map(func(x int) int { return x * 2 }, Filter(xs, func(x int} bool { return x > 5 }))
Comment From: ncruces
@adambudziak how is the return type inferred?
Is it the type of the expression returned, or does it come from the usage context (e.g. the type of the Filter
argument)?
Comment From: adambudziak
@ncruces personally I feel inferring by the type of the expression returned is more viable.
here
double := func(x int) => x * 2
the compiler will know immediately that the type of x
is func(int) int
and it should raise errors if someone tries to pass it to a place which expects func(uint) uint
(as it would for normal functions right now).
I really meant the equivalence of the syntax. If you get
double1 := func(x int) => x * 2
double2 := func(x int) int { return x * 2 }
there should be no difference in how double1
and double2
behave or are treated by the compiler.
Comment From: ghost
你好,由于垃圾邮件泛滥,此账号已经停用。 请将邮件重发到 @.**@.** 谢谢。
Comment From: aarzilli
If you want the return type to be inferred from the body of the function you have to say how this would interact with type inference for type arguments of functions.
Comment From: billinghamj
I don't think this will help much at all, as it still requires types to be specified. We already know:
- that in realistic situations, type names are often very long, and often out of the control of the person using external packages - just removing the word "return" or some braces essentially does not help when the types are the primary issue
- that the compiler is able to infer the types in this situation
Comment From: sammy-hughes
@adambudziak
I'd like to suggest a narrower approach that I hope will be considered less vulgar to the language: make a slightly shorter syntax for cases when the function only has one statement and it's a
return
. ...
Meh. The reason this feature excites me every time I think of it is to be able to conveniently use propagators and combinators without pontificatiously repetitive argument and return types, especially when I don't in advance know that I will end up mapping the result of an enumerated result of a filtered result of a mapped result of a ... and so on. The form you suggested does not change that situation.
I get that it's a total A->B problem, but the following tackles your common.MustConvert: X, Y, Z
example with the same number of assignments in the applicable scope, so the pattern should be a direct swap for the kind of pattern you find understandably uncomfortable. Whether or not you find it improved over your suggestion, it's at least equally a dramatic improvement over your original example.
apiXs := func(convert) {
convert_z := func(z) {api.Z{Field1: z.Field1, Field2: z.Field2}}
convert_y := func(y) {api.Y{fieldB: y.FieldB, zs: zs: convert(y.Zs, convert_z)}}
convert_x := func(x) {api.X{fieldA: x.FieldA, ys: convert(x.Ys, convert_y)}}
return convert(domainXs, convert_x)
}(common.MustConvert)
and the mapping example you gave is similarly already fine, given an implementation as described previously to your suggestion: Map(func(x) {x * 2}, Filter(xs, func(x) {x > 5})
(I copy, paste, adapted it. It's not clear if you meant Map(func, slice)
and also Filter(slice, func)
)
Nothing personal, @adambudziak, but I want this feature. Fat arrow, skinny arrow, naked func
, or the ?consensus? func(...) {...}
with inferred types, I WANT TO USE IT! That doesn't happen unless folk stop suggesting alternate versions of this feature!
Comment From: xiaokentrl
许多语言都提供了一种轻量级的语法来指定匿名函数,其中函数类型是从周围的上下文中派生出来的。
考虑 Go 之旅 ( https://tour.golang.org/moretypes/24 )中的一个稍微做作的例子:
```go func compute(fn func(float64, float64) float64) float64 { return fn(3, 4) }
var _ = compute(func(a, b float64) float64 { return a + b }) ```
在这种情况下,许多语言允许省略匿名函数的参数和返回类型,因为它们可能来自上下文。例如:
scala // Scala compute((x: Double, y: Double) => x + y) compute((x, y) => x + y) // Parameter types elided. compute(_ + _) // Or even shorter.
rust // Rust compute(|x: f64, y: f64| -> f64 { x + y }) compute(|x, y| { x + y }) // Parameter and return types elided.
我建议考虑在 Go 2 中添加这样的形式。我不建议任何特定的语法。就语言规范而言,这可以被认为是一种无类型函数文字的形式,可以分配给函数类型的任何兼容变量。这种形式的文字将没有默认类型,并且不能以与错误
:=
相同的方式用在 a 的右侧。x := nil
用途 1:Cap'n Proto
使用 Cap'n Proto 的远程调用采用一个函数参数,该参数通过请求消息进行填充。来自https://github.com/capnproto/go-capnproto2/wiki/Getting-Started:
go s.Write(ctx, func(p hashes.Hash_write_Params) error { err := p.SetData([]byte("Hello, ")) return err })
使用 Rust 语法(仅作为示例):
go s.Write(ctx, |p| { err := p.SetData([]byte("Hello, ")) return err })
用途二:errgroup
errgroup 包 ( http://godoc.org/golang.org/x/sync/errgroup ) 管理一组协程:
go g.Go(func() error { // perform work return nil })
使用 Scala 语法:
go g.Go(() => { // perform work return nil })
(由于在这种情况下函数签名非常小,这可能是轻量级语法不太清晰的情况。)
good
//Hand Error
func main() {
f, err := os.Open("/test.txt") :e {
fmt.Println( e )
}
f, err := os.Open("/test.txt") :e HandEorr( e )
//or
f, err := os.Open("/test.txt") e: {
fmt.Println(e)
}
f, err := os.Open("/test.txt") e:
}
Comment From: madhanganesh
I think this the arrow syntax is more a syntactic sugar that makes code readability lot easier. I hope this feature of using -> (or =>) and type inference is coming to go soon.
Comment From: ty1824
Forgive me if I've missed some concrete developments here - there are 458 comments dating back to 2017 ~and I haven't read through the entire history.~ edit: I have read most, and believe this is at least a somewhat different take on the problem, highlighting the possibility to split into smaller problems & frame it as an evolution of the existing syntax.
I'm a language developer by trade but a newcomer to Go. I've only worked on a single project, but after completing it my first step was to search for this exact proposal. There are many situations in which functional composition could lead to shorter, likely even more readable code, but instead I feel like I have to make a choice between reading grossly verbose inline signatures like the following littering my code:
Form A (current):
func(msg somePb.SomeProtoMessage, ctx context.Context) (*otherPb.OtherProtoMessage, error) { ...
This pops up all over my code when trying to work with connections and operations gracefully. I avoided creating additional functional utilities because the signatures above were just for a single argument, let alone any functions that take multiple function arguments.
With some type inference on arguments (as has been suggested frequently in this thread), the above could read, 40 characters shorter, as:
Form B (parameter type inference):
func(msg, ctx) (*otherPb.OtherProtoMessage, error) {...
This already is a solid savings, and only lightly departs from existing syntax, which is often ideal to maintain consistency in the language. More syntax = more to learn & more likelihood for confusion. Additionally, this avoids the need to handle return type inference, which some folks have noted could be problematic. Though the same point could be made for parameter types, this minimizes the problem.
Of course, as many folks have mentioned, some lightweight syntax to encourage return type inference would also be useful (replace **
with your favorite two character arrowism):
Form C (return type inference/arrow):
func(msg, ctx) ** {
Now the signature is down from a whopping 86 characters (prior to the curly brace) to 13, even without removing the traditional func
(which does make parsing a bit more straightforward!)
And sure, if the language is feeling even more adventurous, it could always go further to
Form D (remove func
):
(msg, ctx) ** {
bringing it down to 9 characters. But this is negligible compared to the 85% reduction from the original 86 character Form A to Form C's 13, and does introduce further concept/awareness overhead. So I would not necessarily encourage this path as a starting point, rather something that could be evolved to if it appears necessary.
Given either Forms C or D: it is perfectly reasonable to assert that a construct like this would lead to less-readable code; the parameter types aren't present and must be known/referenced. However, even before we get to the prevalence of modern tooling that makes this much less of an issue, it can still be argued that this is more readable. Rather than being bogged down by the signature, I can focus on the logic of the function.
In the context of modern tools (I have been using Visual Studio Code to develop), this becomes even more compelling. Simply hovering over an element provides immediate information (The VS Code Go extension developers are fantastic, thank you so much!!!). Even GitHub can provide code insight while browsing repositories.
It really seems like it's time to make a decision here. I'd even argue that forms B and C aren't mutually exclusive and could coexist. But the nice thing here with B and C is that they could be framed as an evolution of the language - incrementally adding new features to the language (parameter type inference, then return type inference/arrow syntax), rather than releasing a drastically new syntax.
Comment From: atdiar
One could argue also that it is mitigated by code completion and AI tools that will write the code for you.
The simpler/more consistent the rules, the easier for an AI tool to infer.
Just another data point to think about too. :)
Comment From: billinghamj
@atdiar They certainly do help with writing code (though sadly Copilot doesn't seem to look at the language server's data when it's making suggestions)
But the real issue is readability after that point - the crazy-long lines from generated type naming make code harder to understand and maintain
Comment From: atdiar
Yes, I agree. I have code that make use of callbacks quite more extensively than most regular go code. So I actually wanted at some point to be able to elide the argument type info as well.
On the other hand, it's also interesting to note that sometimes, consistency trumps ease on the eyes. Easier to read (terseness?) does not necessary mean easier to understand (familiarity with a single idiom).
Comment From: earthboundkid
I've argued before that in cases where the existing type restrictions make func(a, b)
illegal because it needs to be func(A, B) C
, func(a, b)
should be interpreted as func(a A, b B) C
instead of func(_ a, _ b)
. It would be backwards compatible, and it doesn't introduce any new syntax. The hard part would just be education/awareness, but we already have other forms of type inference, so it's not a huge conceptual leap.
Comment From: ty1824
On the other hand, it's also interesting to note that sometimes, consistency trumps ease on the eyes. Easier to read (terseness?) does not necessary mean easier to understand (familiarity with a single idiom).
True, however Go already has the concept of type omission (mostly due to inference) in various places. a := "hello"
is a type omission, albeit with different syntax than func(a) error { a + "hello" }
func foo[T any](v T) T { return v }
func bar() { foo(5) }
And here is another form of type omission. I do not need to provide the type argument for foo
when invoking it from bar
. So conceptual mismatch isn't really a problem - omission is already present and must be accounted for when reading Go code.
This is mainly an expansion of the point @carlmjohnson just made
The hard part would just be education/awareness, but we already have other forms of type inference, so it's not a huge conceptual leap.
Now, I believe that this really does need to be broken down into smaller decisions for it to be solved. It's too easy to take an entirely new syntax and tear into it for a number of reasons - it's much easier to take a single change (parameter inference vs return type inference vs arrow syntax) and weigh the pros/cons and make a decision.
Given an example function signature:
func(a TypeA, b TypeB) (TypeC, error) {...
Should Go support function literal parameter type deduction? If so, which syntax?
1. func(a, b) (C, error) {...
2. func(a _, b _) (C, error) {...
3. No change.
(Maybe I'm missing a few obvious ones but it really feels like viewing this as a standalone new feature narrows the options significantly)
Comment From: ty1824
Given the same example function signature as above:
func(a TypeA, b TypeB) (TypeC, error) {...
Should Go support function literal return type deduction? If so, which syntax?
1. func(a TypeA, b TypeB) {...
2. func(a TypeA, b TypeB) _ {...
3. func(a TypeA, b TypeB) ** {...
where **
could be any new syntax: ->
, =>
, etc.
4. No change.
Either of these two problems can be answered and solved independently, allowing for some forward progress without overcommitting. Does not stop them from being solved at the same time, if that ends up happening.
Should we create each of these as a new issue so they could be discussed/solved separately, if necessary?
Comment From: atdiar
From experience trying to come up with such syntax for callbacks and from the rewrite experiments at the begining of the thread, I personally think that return type elision is confusing.
Eliding argument types however is not confusing since their names act as placeholders. But this syntax seems to be hard to retrofit backward-compatibly.
The earlier point is more geared toward having two syntax for function calls rather than mere type elision. In one case, it would be just the same syntax with less info. In the other, one would introduce a new arrow symbol, so it is essentially a new visual token that people or machines have to understand. Perhaps not a big deal but from the javascript experience, it still adds some conceptual complexity.
Comment From: jcsahnwaldt
I find the latest comments about type inference a bit confusing...
When this proposal was created, Go didn't have generics. There was no type inference, and this proposal didn't need any type inference either.
Yes, any proposal for a short syntax for anonymous functions would allow omitting types when writing an anonymous function. But that doesn't mean the types are inferred. Rather, they are declared explicitly. Just not in the place where the function is defined, but where its type is declared.
Example: func foo(bar func(int) int) { ... }
explicitly and fully declares the type of bar
. In code like foo(x => x+1)
, the compiler doesn't have to infer the parameter and return types of the anonymous function. It just looks them up (in the declaration of foo
and its parameter bar
).
Of course, if bar
is declared as a generic function the story gets more complicated, and there may be type inference. But that's a separate issue.
I'm not sure whether TypeA
, TypeB
etc. in the latest comments are meant as generic types, but I think they're meant to be concrete types (e.g. structs defined somewhere else). If that's the case, then there's no type inference needed in these examples.
More precisely: I'd say it's just type lookup, not type inference. I guess one could call it type "inference", but it's a kind of "inference" that is much, much simpler than the kind of type inference used with generics.
I hope this helps avoid the confusion from apparently using the word "inference" for very different issues. Or am I missing something?
Comment From: atdiar
It is called type deduction in Go usually but one could argue that it's a flavor of type inference. :)
Comment From: ty1824
Yes, Type Inference is a general term across languages that can refer to simple inference or more complex (constraint-based, flow-based, etc.) inference. In general, if something is not explicitly declared at a usage site, it is inferred. Some languages/communities use more specific terms for different versions of inference, and that can be helpful to distinguish without a lengthy explanation.
The goal of my examples was to demonstrate new syntax that would not require additional inference capabilities as the types could be derived from the immediate context (function call, assignment, etc). I'll be sure to use type deduction in the future!!
Comment From: mrwonko
This was suggested in #58559 as a way to quickly turn an operator like <
into a function to pass around.
But in my usecase of func cmpopts.SortSlices(lessFunc any)
, the types can't be deduced automatically, lessFunc
just needs to be any function of the shape func(T, T) bool
.
I would like to be able to call it like cmpopts.SortSlices(<[string])
, meaning cmpopts.SortSlices(func(lhs, rhs string) bool { return lhs < rhs })
.
Comment From: GiGurra
I would love to see a lightweight anonymous function syntax in go, but: * it is probably only useful in chains of operations (otherwise it wont help that much imo) * it would need to probably omit the return keyword to be useful.
If we get it though? very nice... :)
Comment From: Victor-Cooper
I'm sympathetic to the general idea, but I find the specific examples given not very convincing: The relatively small savings in terms of syntax doesn't seem worth the trouble. But perhaps there are better examples or more convincing notation.
(Perhaps with the exception of the binary operator example, but I'm not sure how common that case is in typical Go code.)
how about this example: https://github.com/golang/go/issues/59122
Comment From: steve-taylor
I like lambda function syntax where it makes sense. Coming from a JavaScript background, a lot of people have incorrectly treated =>
like it's the shiny new replacement for function
everywhere. I'm sure a lot of them assume that function
is deprecated.
There's a danger of the same thing happening to Go. To prevent this: * Go lambdas shouldn't allow a statement block, just a pure expression; and * Go linters should warn against creating a function and assigning it to a variable in the same statement.
Comment From: DeedleFake
Go linters should warn against creating a function and assigning it to a variable in the same statement.
This is too broad. There are plenty of valid cases for doing so. For example, you might do something like
f := func() { ... }
if condition {
f = func() { ... }
}
Comment From: seebs
We currently use :=
for assignment-with-type-inference, and that makes me think that if we needed a type-inference-hint for function-like things, maybe something parallel to that would make it easier to read.
(a, b int) :{ a+b }
okay i sort of dislike this but i do appreciate the sort of cute moustache emoji it unintentionally resulted in so i'm leaving it here.
Of the various syntaxes, the =>
ones have been the easiest for me to read. The biggest problem, I think, is that as soon as you start having to fill in all the missing details, you lose most of the clarity of that initial a => a.x
type function. something that looks like SortByKey(a => a.x}
is pretty easy to read; SortByKey((a mystruct) int { return a.x })
seems much less so. But then it only works for the very simple cases, and creates large friction the moment you cross whatever line-of-simplicity it is enforcing.
A vague thought: A function is really a weird combination of a type signature and a literal, but with the weird trait that the type signature has components which must be named. If I write map[int]string
, I don't need to name the int, but in a function, if I want func (int) string {...}
, I need to name the int to write that block, unless I'm ignoring the argument. And because names can be both object names and type names, func (a) b {...}
is ambiguous as to whether I intended to specify types (and I'm not going to use the parameter), or intended to specify names (and I'm assuming you can infer the parameter's type).
And really, the block part of a function is a lot like other blocks, except that the func
declaration has introduced a scope that gets used with that block. There's a similarity between for i := range foo { /* do things with i */ }
and func (i int) { /* do things with i */ }
, and I think you can talk about the block syntax separately from the scope-introduction syntax. And the obvious thing would then be to say that, in something like a => a.x
, we're just eliding the braces from our block, but actually I think C has convinced me firmly that optional braces were a tragic mistake. else[*].
So, say we have a named function type; type Sorter[T any] func(T, T) bool
. We could now allow Sorter(a, b) {...}
, and omit the type declarations, which are present in the definition-of-Sorter. But then this is confusing because it's hard to distinguish a function with no return from a function whose return type we want inferred.
I think any syntax for this has to be distinct enough from the existing function syntax that it can't be mistaken for a partial or incorrect function definition.
[*] you've heard of the dangling else problem, this is the actual dangling else we were talking about.
Comment From: griesemer
@seebs Note that the :
in :=
stands for declare the variables on the RHS rather than for infer their types (we don't have a :
in var x = 42
where we also infer the type).
Comment From: earthboundkid
Swift solves the problem of having to name the variables in a function literal by using $0 etc. Here’s a code snippet I found:
let string = "Hello, world!".transformWords { $0.lowercased() }
Comment From: Fryuni
if I want func (int) string {...}, I need to name the int to write that block
That is false though... You only need the name the value if you want to refer to the value, in the same way that you need to name a struct if you want to refer to it by name.
https://go.dev/play/p/mTvILwDsCJy
Comment From: TelephoneTan
if anyone is familiar with js, to support Promise-Chaining, we do need let caller determine the next output type.
promise.then().then().then()
Comment From: DeedleFake
Promises are a straight downgrade in 99% of cases from the concurrency primitives that Go provides. They only exist in JavaScript out of necessity due to the limitations of its single-threaded, event loop based model. While being able to chain methods in the way you describe would be nice, being able to do so for the purpose of creating a promise-style system is not a particularly convincing argument in my opinion.
Comment From: tschundler
func () {}
is just two more characters than () => {}
, and IMO Go has a good direction for itself keeping syntax and language features minimal and heavily scrutinized. I think it was once stated they want to make sure the language doesn't look like line noise, which certainly happens the more symbols you introduce.
But also Go tends to lend to a lot of visual noise have too much information. So, I do wish that I could simplify things a little, and I think the best way is to allow removal of types in function signatures (and maybe returns for one-liners) where they can be trivially inferred. eg slices.ContainsFunc(foo, func(a) { a > 5 })
becomes valid instead of the more noisy slices.ContainsFunc(foo, func(a: int) { return a > 5})
It's not just useful for data mutation callbacks like the case above. This also comes in handy for table tests - often my table tests include a function to generate the input, or to mutate a base case before running some operation. It would be nice to just have func (base) {base.foo = "bar"}
just to have less repeated noise.
Go already does a degree of elision in things like anonymous structs and :=
, so I don't feel it's inconsistent with the language.
Comment From: neild
slices.ContainsFunc(foo, func(a) { a > 5 })
Unfortunately, func(a) {}
already has a defined meaning: A function taking an unnamed parameter of type a
.
Comment From: earthboundkid
Unfortunately,
func(a) {}
already has a defined meaning: A function taking an unnamed parameter of typea
.
I argue elsewhere in the mega-thread that if the type signature of the function is already fixed by the context, this can just be special cased to use the inferred type instead. I'm not a compiler expert, so I don't know how reasonable this is, but it feels to me like it would be doable as part of the type resolution process.
Comment From: griesemer
@carlmjohnson I don't know how your suggestion can reasonably work. Say we have a function:
func foo(f func(int)) { f(42) }
that requires a function argument f
. In current Go we can call this with a function argument like this:
foo(func(int int) { println(int) })
We just happen to name the argument int
- generally a bad idea but perfectly valid. We cannot make this invalid as it would be backward-incompatible.
Now we decide to "leave away the types" because we are in this special context. So we write:
foo(func(int) { println(int) })
The function signature func(int)
still is a perfectly valid signature in current Go. But the body won't compile anymore because int
is still considered a type. To make it "work" in this case, the compiler would have to decide that the signature is really the new form, where int
is meant to be the parameter name and where its type is left away, and then infer the type for it from the call, and then see if the function compiles. Again, note that the compiler cannot simply assume that it must be the new form because it may invalidate correct existing code.
More generally, functions can be arbitrarily complex, and so the meaning of a function signature may depend on arbitrarily complex code in a function body. That doesn't seem like a good choice for a programming language, and certainly not for Go.
Worse, there are even functions which are valid under both interpretations, and then the compiler can't make the decision at all. For instance, if the closure were func(int) {}
, both interpretations are valid. They happen to mean the same thing because the parameter is not accessed and it works out, but perhaps there are cases where that is not the case (I'd be curious to see those).
We really do need a syntactic difference (if ever so small) to distinguish between the existing function signature syntax and light-weight function signatures.
Comment From: earthboundkid
foo(func(int) { println(int) })
is not a compilable old form function, so it must be a new form function. foo(func(int) { /* body does not use 'int'*/ })
is valid under either interpretation.
Do we have an example of something that is valid under the old form but not the new form and would be broken by interpreting it as new form? I haven't been able to think of an example where this takes a working program and breaks it yet.
Comment From: zhuah
@griesemer
Perhaps the simplest approach would be to replace the func
keyword with lambda
, resulting in something like foo(lambda(int) { println(int) })
, in this case, int
would be recognized as the parameter name rather than the parameter type.
I'm curious about the Go team's perspective on this proposal, since it was raised back in 2017, which feels like a long time ago, and we already have many comments here.
Comment From: sammy-hughes
I'm curious about the Go team's perspective on this proposal, since it was raised back in 2017, which feels like a long time ago, and we already have many comments here.
@zhuah, the guy you replied to has a REALLY good idea what the Go team thinks of things!
We really do need a syntactic difference (if ever so small) to distinguish between the existing function signature syntax and light-weight function signatures.
I'm skimming over the history of this thread, but was there ever a parse-related objection to the func a, b {a+b}
syntax suggested by @bcmills back in this post?
Comment From: zhuah
@sammy-hughes I know @griesemer is one of Go creators and i respect him so much for his great work on Go, i apologize if my previous comment offended people, i'm not so good at English, and that comment was translated by ChatGPT.
What my question is that whether the Go team tend to accept this proposal or not, or are considering what syntax to adopt?
But why would syntax be a problem, so many languages have lambda expression, Python, JS, Rust and even C++ and Java, their lambda syntax are relatively similar, i believe that's also suitable for Go, even invent new syntax should take only months rather than years on such a small language change.
Of course, it's totally acceptable if Go team decide to decline this proposal for code readability, i just think the day-by-day discussions on syntax seems a bit useless.
Comment From: timothy-king
@zhuah Suppose one had the valid Go function: func Apply(lambda func(A) B, a A) B { return lambda(a) }
. Right now lambda is an valid identifier. If one makes lambda a keyword, this currently valid function would stop parsing (token LAMBDA instead an identifier token) and would no longer be valid Go. My understanding of your suggestion is that it is a backwards incompatible syntax change. It is not impossible for this change to happen, but the bar is extremely high. AFAIK Go has made 0 backwards incompatible syntax changes.
It is really important to get syntax decisions right. To maintain backwards compatibility, the language will be stuck with supporting programs using any new syntax for many years. New users will have to learn it. Programmers will have to read it. It imposes a cost on everyone using the language. This means that some discussions, where good suggestions haves both pros and cons, can take years to converge before progress is made. Progress can happen though, see generics. This process requests a lot of patience from all participants.
Comment From: griesemer
@carlmjohnson Even if it is always possible to decide whether code means the new form or the old form, since the decision would require checking whether the function body compiles dep. on the interpretation of the parameters, this seems like a pretty bad idea. Certainly not in the spirit of Go. Note that the function body may be long or complex and it may not compile in one or the other form because of other errors. It would be a very subtle and fragile mechanism. It seems safe to say that we'd not add such a mechanism to Go.
@zhuah What @timothy-king said. Also, yes, it's easy to invent new syntax and implement it (I have prototyped several versions of this a long time ago). But that is not the point. We try not to introduce new notation unless we have very strong technical reasons and a good amount of "buy-in" from the community. So far we have not had that. Also, it's not a huge priority item. It's a nice-to-have.
@sammy-hughes I believe the func a, b { a + b }
notation works fine (no parsing issues). But if I recall correctly it was not particularly liked by many people.
Comment From: earthboundkid
@carlmjohnson Even if it is always possible to decide whether code means the new form or the old form, since the decision would require checking whether the function body compiles dep. on the interpretation of the parameters, this seems like a pretty bad idea. Certainly not in the spirit of Go. Note that the function body may be long or complex and it may not compile in one or the other form because of other errors. It would be a very subtle and fragile mechanism. It seems safe to say that we'd not add such a mechanism to Go.
I'm not sure I understand the objection yet, because in my proposal, it would always use the new form based whether the context of the callsite constrains the type of the argument, and never based the body of the function argument itself. It would be shift in the intensional meaning of existing code (from f(func(a, b, c){ })
meaning f(func(_ a, _ b, _ c){ })
to it always meaning f(func(a a, b b, c c){ })
), but I'm still struggling to understand an example where it would take a valid program and give it observably different behavior or a breakage of working code (a change in extensional meaning, to use the philosophy jargon).
Comment From: earthboundkid
Here's a thing I don't like about my own proposal:
func f1(callback func(int)) { /* do something with callback */ }
func f2(callback any) { /* do something with callback using reflection */ }
f1(func(float64){ }) // would always be interpreted as f1(func(float64 int){ })
f2(func(float64){ }) // would continue to mean f2(func(_ float64){ })
Losing the compile time error for f1(func(float64){ })
would be a shame and lead to hard to debug issues. But it wouldn't break any existing code.
Comment From: griesemer
@carlmjohnson I see. Interesting. I suppose that could indeed work (I haven't been able to come up with code that could break so far, either). But it still seems rather fragile.
Comment From: avamsi
Sorry, I think I still don't understand @carlmjohnson's proposal -- how would the following snippet be interpreted?
package main
func f(g func(int)) {
}
func main() {
f(func(int) {
_ = int(42)
})
}
Comment From: earthboundkid
Okay, I think that’s an example of breaking existing code because you can’t use an integer as a callable.
Comment From: lnlife
I created a function to find first matched element from a slice of objects. It works like Array.prototype.find()
in Javascript:
func array_find[T any](a []T, fn func(u *T) bool) *T {
for k, v := range a {
if fn(&v) {
return &a[k]
}
}
var empty T
return &empty
}
And use it:
tom := array_find(users, func(u *User) bool {
return u.name == "tom"
})
What I expected is something like:
tom := array_find(users, (u *User) => u.name == "tom")
Comment From: sammy-hughes
What I expected is something like:
tom := array_find(users, (u *User) => u.name == "tom")
Yeah. I think everyone here is on the same page with you. What we can't seem to collectively figure out is what the specific expression should look like.
We all want to be able to throw a quick arithmetical statement or a map/slice-index operation or an attribute assignment as a function argument, or heck, just not have to write package-scope functions to be able to operate on generic parameters. What we can't settle on is whether it should be fat-arrow, skinny-arrow, vertical-bar, naked-func, or even whether it should include curly braces or not.
Comment From: hherman1
What about:
func(u *User) u.Name == “Tom”
In other words, a func without {} permits a single expression which is automatically returned.
Comment From: sammy-hughes
At risk of being spammy, multiple-choice reaction poll?
- 👍 like me, you just want this capability, whatever it looks like, within generous reason.
- 👎 The specific syntax matters to you, and there is a definite right/wrong way to represent lambdas.
- ❤️ require an explicit return as in
|a, b| return a+b
,(a, b) return a+b
,(a, b) => return a+b
,func(a, b) return a+b
, etcetera. - 👀 omit the return as in
|a, b| a+b
,(a, b) => a+b
, orfunc a, b { a+b }
- 😀 use the keyword
func
. Examples would befunc(a, b) { a+b }
,func(a, b) return a+b
,func(a, b) => a+b
,func a, b { a+b }
,func a, b => a+b
, and so on. - 🙁 this should omit an initial keyword entirely. Examples would be
(a, b) => a+b
,(a, b) return a+b
,(a, b) { a+b }
,|a, b| a+b
, etcetera. - 🚀 wrap the body in curly brackets, so it stays easy to parse, e.g.
(a, b) => { a+b }
,|a, b| { a+b }
,|a, b| { return a+b }
, orfunc a, b { a+b }
. A weird example would befunc{ a, b -> a+b }
. - 🎉this style of expression should not use curly brackets, even if that restricts this functionality or makes it weirder to use. Examples would be the
func(a, b) a+b
,|a, b| a+b
,(a, b) => a+b
,(a, b) return a+b
, etcetera.
EDIT 2024-02-13: forgot a counter-statement for "omit the return". Added heart statement as negation to eyeballs and boosted it by changing my eyeballs vote to heart.
Comment From: billinghamj
The really key thing is being able to omit type info. At that point, virtually any syntax would be great.
I wonder if the Go team could take this away as a clear desire for the ability, and that whatever syntax they think is best basically works fine?
Comment From: ianlancetaylor
Thanks, I think we generally understand that. But we haven't found a perfect syntax yet.
Comment From: DeedleFake
I like that func { a, b -> a + b }
mentioned above. Reminds me of Kotlin's syntax, but with a bit more telegraphing thanks to func
. It also kind of feels like a natural progression to me:
func F(a, b int) int { return a + b } // Named.
func(a, b int) int { return a + b } // Anonymous.
func { a, b -> a + b } // Typeless.
The ->
is a little unusual, but, again, it's fairly similar to a fair number of other languages that way, including Kotlin and Elixir and, to some extent, Java.
Not sure about the need for an explicit return
. I wonder how feasible it would be to say that the return
is omitted unless there's a semicolon, including automatically inserted ones, after the ->
. In other words,
func { a, b -> // Automatic semicolon here.
return a + b // Return now necessary.
}
The single-line version would need to be only an expression to the right of the ->
, not a statement. This would hopefully disssuade some ridiculous syntaxes and encourage people to write longer anonymous functions as multiple lines with return
s.
On the other hand, it may be best to just just omit the implicit return
for the initial version of this and add it later if it seems necessary. It would definitely help with things like slices.SortFunc()
, though.
Comment From: fzipp
My 2 cents
func F(a, b int) int { return a + b } // Named
func (a, b int) int { return a + b } // Anonymous
func (a, b): { return a + b } // Types inferred; with body
func (a, b): a + b // Types inferred; with single expression
Example:
slices.ContainsFunc(s, func(x): x < 0)
Comment From: sammy-hughes
I like that func { a, b -> a + b } mentioned above.
Yeah. I was just trying to cover the gamut in terms of examples. I personally declared it weird, but then it grew on me. Also, the skinny arrow is just a backwards channel-send/receive operator.
The result is pleasantly unambiguous without seeming alien.
Meanwhile, yeah. WHATEVER the syntax is, I just want it!
Comment From: jimmyfrasche
Ignoring the specifics of the syntax, there aren't really that many choices in general:
- :-1: we shouldn't do this at all
- :tada: omit func
- :heart: omit types
- omit return:
- :rocket: always
- :eyes: sometimes
Always omitting return is like python's lambda:
where you can only use it for a single expression with a single return.
Sometimes omitting the return is like javascript's fat arrow syntax where you can provide a single expression or a regular function body.
use the list above to vote for your must haves
Comment From: entonio
Brackets are probably ok in the US, but in other countries we usually have them tucked away behind key combinations. (There's a story about how 'programmers' always use the US layout because of this, but I doubt most do that, because we keep having to use other people's keyboards and vice-versa all too often). Now, that's already an issue when writing regular code blocks, but somehow when doing it inside an argument list, as these functions are mostly meant to be used, is even more of a chore. I bring this point up only because people in the US may not be aware of it. So the possibility of having plain -> a + b
would be wonderful, not just a matter of trading any 2 characters for any other two.
Comment From: chad-bekmezian-snap
Whatever we choose here, a restrictive syntax that does not allow for the use of return
and allows for only one expression, which makes up the function's body, are ideal to me. Additionally, omitting func
is a big win imo as well. The less syntax involved in utilizing this syntax, the clearer it will be to see what is happening in a simple expression.
Comment From: NatoBoram
The syntax I'm preferring the most is exactly the same as the normal syntax except the types are inferred, but that syntax is not an option because of https://github.com/golang/go/issues/21498#issuecomment-1903249011. It's also the most preferred option by the community in https://github.com/golang/go/issues/21498#issuecomment-1946975344.
The issue is that we need a slight difference, one character or so, so that existing code don't break.
One way would be to use fun
instead of func
for lambdas. This way, it looks almost identical and I think it respects Go's way of thinking.
Comment From: jcsahnwaldt
Maybe the func(arg _) _
syntax (inferred types, but with placeholders) would be an option that achieves somewhere between 50% and 80% of the benefits with 10% to 30% of the effort. Maybe not quite an 80-20 solution, but close.
I think this syntax was first suggested in https://github.com/golang/go/issues/21498#issuecomment-324726831 in 2017.
Cases like https://github.com/golang/go/issues/21498#issuecomment-1090879825 by @DeedleFake and https://github.com/golang/go/issues/21498#issuecomment-1091880619 by @bradfitz would become a good deal less painful if type names could simply be replaced by _
placeholders.
And it doesn't shut the door for other solutions. If users gain some experience with it and still think it's too verbose for cases like func(a, b _) _ { return a+b }
, support for syntax like (a, b) => a+b
can still be added. No harm done.
Comment From: sammy-hughes
I don't know why I didn't quite get the templating algebra necessary to allow arbitrary covariance between the different parameters and the return. Phew. Is it too late to just ask for generic, anonymous functions instead?
Just kidding....I think...
Comment From: eihigh
Although I do not agree with it very positively, I think that abbreviated notation could be introduced only when the body of the function can be written in a single expression.
func (a, b int) int { return a + b }
func (a, b int) = a + b
func (a, b) = a + b // can be inferred?
func (a, b int) = (a+b, a*b) // multiple returned value
In order not to add new tokens, I have chosen = for the symbol. It also looks like type aliases.
Comment From: leaxoy
After introduce iterator, lack of simplified function syntax makes code worse, think such case:
without simplified syntax:
x := iter.Filter(
iter.Map(s.Exprs, func(t *expr.Expression) result.Result[*value.Value] {
return e.eval(ctx, t)
}),
func(r result.Result[*value.Value]) bool {
return r.IsOk()
},
)
with a possible simplified syntax:
x := iter.Filter(iter.Map(s.Exprs, |t| e.eval(ctx, t)), |r| r.IsOk())
Comment From: earthboundkid
Also for writing a new iterator, a short function is IMO an improvement:
func Count() iter.Seq[int] {
return (yield) => {
n := 0
for {
if !yield(n) {
return
}
}
}
}
The noise of return func(yield func(int) bool) {
isn't great. Hopefully 1.24 can simplify it to return func(yield iter.Yield[int]) {
, but a short function syntax would be better.
And what if if !yield(n) { return }
could be written as try yield(n)
… 🤔 😄
Comment From: jimmyfrasche
Having recently written a dozen or so iterators to play with the feature I certainly agree that they are an excellent example of why sometimes you want the short syntax but with a full block instead of just an expression.
I would be quite happy with (args) => expr
or (args) => block
syntax.
Comment From: griesemer
@SuperWindCloud Golang is not standing still - it's simply not jumping the gun. It is trivial to add some lightweight function syntax, many different approaches have been proposed in this tread, and some have even be implemented as prototypes. A =>
-based notation might well be the right one. But we've been busy with other language changes recently (range-over-func, changes to alias type representation). We wil get to this one when the time allows and when there's a clear concensus on the approach. I don't think we have that yet.
Comment From: tooolbox
I still agree with the original comment from @davecheney
Please no, clear is better than clever. I find these shortcut syntaxes impossibly obtuse.
Eliding all the types makes the code harder to read and understand. It might not be "pretty" or "neat" to have all those types and syntax in there, and it might take longer to write (though we do have auto-completion in editors) but the code can then be read and understood. I have heard it said that code is written once and then read many times. Go's syntax is a breath of fresh air from this perspective and I hope we don't lose that characteristic to save a few keystrokes or "reduce noise".
Comment From: brightpuddle
@tooolbox
We've had type inference in Go since release with i := 1
instead of var i int = 1
, and also in the val, err := myFunc()
. When the type is obvious like this the extra words are noise, and they're still available in the former case if needed.
In a similar way we can all see what x
is in Map(x => x + 1, []int{1, 2, 3})
. If the type didn't happen to be right there in the array, gopls will tell you, or as a developer trying to write clear code, you could resort to the current syntax.
I think most of us understand the potential value for this anyway--I just want to make it clear that this proposal helps with readability and is not just to "save a few keystrokes." Concise isn't necessarily obfuscated; in many cases it's the reverse.
Comment From: jrop
@brightpuddle I agree. Type inference increases the signal to noise ratio, in my humble opinion. Being able to elide the type annotations in lambdas seems to me to bring the code that matters to the forefront and make data pipelining and transformations more glanceable.
Comment From: Insomniak47
@tooolbox like @brightpuddle says there's already inference everywhere in golang. You (and the rest of the community) are just deciding on when inference improves readability vs when it doesn't. This type of syntax is very useful and easier to read and can reduce noise and can be more clear when done carefully.
Conciseness while being careful not to be too terse is one of the ways to make writing anything more understandable. The main argument that most people are making here isn't that it's 'too much code' but that it's code that makes the expression of common programming patterns less readable and less understandable because of the associated noise.
Comment From: aarzilli
@tooolbox like @brightpuddle says there's already inference everywhere in golang
That's not really true. Before generics we had type inference for constants and for variables declaration. With generics type inference for generic instantiation was added. That is all. Also the type inference needed to make this work is fairly complicated, take:
Map((x) => { ... }, []int{1,2,3})
For this to work the compiler needs to:
- start doing type inference on the instantiation of
Map
without typechecking the first argument - once it has partially determined the type of the first argument to
func (int) ?
start typechecking the body of the anonymous function with a partial signature - use the body of the anonymous function to determine that its return type is whatever it happens to be
- finish the type inference for the instantiation of
Map
and continue.
Partially determined types can propagate indefinitely deep, of course.
Also, consider this:
Map((x) => {
if x != 0 {
return x
} else {
return 3
}
}, []int16{0,1,2,3})
is this supposed to be valid?
Comment From: neild
I would expect the Map
case to require explicit instantiation of the types:
_ = Map[int,int]((x) => x+1, []int{1, 2, 3})
In contrast, Reduce
allows the type parameter to be inferred from the third parameter, and wouldn't require explicit instantiation:
_ = Reduce((x, y) => x+y, 0, []int{1, 2, 3})
Comment From: aarzilli
I would expect the
Map
case to require explicit instantiation of the types:
Good for you but note that almost everyone that's advocating for lightweight anonymous functions in this issue, and writes example code, writes something that needs fairly complicated type inference to work.
Comment From: Insomniak47
@aarzilli I'm not claiming that it doesn't take work, and I'm certainly not claiming that it won't be hard. How difficult it'll be is certainly something that should go into the decision whether or not to adopt it. I'm simply saying that the argument from @tooolbox around inference ignores the fact that the language already does it in places people find useful and don't introduce readability issues... and I doubt many people want to type all their vars.
I guess more specifically my point is that "more text" doesn't make something easier to read when the information is redundant in context like it can be in a ton of these cases.
Good for you but note that almost everyone that's advocating for lightweight anonymous functions in this issue
Plenty of strongly typed languages (C#, Rust) support eliding types when possible and including them when there's ambiguity. I don't see why @neild's point conflicts with that but a bit of lookahead would be nice as a QOL on his map example. All tradeoffs that need to be decided on but I don't see the conflict you're seeing.
Comment From: sammy-hughes
Wait. Does this mean...? Is this just bookkeeping, or...?
Comment From: aarzilli
@sammy-hughes see https://github.com/golang/go/issues/33892#issuecomment-2274235443
Comment From: sammy-hughes
Drat. I got all excited for nothing! Thanks for the explanation/link, @aarzilli.
Comment From: simpleKV
strong support, it will let our error check more simple,
as you can see https://github.com/golang/go/issues/69482
@neild you can pick and merge some content of #69482
Comment From: simpleKV
I'm sympathetic to the general idea, but I find the specific examples given not very convincing: The relatively small savings in terms of syntax doesn't seem worth the trouble. But perhaps there are better examples or more convincing notation.
(Perhaps with the exception of the binary operator example, but I'm not sure how common that case is in typical Go code.)
https://github.com/golang/go/issues/69482
Comment From: tmaxmax
I've read this entire thread, from the beginning to the end. Here's a proposal I've compiled based on everyone's concerns. This proposal assumes the sentiment that eliding types generally does more good than bad, and that an a bit more complicated implementation is justifiable if the end result meets the principle of least surprise and fully satisfy the desired use cases.
Syntax
What I propose, at a glance:
func { x, y -> x + y }
func { x, y ->
if x % 2 == 0 {
return x
}
return y
}
For the following reasons:
- it is significantly terser than the existing function literal syntax in all use cases, single line or multiline (otherwise it wouldn't be worth adding)
- has an easy, non-ambiguous parse, without arbitrary lookahead (unlike () => {...}
, func (...) => ...
etc.), which doesn't clash with other language features
- conversion from single-line, no-return format to multi-line doesn't imply contrived keyboard gymnastics – just add a newline and type "return"
- doesn't need to solve the ambiguity of func (a, b) ...
, where a
and b
would be types by default
- looks balanced and cohesive in parameter lists, unlike func a, b { ... }
slices.SortFunc(s, func { a, b -> a.Size < b.Size })
slices.SortFunc(s, func a, b { a.Size < b.Size })
For the initial token instead of func
, a symbol like \
could also be considered. Specifically \
is easy to type and is used in the language exclusively for escape sequences , which implies that no one feels like it would mean anything. I'll use the two interchangeably in the proposal to demonstrate the feel of both, but only one should be chosen.
Here are the rules for building these literals (later update: I've dropped the idea of distinct single-line/multiline forms in a subsequent comment, which makes the syntax easier and more cohesive; the idea of \
is also dropped):
- the symbol for separating the parameter list and body (->
) is not elidable for functions without parameters – the expression would again be ambiguous when parsing
func { fmt.Println("Hello") } // compiler error: expected `->`
- the single-line form requires either a parameter list, an expression after
->
, or both:
func { -> 1 + 2 } // works
func { _ -> } // works; has type func(Type), nice to have for no-op functions
func { -> } // compiler error: expected parameters or expression
func { x -> c <- x } // compiler error: statements illegal; use multiline form
func { square -> square.Side = 5 } // compiler error: statements illegal; use multiline form
func { -> return } // compiler error: statements illegal
func { -> _ = 1 + 2; return 42 } // compiler error: statements illegal
- the multi-line form requires at least one statement starting on the next line after
->
, and one of the statements must not be a return statement:
func { ->
} // compiler error: expected statement
func { ->
1 + 2 // compiler error: unused value
}
func { _ ->
fmt.Println("Hello")
} // works
func { _ ->
return
} // compiler error; use func { _ -> }
func { ->
return 5
} // compiler error; use func { -> 5 }
- (not a purely syntactic rule) the lightweight function is required to have either parameters, return values, or both:
func { ->
fmt.Println("Hello")
} // compiler error: lightweight function is of type func(), use normal function literal instead
func { -> cfg.N = 5 } // compiler error: same as above
These rules makes any form of lightweight func literals which represent a func()
illegal – in every case the normal function literal is better. These rules also try to eliminate multiple ways to do the same thing, so that there's no debate on which form of lightweight literal to use. The debate remains only on whether you want types or not, in the places where you don't have to specify them.
Type deduction
Syntax aside, I choose the following type deduction approach: - function parameter types are exclusively deduced from the assignment context - return type is deduced from the assignment context or, when that's not possible, from the function body - deduction of types inaccessible in the package scope of the lightweight function is a compiler error
The deduction method of parameter types results in the fact that the function must specify the exact number of parameters the assignment context allows to exist:
var x func(int, int) int
x = \{ _, _ -> 5 } // works
x = \{ -> 5 } // compiler error: expected 2 params, got 0
x = \{ _ -> 5 } // compiler error: expected 2 params, got 1
As for the return type, the more involved deduction brings some complications but I believe that's worth, as it makes the feature more intuitive and useful.
One consequence of this deduction method is that utilities such as:
iter.Map21(it, func { x, y -> strconv.Itoa(x * y) })
would work as intuitively expected, which wouldn't be possible by exclusively deducing the return type from the assignment context. When there are multiple returns, I expect that the type of the first return value is used:
iter.Map21(it, func { x, y ->
if x % 2 == 0 {
return x + y
}
return strconv.Itoa(x) // compile error: expected int, got string
}
Another consequence of return type deduction based on the body is convenient lazy values:
// with proposal – no needless repetition and noise, with the `\` symbol almost looks just like a value
getIter := \{ -> iter.Range(0, 1000) }
// without proposal – repetition and noise
getIter := func() *iter.Range[int] { return iter.Range(0, 1000) }
// observation: this obviously won't work
x := \{ _ -> 5 } // compiler error: expected 0 parameters, got 1
or conveniently creating a new scope for something that needs a new context with a specific cancellation:
// with proposal – helpful especially with return values with lengthy type names
header, body, err := \{ ->
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
return requestDocumentOCR(ctx, input)
}()
// without proposal – typenames inspired by work code
// in this situation you either repeat the type or, if using named parameters, the variable names
header, body, err := func() ([]document.TableRow, []document.TableRow, error) {
// same code as above
}
These are also use cases which should intuitively work and it would make sense and bring benefit to do so. Not having them work would seem purely artificial and be frustrating, I believe.
Technically we could just use the return body but prioritizing the assignment context prevents other surprises, for example the covariant return types problem:
var g errgroup.Group
// lightweight func type is func() error
// would be deduced as func() *concreteErrorImpl otherwise and compilation would fail
g.Run(func { -> &concreteErrorImpl{} })
It also makes usage of APIs with lazily initialized values very comfortable:
req := &http.Request{
// lightweight func type is func() (io.ReadCloser, error), would not be deducible otherwise
GetBody: \{ -> strings.NewReader(""), nil }
}
This also allows implementing the least surprising behavior for cases like:
var fn func(int)
fn = func { _ -> fmt.Println("Hello") })
Here the assignment context specifies that the lightweight func should have the func(int)
type; the expression from the lightweight func's body gives a value back but the compiler doesn't require it be used, so because of the context it is allowed to compile the lightweight func as func(int)
and not as func(int) (int, error)
. Do note that something like:
var once sync.Once
once.Do(func { -> fmt.Println("Hello") })
won't work, as the lightweight func literal is determined to be func() (int, error)
– the other variant, func()
, is illegal, as per the last rule specified at the beginning. Use func() { fmt.Println("Hello") }
.
The third rule means that the function parameters or return value can't be something you couldn't explicitly type. For example, this shouldn't compile:
package foo
type Option func(*config)
type config struct{ X int; /* ... */ }
type Foo struct{ /* ... */ }
func New(options ...Options) Foo
// --------------
package main
func main() {
_ = New(func { cfg ->
cfg.X = 5
}) // compiler error: couldn't deduce type for parameter
}
This fails in the same way writing func(_ *foo.config)
does. Point is – if you can't spell out the types, you can't use the lightweight function.
Later edit: further clarifications on type inference here, here.
Only deduced types
Explicit parameter types could theoretically be supported. Would allow to tersely write things like:
cmp := func { a, b Square -> a.Side - b.Side }
if sort.Descending {
cmp = func { a, b -> b.Side - a.Side }
}
slices.SortFunc(squares, comp)
But it's not much less compact than:
cmp := func(a, b Square) int { return a.Side - b.Side }
if sort.Descending {
cmp = func { a, b -> b.Side - a.Side }
}
// with more than 2 branches there's really no compactness advantage anymore
var cmp func(Square, Square) int
if sort.Descending {
cmp = func { a, b -> b.Side - a.Side }
} else {
cmp = func { a, b -> a.Side - b.Side }
}
It's even nicer if you use predefined function types.
There could be another usage of parameter types and that would be creating curried functions:
curried := \{ x int -> \{ y int -> \{ z int -> x + y + z } } }
But note that without parameters it's still nice to curry with this proposal:
var curried func(int) func(int) func(int) int = \{ x -> \{ y -> \{ z -> x + y + z } } }
Still miles better than the status quo and a niche anyway, so it doesn't make sense to optimize further.
Explicit return types also wouldn't be worth. Some syntax for that could be:
func { x, y: return_type -> ... } // looks too much as if y is of return_type
func { (x, y) return_type -> ... } // pretty much as verbose as normal functions unless parameter type names are very long
It's also not worth it because there is return type inference. To conclude this, adding any sort of explicit types to this feature doesn't bring any value and departs the lightweight functions from their original purpose.
Only one-liner expressions
At the beginning you have seen that in single-line usage all forms of statements are forbidden. This also includes what Go defines as simple statements: assignments, channel sends, increments and decrements, and variable declarations. On paper it would seem nice to be able to do something like:
var r ring.Ring // from container/ring
var c chan<- any // obtained from somewhere
r.Do(\{ v -> c <- v })
Odd syntax aside, especially since Go 1.23 we could have a better way to do this:
for v := range r.All() {
c <- v
}
Not only does this primitive, imperative construct named for
have no types, just like the quasi-functional one from above, it is also compatible with control flow statements and it is standard, not a library-specific loop function.
Allowing one-liner side-effect causing lightweight functions to exist would invite the community to create these inferior foreach
-like constructs. Lightweight functions should make the functional side of the language to flourish, not eat into its imperative side.
With the proposed syntax restrictions, one can only write:
r.Do(\{ v ->
c <- v
})
which screams "just add an iterator already". And I believe this is what we want: the language should guide you to the solution that's most optimal. It should push pure transformations to the functional utilities and side effects to the imperative constructs.
Final thoughts
I'd have a preference towards using \
as the initial token:
- shortens things a bit more
- would enable IDE completion specific to lightweight funcs – you type \
, press enter and the braces, parameter list and arrow are automatically inserted. This is not possible with func
, as that would autocomplete the normal function syntax
- still recognizable as a function after you find out about the feature, given that it's not used anywhere else apart from escape sequences in strings
- creating lazy values and new execution scopes looks very nice, as if they'd have their own syntax
I can see how some may have objections against \
as not looking Go-like, making call sites a symbol soup or looking a little too clever.
If you are against eliding types altogether, no form of this feature will be agreeable to you. Otherwise to me it seems that this form of lightweight functions solves all concerns and, especially when using func
as the initial token, really looks like it could be part of Go. It serves the desired use cases properly, its scope is well defined and doesn't seem to cannibalize the normal function syntax – given the restrictions, it cannot be used in all places, in many it's not worth using it and in some they seem to complement each other (see the sorting comparators example). It does push a little on the implementation complexity but to me it seems justified (the inference work this requires might also pave the way to other useful inferencing in the future).
Would be curious to hear everyone's thoughts.
Comment From: DmitriyMV
Why not something like
fn := func -> { return 42 }
fn2 := func |a| -> { return a + 42 }
fn := func -> { 42 }
fn2 := func |a| -> { a + 42 }
or even
fn := func -> 42
fn2 := func |a| -> a + 42
That way we ensure compatibility with existing func(...)
syntax and it look's Go-like.
@tmaxmax
I can see how some may have objections against \ as not looking Go-like, making call sites a symbol soup or looking a little too clever.
I don't think I like the addition of special symbols. I also don't like the fact that function parameters became the part of the expression. That is: being inside { ... }
.
Comment From: tmaxmax
Here are a few thoughts:
- You've added a special symbol: |
– even though it is at the beginning it functionally does the same thing, and that is to disambiguate between the two function syntaxes.
- You can't drop the braces because multi-value returns become syntactic ambiguities in many contexts – parameter passing, variable assignment etc. If you don't drop them, the arrow is redundant, and if you remove the arrow you get an already proposed syntax which was already mostly rejected for not being "Go-like"
- I'm not sure what you mean by "compatibility with the already existing syntax": yes, it somewhat resembles it more, but it's still a completely different syntax. There is no functional compatibility here.
- Putting the parameters inside the expression may look unconfortable, as there isn't much of this in Go. But I think this brings a series of objective advantages:
- as described in my proposal, it can be used to restrict the number of syntactic form these functions can take in a natural fashion. An issue people have is that these functions can do exactly what normal functions do, and this design elegantly solves most of that – the lightweight function is restricted to the forms which actually make sense. It's more difficult to do this when reusing already existing syntax constructs. This doesn't mean that for each new feature a radically different syntax should be employed. One should do so where it makes sense, and here it makes: this is a feature about syntax.
- by having a new block syntax you don't mess with the old blocks – your syntax, and many others, imply that the normal block be valid if it contains a single expression, which is not true now. Of course, in the implementation that's not what's going to happen – but it will still look like it. You'll have people asking themselves whether "they can do this with other blocks"; you'll have people propose that other blocks should do this, because now you have precedent. By having this very distinct form I've proposed it's made clear that this is a new thing, a different thing, an isolated thing: this is the lightweight anonymous function syntax, and nothing else is or will be like it.
- this syntactic form brings the advantage of very easily switching from single-line to multi-line, as described in the proposal. It's also very easy to add or remove parameters. It also makes it very clear when a single-line form should be used and when the multiline should, due to the syntactic restrictions made possible by its distinct and distinctive form.
- I don't really care about the \
symbol. It's nice in some cases, has an objective advantage (IDE completion), but it's not essential to the proposal in any way.
Finally, what is or isn't "Go-like" is subjective. I think this is the problem of this proposal – too many voice opinions on taste and not on function. For its success two things must happen: - a collective decision that eliding types in most scenarios the lightweight function syntax allows is a positive thing - a syntax and feature set must be devised which is functionally the best one: short, orthogonal, clear (can mean one and only one thing when read), and straightforward to use (there aren't two ways to do the same thing, you don't have to choose). Taste comes second.
Comment From: ianlancetaylor
If I'm reading https://github.com/golang/go/issues/21498#issuecomment-2445580267 correctly, it adds a syntactic meaning to a newline. I don't think we want to do that, beyond the existing purely lexical choice of whether to insert a semicolon before the newline.
Comment From: tmaxmax
It's not the newline itself that would have a meaning – a semicolon would be inserted automatically after the ->
if there is nothing else in place. I've sketched some EBNF of this based on the language grammar:
FunctionLit = "func" ( NormalFunctionLit | LightweightFunctionLit ) .
NormalFunctionLit = Signature FunctionBody .
LightFunctionLit = "{" LightFunctionExpr | LightFunctionStmt "}" .
LightFunctionExpr = ( IdentifierList "->" [ ExpressionList ] ) | ( "->" ExpressionList ) .
LightFunctionStmt = ( [ IdentifierList ] "->" EmptyStmt ";" NonReturnStatement ";" [ StatementList ] ) .
NonReturnStatement =
Declaration | LabeledStmt | SimpleStmt |
GoStmt | BreakStmt | ContinueStmt | GotoStmt |
FallthroughStmt | Block | IfStmt | SwitchStmt | SelectStmt | ForStmt |
DeferStmt .
You can see how multiline light function literals must start with an empty statement (i.e. a semicolon). This seems doable but I'm not familiar with the internals, so please confirm if what I envision is actually possible. Thank you for the input.
EDIT: If you mean that regardless of automatic semicolon insertion you don't want the newline to change the behavior, there's nothing stopping us from raising some restrictions. The syntax would still work albeit there would be some redundancies, for example:
var x int
func { -> x = 5 }()
func { ->
x = 5
}()
Both would work. I wanted to avoid such scenarios so we wouldn't encourage lightweight functions for producing side effects but the benefit of that is subjective. It would be easier not to have such restrictions in place. We could just have go fmt
take care of that, the same way if cond { x = 5 }
is rewritten on multiple lines now, even though it's a valid statement on a single line.
Comment From: griesemer
@tmaxmax Just FYI, the semicolon insertion mechanism is not expressed in EBNF, it is expressed in prose in the spec; you'd say that a semicolon is inserted automatically after a "->" at the end of a line.
Then, rather than requiring a newline (or empty statement) after "->", what you actually expect for light-weight functions that contain statements, is that the "->" is followed by a ";". This ";" indicates that what follows is a statement list rather than an expression list. This also means that something like
func { x, y ->; if x % 2 == 0 { return x }; return y }
would be permissible.
I'm not sure why you need the NonReturnStatement. Why can it not just be ";" StatementList ? Also, why the various requirements for the parameters? How about:
LightFunctionLit = "func" "{" [ IdentifierList ] [ "->" ExpressionList | ";" StatementList ] "}" .
That is, the "->" indicates an expression list, and the ";" a statement list.
This would allow (for suitably inferred types T):
func {} // = func() {}, could also disallow this case
func { -> } // invalid, no expression list
func { a, b, c } // = func(a, b, c T){}
func { a, b, c -> } // = invalid, no expression list
func { -> 1+2, 3 } // = func() (T1, T2) { return 1+2, 3 }
func { a, b -> a + b } // = func(a, b T) T { return a + b }
func { ; } // = func() {}, allowed because a StatementList may just contain an empty statement, and its ";" can be omitted before a "}"
func { ; return 1 } // = func() T { return 1 }
func { x, y ->; if x % 2 == 0 { return x }; return y } // = func(x, y T) T { ... }
Seems a bit simpler.
As an aside, I'd insist on the func
keyword. The \
is too cryptic for Go.
Comment From: tmaxmax
@griesemer We can settle on func
, that's perfectly fine.
Agreed on the semicolon. I've redundantly added an EmptyStmt
in that EBNF but it's followed by a ;
. We're on the same page here.
I agree I've complicated the syntax. I wanted to avoid having multiple ways of expressing the following (I'll use your syntax proposal for the examples:
func { ; return 1 } // already expressible as func { -> 1 }, this is why I wanted the NonReturnStatement
func {} // already expressible as func() {}
func { ; } // already expressible as func() {}
func { ; x = 5 } // already expressible as func() { x = 5 }
I wanted to enforce at the spec that all multiline forms which can be a single-line form with an expression are in fact written as expressions – these would be the funcs with a single return statement – and that any form of func()
is expressed with the old syntax. Though now that I think about it, this sort of style enforcement may lend itself better for go fmt
. Hopefully the intent here is clear – to have one single way of using the lightweight function for each corresponding use case.
Apart from that, the direction you're going with eliding the ->
in some situations is interesting. I'd probably disallow the func() {}
equivalents, and I'm not sure how I feel about func { a, b, c }
, but that's a personal matter. I see that you're choosing to have a ->
only if there is an expression afterwards – my approach was to always have it to indicate this is a special sort of function. I'm fine to go this way, too, albeit my feeling is that I'd still prefer the arrow to be present.
How do you read [ "->" ExpressionList | ";" StatementList ]
, with respect to precedence? Would it be [ ("->" ExpressionList) | (";" StatementList) ]
? If that's the case, then in your last example shouldn't there be no ->
?
And if I understand it right, the multiline format in normal Go code would look like:
func { x, y
if x % 2 == 0 {
return x
}
return y
}
right?
Comment From: DeedleFake
An alternative: Replace the ->
with the keyword return
. Among other things, it makes converting from expression-based to statement-based even more straightforward as from the programmer's point-of-view they'd be nearly identical anyways.
func { a, b return a + b }
// would be equivalent to
func { a, b
return a + b
}
My main concern is that it might not scan as well, but I'm not sure how much difference it would make. It's slightly longer, too.
Edit: This could make functions that return nothing but do call something look a little weird. For example,
// Doesn't actually return anything despite the usage of the word.
var f func(int, int) = func { a, b return doStuff(a + b) }
// Might make more sense to require a semicolon to turn it into a one-line statement body:
var f func(int, int) = func { a, b; doStuff(a + b) }
Edit 2: I'm really not sure about the length change. I know I just proposed it, but the one with ->
just really looks better to me. The usage of return
just makes it feel just a bit too wordy.
slices.Collect(xiter.Map(func { a -> 2 * a }, seq))
// vs.
slices.Collect(xiter.Map(func { a return 2 * a }, seq))
Comment From: tmaxmax
My feeling would be to alywas keep the arrow:
LightFunctionLit = "func" "{" [ IdentifierList ] "->" [ ExpressionList | ";" StatementList ] "}" .
then disallow any form which would have the type func()
, and have go fmt
format func { ->; return 5 }
to func { -> 5 }
.
If we never drop the ->
then there's no need to feel like replacing it with something else (and as you've shown, @DeedleFake, using return
there would be awkward).
Forms like func { a, b, c }
or func { x y; if ... }
(where ;
would be a newline) look especially weird to me. Having a ->
after would indicate in the first case that those are parameters to a function which does nothing.
It's subjective, though. I could live with both. It doesn't seem to me that the arrow would complicate things, and always keeping it doesn't make the syntax more verbose – for expression functions it would still be there, for statement functions 2 extra characters won't make a difference.
EDIT: The semicolon idea looks nice and seems already compatible with what @griesemer wrote. The following are equivalent:
func { a, b; doStuff(a, b) }
func { a, b
doStuff(a, b)
}
The more I think about it, the more the idea of no ->
for statement funcs grows on me. I could live with the following:
func { a, b -> doAndReturn(a, b) }
func { a, b; doStuff(a, b) }
func { a, b }
func { -> 5 }
but again, with func()
forms disallowed, and func { ; return 5 }
converted to func { -> 5 }
.
Comment From: griesemer
@tmaxmax The form "func" "{" is what tells the reader (and the compiler) that we have a lightweight function literal - there's no additional syntax (such as the "->") needed. I think the "->" is a well-understood symbol to indicate "map some arguments to the following value(s)" (rather than have it followed by a list of statements). You're right that for the ";" case, code such as
func { x
return x
}
would be permitted, which does look a bit "unusual". Maybe a token that cannot be omitted is better than ";":
func { x -> x*x }
and
func { x |
return x*x
}
[Edited: an earlier version of this comment suggested using ":" instead of "|" but that could lead to confusions with labels as pointed out by @sbinet below.]
Comment From: sbinet
wouldn't the form:
func { x :
return x*x
}
be easily confused with a label ?
Comment From: DeedleFake
Maybe surround the argument list regardless and use ->
and ;
?
// Partially copying Ruby syntax here.
func { |a, b| -> a + b }
func { |a, b|
return a + b
}
Edit: I think this was covered above, but after all the suggestions with varying usages of ->
, I think it's important that the syntax should support multi-line single-expression functions. Using ->
makes this simple by just making it disable semicolon insertion for the line similar to how commas and periods work already.
func { |a, b| ->
a + b // Imagine that this expression was long enough that putting it on the same line is infeasible.
}
Comment From: griesemer
@sbinet Good point. ":" is not a good choice here. @DeedleFake Using "|" on both sides is of course possible, but not helpful if we want to save tokens. But if we have "|", just using "->" to indicate an expression result would work. But the "|" seems unnecessary in that case.
Comment From: ty1824
Would it be possible to have the special-case here be that return
can be ommitted only when there is a single, value-producing statement/expression on the RHS of the arrow (regardless of newline - that should be irrelevant)?
I think the most consistent form with the least overhead uses ->
without the additional |
characters. It seems worthwhile to gravitate towards this.
If return
remains necessary for clarity, even in a single-line definition, that seems like a reasonable concession. The big win here is avoiding the lengthy parameter/return type definitions, which add a huge amount of character overhead and arguably detract from readability.
func { x, y -> return doSomething(x, y, 10) }
feels like a big win compared to
func(x, y <long type name here>) <long type name here> { return doSomething(x, y, 10) }
Comment From: griesemer
@ty1824 When using the "->" form, the "return" would not be required and not permitted in these latest examples. The "->" is followed by one or multiple result expressions. If there's no "->", what follows would be a sequence of statements as usual.
Comment From: tmaxmax
Honestly I think nothing for statements and ->
for expressions is fine. Not sure whether this further bikeshedding of syntax is in any way beneficial.
func { x, y
if x % 2 == 0 {
return x
}
return y
}
func { a, b -> a.Side - b.Side }
// Allowing multiline expressions is also a good suggestion, didn't think of this
func { s ->
strings.Map(func { r
if unicode.Is(r, unicode.Letter, unicode.Digit) {
return r
}
return -1
}, strings.Trim(s))
}
is all code I'd be happy to see. I feel like it has a tasteful elengace to it which isn't there with other forms.
Maybe the only thing left to discuss is what happens if one writes the parameters on multiple lines. Would this code be possible:
func {
x,
y
fmt.Println(x, y)
}
Should it be possible? I'd personally vote against it, the parameter names should be painfully long for this need to exist.
Comment From: ty1824
@griesemer
Sorry, as long as everyone is in agreement and this is parsable in all cases without a separating character (->
), I'm on board :) Some of the corner case interactions with other syntax features mentioned in the other parts of this thread caught me off guard, the ->
seemed like a useful token to prevent any possible such interaction.
@tmaxmax
I'll be the first person to support type ommission, but I'll also be the first to say it probably needs to be optional. There are situations where it may be necessary for a subset of parameters to state their type (in case of inference problems or clarity) (e.g. func { a, b int, c, d -> ... }
.
In that case, multiline parameters may be absolutely necessary.
Isn't the idea that newlines shouldn't affect parsing of the construct anyways?
Edit:
Now that I think of it, a separating character would be necessary in the case of explicit types.
func { a, b foo ... }
is ambiguous - is foo
here the type of `b or an expression referencing a variable?
func { a, b foo -> ... }
is non-ambiguous, foo
is a type.
Comment From: tmaxmax
@ty1824
(in case of inference problems or clarity)
Where would you imagine there would be inference problems?
Isn't the idea that newlines shouldn't affect parsing of the construct anyways?
That's true. I guess we could allow optionally wrapping the parameter list in parantheses, or even enforce it for multiline parameter lists. Not sure how would you format it, though:
func { (
someParam,
someOtherParam,
)
// code
}
func { (
someParam,
someOtherParam,
)
// code
}
func {
(
someParam,
someOtherParam,
)
// code
}
// etc.
is ambiguous - is foo here the type of `b or an expression referencing a variable?
That example is not possible. If you are to write it on a single line it would be func { a, b; foo ... }
because of automatic semicolon insertion. In the formal grammar there wouldn't be just whitespace after the parameter list – there is a semicolon. For the computer parsing would not an issue even without introducing any new tokens, even for multiline parameter lists. The only issue is for the programmer. This will look weird:
func {
someParam,
someOtherParam
fmt.Println(someParam, someOtherParam)
}
but I believe it's very much parsable by the computer.
Comment From: griesemer
More complicated cases that require types or many lines are not lightweight - for that we have an existing and fine function literal syntax. Let's not get bogged down by the complicated cases but make the lightweight cases readable and compact.
Comment From: tmaxmax
I agree. If multiline parameter lists can be naturally forbidden through the language spec then all is good – but is this possible?
Otherwise I feel that we've achieved that: the form with ->
for expressions and nothing for statements is both readable and compact.
Comment From: jimmyfrasche
I preferred the javascript style params => exprOrBlock
syntax, less clunky—but as long as you can have an expression or a regular code block I'm happy.
Comment From: ty1824
@tmaxmax I want to point out that I've been eagerly following since your initial proposal a few days ago because it was well thought and addressed nearly all of the practical concerns others have mentioned, rather than being stylistic. I'm a big fan of it - it's syntactically concise, requires few additional concepts, and those that it does require (unique bracket positioning, arrow) are fairly standard in the general programming language landscape.
I've been concerned by some of the distracting changes to it, namely adding additional syntax variations and arbitrary keywords/tokens (though it seems like the latter, like |
have safely been put to rest)
Where would you imagine there would be inference problems?
This is a reasonably common problem in my experience - I'm coming from a Kotlin background, which makes liberal use of type omission. It's not frequent, but there are many days where I'll have an anonymous function where I have to explicitly specify at least one parameter type because it is ambiguous. I've also built several languages for my workplace (also with type deduction and/or inference) where this has been an issue.
And the general consensus from users is that they prefer to solve the problem with the smallest footprint possible (add an explicit type for the parameter in question) rather than reverting to the overall more complex syntax.
@griesemer
Is it not a worry that having two separate forms of the lightweight function will contribute to overall syntax bloat and confusion? One key point of @tmaxmax 's original proposal was that changing between the two forms was as simple as adding a new line and adding a return keyword. Now the user will have to know/remember to add/remove ->
, which is decidedly worse UX.
It feels like there would need to be a significant benefit to the proposed distinction - is there? Why could these forms not all coexist side-by-side: Single-line expression
func { a, b -> a + b }
Multi-line expression
func { a, b ->
a + b
}
Multi-line statement w/ no return
func { a, b ->
doSomething(a, b)
doSomethingElse(b, a)
}
Multi-line statement w/ return
func { a, b ->
doSomething(a, b)
doSomethingElse(b, a)
return a + b
}
My takeaway is that the original form is syntactically consistent (+UX), easily parsed and can support both single-expression and statement forms. And my personal input is that it would make it easy to add explicit types were they necessary (which, in my experience with constructs like this, is always going to be true at some point).
Comment From: DeedleFake
This is a reasonably common problem in my experience - I'm coming from a Kotlin background, which makes liberal use of type omission. It's not frequent, but there are many days where I'll have an anonymous function where I have to explicitly specify at least one parameter type because it is ambiguous. I've also built several languages for my workplace (also with type deduction and/or inference) where this has been an issue.
This proposal has generally operated under the assumption that this syntax sugar will only be allowed in cases where the expression that creates the anonymous function is immediately being assigned to a variable of function type, including usage as an argument to a function. In other words,
// Given the following:
func example(func(int, int) int)
example(func { a, b -> a + b }) // Legal.
var f func(int, int) int
f = func { a, b -> a + b } // Legal and simplifies recursion a bit.
f := http.HandlerFunc(func { rw, req; rw.Write([]byte("data") }) // Legal?
func middleware(h http.Handler) http.HandlerFunc {
return func { rw, req
// Do stuff.
h.ServeHTTP(rw, req)
} // Legal.
}
var f any = func { a, b -> a + b } // Not legal.
Are there other situations besides that last one where it might not work? Elsewhere in this thread, the proposal has been not to infer types, but rather to just copy them from the usage of the expression.
Comment From: ty1824
@DeedleFake
Thanks for pointing this out - I take generics for granted as I make liberal use of them (either by defining them or consuming generic libraries) but didn't realize that was more of my focus here.
Type arguments for generic calls can be omitted and derived from the context. In the case where an anonymous function is used, there would be two choices:
- Always explicitly declare type arguments for calls to generic functions using anonymous function parameters
- Define parameter types where necessary to ensure generic inference works
Having both choices is good, as in some cases one or the other can become extremely tedious.
Elsewhere in this thread, the proposal has been not to infer types, but rather to just copy them from the usage of the expression.
One implication of this is that there's a likelihood that anonymous functions would not work with inferred generics. If that's a limitation imposed upon the first version I think it's a reasonable tradeoff, but it will almost certainly become a big headache over time. Designing the syntax in a way that leaves it open to supporting better solutions in the future (i.e. leaving "syntactic room" for explicit argument types) seems like a pragmatic choice. The problem doesn't need to be solved now, but we don't preclude it from being solved later.
Comment From: tmaxmax
@ty1824
Thank you for your words. I was happy to go with the one syntactic variation where ->
would be dropped because the sacrifice didn't seem too big, I liked the indication of expression vs statement, and hoped for a quicker convergence. Now that you bring the UX point up, if I think a little more about it, it is indeed not confortable to have to add/remove the ->
if the complexity of the function body changes.
There might still be a readability advantage at clearly differentiating the two. Here's such a case:
func { a, b ->
fmt.Println(a, b)
} // func() or func() (int, error)? Depends on context
func { a, b
fmt.Println(a, b)
} // clearly func()
Though the dependence on context would exist regardless for the ->
form, unless code like the func { rw, req; rw.Write(...) }
is expected – then you can write single-line statements and you don't need to contextually determine the right type of the expression function (also @DeedleFake yes, that would be legal, there's nothing in the language to make it not be, only maybe go fmt
could always rewrite it to be multiline and remove the semicolon).
We should remember that technically you can create new lightweight functions with no parameters, because the return type is also inferred from the function body. Here's where the no ->
form may have an advantage:
x := func { ; fmt.Println("Hello") } // func()
x := func { -> fmt.Println("Hello") } // func() (int, error)
You can intentionally decide the function type. But:
1. The syntax is ugly and unintuitive (you have to know the peculiar stuff surrounding semicolons in Go) – people would probably always end up writing the ->
form
2. I'd want to forbid any forms of func()
because the normal literal is just better
So I'd argue that this advantage is gone.
An advantage of always having the ->
is solving the multiline param list conundrum:
func {
someParam,
someOtherParam ->
fmt.Println(...)
}
Now it's clear where the parameter list ends. While I don't expect nor wish to see this sort of lightweight functions, again I don't think we have means to make that illegal through the language.
Given that we want the multiline expression form, if we always have the arrow then this means that for every lightweight function of the form:
func { params ->
ExpressionStmt
}
the return type of the lightweight function would be the type of that expression, unless the context it's used in requires a void return and the value that expression is evaluated to can be ignored (e.g.. function calls would work, arithmetic wouldn't). This seems acceptable to me.
To reiterate, I don't think always having the ->
would cause confusion. As a refresh, this was the argument made by @griesemer against ->
for the statement form:
The form "func" "{" is what tells the reader (and the compiler) that we have a lightweight function literal - there's no additional syntax (such as the "->") needed. I think the "->" is a well-understood symbol to indicate "map some arguments to the following value(s)" (rather than have it followed by a list of statements).
It is technically true that for the computer the ->
is not needed, and that the programmer can also differentiate between the two. I do have a feeling though that ->
would help the programmer even more – the {
is a small symbol and func
has other uses already. I liked the func { ... ->
signature of this lightweight form. I also haven't really ever tought of ->
(or similar arrow symbols, for the matter) as mainly a "map this to that" symbol – for me it more of a "take these and do this with them", which is more general and allows statements to exist afterwards without issue.
But this is all very subjective and personal, so strongly arguing in favour of one or the other isn't bringing much value. It doesn't seem though as if the form without an arrow has an upper hand in objective terms – functionally it seems somewhat inferior, in the end.
To conclude this part, I'd probably stick to my initial proposal, with a multiline expression form in addition, unless there is very strong and majoritar preference for a statement form without it.
When it comes to type deduction, I think annonymous functions would work with inferred generics. I'd expect that the implementation would use every data point it has available to deduce all possible generic types and then apply them to the lightweight function. The lightweight function can contribute to inference with the return type, where possible and necessary. The only time lightweight functions would not work is when their parameter types can't be inferred:
func example[T, U any](f func(int, T) U)
example(func { n, v -> 5 }) // compiler error: can't infer T
example(func { n, v -> v }) // compiler error: can't infer T; it would be this error and not `can't infer U` first because if T is inferred then the return value of the lightweight func is and U is inferred as a consequence
example[uint](func { n, v -> 5 }) // works; T = uint, U = int
example[uint](func { n, v -> v }) // works; T = U = uint
This is how I'd expect it to work and how I'd find this feature to be useful and fulfill its purpose in the majority of cases. It's true that specifying parameter types would make it even nicer:
example(func { n, v uint -> v }) // T = U = uint
but I'm not sure that this occurs often enough to warrant the additional complexity. Allowing parameter types would also allow you to:
f := func { x int, s string -> fmt.Println(x, s) }
g := func(x int, s string) (int, error) { return fmt.Println(x, s) }
and at this point we're just remaking function literals – with parameter types the lightweight form would be just as strong as the other, minus some return type inconveniences.
Lightweight function literals serve the use case of making generic abstractions easier to use. Making them capable to replace normal functions should not be in our scope.
Later thought but not a proposal: It would have been cool though to have had since the beginning these "fluid" function literals. Imagine something like:
func { x, y -> x + y } // purely inferred form
func { x int, y -> x + y } // partially inferred
func { (x int, y int) int -> x + y } // no inference
and instead of distinct function declaration syntax:
const Example[T fmt.Stringer] = func { x T, y string -> x.String() + y }
But at this point, just maybe write OCaml or something.
Comment From: DeedleFake
@ty1824
I wrote the entire comment below and right as I was about to comment it ~~yours got updated with examples~~, so most of this is now redundant, but, regardless, here it is for that which isn't. Edit: Oh. Nope, it wasn't an update. It was a whole nother comment from someone else. Well then. Never mind.
One implication of this is that there's a likelihood that anonymous functions would not work with inferred generics.
Such as in the following?
func Example[T any](f func(T, int) bool) T
Example(func(string, int) bool { return true }) // Currently legal. Needs extra arguments and return manually typed.
Example(func { a, b -> true }) // Can't infer T. Not legal.
Example[int](func { a, b -> true }) // T explicit, so legal.
I think that's completely fine, though. If generics ever get reverse inference, i.e. something like var f int = Example(...)
becoming legal, then it can extend to the second line of the above for some situations at least, but even without it I think that it's worth it to skip typing that which doesn't need to be in the function literal.
Comment From: griesemer
We clearly need to distinguish between short (lighweight) function literals that just return expression(s), and ones that contain statements. For instance, the compiler needs to know if calling fmt.Println() is just a statement, or whether we care about its return values.
We can solve this by always requiring a return statement:
func { s -> return fmt.Println(s) } // prints s and produces (int, error)
vs
func { s -> fmt.Println(s) } // prints s but produces no result
The -> seems very misleading in the 2nd case. Requiring a ";" after the "->" to indicate a statement list makes one-liners look odd and may hide bugs, it is fragile:
func { s -> ; fmt.Println(s) } // prints s but produces no result
Writing them on two lines instead of one (and rely on automatic semicolon insertion) is confusing:
func { s -> // semicolon inserted here automatically
fmt.Println(s) // prints s but produces no result
}
This leads to a situation where white space changes the meaning of (valid) code, something we have been careful to avoid in Go.
So we can decide to not use the "->" in all cases:
func { s; return fmt.Println(s) } // prints s and produces (int, error)
vs
func { s; fmt.Println(s) } // prints s but produces no result
or
func { s | return fmt.Println(s) }
vs
func { s | fmt.Println(s) }
Using a ";" to separate the incoming parameters from the rest might work but it's very fragile. For one, how does the compiler know that we have a parameter? In this example:
func { s ; fmt.Println(s) }
s might be a global variable, and a stand-alone s would be a statement expression. Not valid because Go doesn't permit statement expressions where the result is not used, but at the very least it's not clear if we made a coding mistake (maybe we wanted s := s) or we meant a parameter. It's also not something that the parser will be able to definitively decide.
In summary, if we really care about short function literals, I think the case where arguments are used in a simple expression(s) should be short because it's common. Otherwise, why bother. That means we should avoid the need for a return statement. Using the "->" seems perfect for this case because it's established in other languages and the meaning is intuitive.
If we want to support short function literals that have statements, we should make a very clear distinction to avoid confusion, and to make it easy to report good errors. That means we need a clear separator between parameters and statements. The ";" is not ideal for the reasons mentioned above. The "|" is a traditional token for this situation, it is used in Ruby, which took the idea from Smalltalk. There's also no need to bracket the parameters between "|"'s, a separation from the rest of the statements is good enough.
With this, the examples above become:
func { s -> fmt.Println(s) } // prints s and produces (int, error)
or
func { s | return fmt.Println(s) } // prints s and produces (int, error)
vs
func { s | fmt.Println(s) } // prints s but produces no result
With this the "->" becomes a shortcut for the two tokens "|" "return". We can decide that we don't want that, which would be fine, too. That would make the syntax even more regular at the cost of longer short functions. Then we just keep the "|" to separate the parameter list from the rest of the body.
These notations have the advantage that they are short, clearly different when we have different meaning, and they are not white space sensitive in the sense that we can write either literal on one or multiple lines as we please w/o changing their meaning.
Here's an updated syntax:
FunctionLit = "func" Signature FunctionBody | ShortFunction .
ShortFunction = "{" [ IdentifierList ] "->" [ ExpressionList ] | [ "|" StatementList ] "}".
Summary:
There's of course a gazillion variations that we could explore, but if we accept the idea that it's ok to put incoming parameters inside the "{" "}" block, pretty much the most simple approach is to list them at the beginning, and have a separator of sorts ("|") between them and the rest of the statements.
If we care about not wanting to write "return" in simple cases, it makes sense to use another separator to indicate that special case, and "->" seems about as good as it gets.
Comment From: jimmyfrasche
Is it possible to split the difference between this and the the JS arrow syntax? Like:
func s -> fmt.Println(s)
func s -> { return fmt.Println(s) }
func s -> { fmt.Println(s) }
Where the first two return (int, error)
and the last returns nothing. That has the nice property that the left of the arrow is always the same and the right side only differs based on its needs.
Comment From: jcsahnwaldt
@jimmyfrasche
Is it possible to split the difference between this and the the JS arrow syntax? Like:
Good point. That's very close to how Java and C# distinguish between "expression lambdas" and "statement lambdas".
EDIT: But if I recall correctly from the discussion above, having the arguments outside the curly braces leads to problems with other parts of Go syntax. :-(
Comment From: ty1824
@griesemer
We clearly need to distinguish between short (lighweight) function literals that just return expression(s), and ones that contain statements. For instance, the compiler needs to know if calling fmt.Println() is just a statement, or whether we care about its return values.
The rest of your post follows assuming this is true (and I agree with you if it is), but this is my sticking point - does this need to be distinguished via syntax? Given that the usage of these lightweight functions is going to be contextual, wouldn't the context be able to determine whether to care about the local function's return type or to ignore it? We're already using them to determine parameter types.
The only examples I can think of where this would cause ambiguity have to do with either: * overloading, which is not relevant for Go, I believe * calling a function w/ a function type parameter using inferred generics, which can be solved by explicitly assigning type arguments to resolve the ambiguity.
If the context is expecting a return, then the return value matters. Otherwise, the return value is irrelevant.
Comment From: ty1824
Regardless of the conclusion of some of these details, I'm very happy to see some constructive progress towards a concrete solution - in general this approach feels like the right take.
As someone who spends most of their time writing libraries/tools for developer experience, I'd be very excited to use this for some!
Comment From: tmaxmax
@griesemer
For instance, the compiler needs to know if calling fmt.Println() is just a statement, or whether we care about its return values.
You can know based on context.
var x func(int) = func { v -> fmt.Println(v) } // works
var x func(int) (int, error) = func { v -> fmt.Println(v) } // works
var x func(int) (int, error) = func { v ->
fmt.Println(v)
} // works
var x func(int) = func { v ->
fmt.Println(v)
} // works
x := func { -> fmt.Println("Hello") } // func() (int, error)
x := func { ->
fmt.Println("Hello")
} // func() (int, error); If you need a func() here, use the normal function literal.
x := func { ->
fmt.Println("Hello")
fmt.Println("World")
} // would work and be a func(); I'd like it to be a compiler error – it is a func(), use the normal syntax
If instead of a function call you'd have an arithmetic expression, like 1 + 2
, it wouldn't work in the func(int)
cases, the same way you can't just write that expression standalone and must assign it to something. The same way today you have expressions of which results you don't have to assign to something, the same way you'd have lightweight function literals which will or will not return their expression's results depending on the context.
So yes, the compiler needs to know, but doesn't need syntax for that. We purposefully design lightweight functions to be able to use them in contexts where everything about them can be deduced. Let's use that.
The -> seems very misleading in the 2nd case.
This is subjective. I've explained in a comment above what the ->
means to me – I don't find it to be misleading, i.e. to always have to imply a return value. That's your feeling, that's your opinion. Let's not drive the entire discussion around it.
Also no one who wrote Kotlin seems to have had this issue. There is an entire community writing lambdas with multiple statements starting with ->
without issue.
Requiring a ";" after the "->" to indicate a statement list makes one-liners look odd and may hide bugs, it is fragile:
No one will ever write that and no one will need to write that if we deduce the return type based on context.
Using a ";" to separate the incoming parameters from the rest might work but it's very fragile. For one, how does the compiler know that we have a parameter?
This doesn't make sense. With your initially proposed syntax:
LightFunctionLit = "func" "{" [ IdentifierList ] [ "->" ExpressionList | ";" StatementList ] "}" .
you start with an optional identifier list for the parameters, a semicolon comes and then the body statements begin. If there is anything before the first semicolon – be it automatically inserted or not – per the syntactic definition of the function literal it must be a valid identifier and per the semantic definition it must be a parameter of the function. There is no ambiguity there: if there are no parameters, the function begins with an empty statement; if there are parameters, the function begins with the parameter list.
But you can avoid all these issues if:
1. you drop your personal preference for what ->
means and just use it everywhere
2. you add another identifier
Here's the syntax for option 1:
LightFunctionLit = "func" "{" [ IdentifierList ] "->" [ ExpressionList | StatementList ] "}" .
Here are the semantics of the function type:
- if what comes is an ExpressionStmt
the function returns the value of that expression or nothing, depending on context
- if what comes is an expression that's not a legal ExpressionStmt
, the value of that expression is always returned, regardless of context
- if what comes is one statement not an ExpressionStmt
or more statements of any kind the function doesn't return anything unless it has a ReturnStmt
somewhere
No whitespace sensitivity, very easy refactoring, works as expected in all scenarios. No need to have to switch between ->
or |
or nothing to change the return value. You don't have to think about it – the compiler will choose right for you. And, unlike whitespace sensitivity, the contextual choice of ignoring an expression result is nothing new.
To address @jimmyfrasche and others' suggestion, what having the braces surround the parameter list allows us is to create a unified syntax for both statement and expression forms. To not have such a split. With the design proposed in this comment, if you have this function:
func { a, b -> strings.Compare(a.Name, b.Name) }
you can go to this:
func { a, b ->
firstA, lastA := splitName(a.Name)
firstB, lastB := splitName(b.Name)
return cmp.Or(
strings.Compare(lastA, lastB),
strings.Compare(firstA, firstB),
)
}
only by modifying what's coming after the arrow. In your proposed design (which suffers from a series of other problems flagged in previous comments and my initial proposal) you have to add braces. Two distinct forms. In the design with distinct symbols for statement and expression forms you have to change the symbol. Why do any of that when you could just... not do it?
I see no objective weaknesses of this design, especially in relation to the other.
P.S. I'll link for reference my original proposal, so context is not lost. Ignore the \
discussion and use the syntax rules defined in this comment.
Comment From: jonathansharman
@tmaxmax
go x := func { -> fmt.Println("Hello") } // func() (int, error); If you need a func() here, use the normal function literal.
The func()
interpretation could also be achieved using the short syntax and an empty return
statement:
x := func { ->
fmt.Println("Hello")
return
} // func()
In this example, that's longer than the "long" syntax, but it's nice to have an escape hatch for using the short syntax when the function (1) returns nothing, (2) can't benefit from return type inference, and (3) has a lengthy or complex parameter list.
Comment From: earthboundkid
Is it possible to split the difference between this and the the JS arrow syntax? Like:
go func s -> fmt.Println(s) func s -> { return fmt.Println(s) } func s -> { fmt.Println(s) }
A problem in JS is that you start out with a single return expression and then you find you need multiple statements, and that requires you to go back and wrap everything in brackets. It would be better if brackets were always required.
Comment From: jimmyfrasche
Sure, I've had that happen to me more than once. It's not a problem. It's so little effort. It's also entirely mechanical so you could probably get an editor to do it for you when you press a button. You'd have to be doing that a whole lot if having such a button saved much time. It also doesn't happen that often. Most of the time you guess the correct way and it stays like that.
The readability benefits trump any rounding-error efficiencies.
Comment From: earthboundkid
In the case of JS, I would say the readability benefit was negative because f = () => {}
returns nothing so to return an object (a frequent need), you need to write f = () => ({})
. f = () => { a }
is a syntax error (hopefully) and f = () => ({ a })
is an object with a key named a
with the value of the a
variable. Anyway, Go won't have the same problems as JS, but I feel like JS had a pretty significant missed opportunity with the syntax of their arrow functions. Plus the whole business about this
which is fine once you learn the rules from book or something but definitely not something you can learn by osmosis.
For Go, I think the takeaway is to avoid rules that are overly subtle. It's better to have something obvious that always works.
Comment From: infogulch
Not really feeling these fancy compact syntaxes; doesn't look like Go in my subjective opinion. I'd prefer a way to explicitly omit the types the same way you can explicitly omit variable names with _
, like:
func(a _, b _) _ { return a + b }
This would provide the benefits of type inference, with no new syntax and just reusing/expanding _
to apply in the type position, maybe only for anon funcs.
Comment From: avamsi
For Go, I think the takeaway is to avoid rules that are overly subtle. It's better to have something obvious that always works.
func s -> fmt.Println(s)
func s -> { return fmt.Println(s) }
func s -> { fmt.Println(s) }
@jimmyfrasche's proposal reads the most obvious to me (perhaps there's some room for confusion with the first expression if the return type is a chan? -- but that problem exists with some of the other proposals as well).
I'd like to have some lightweight syntax (compared to normal funcs, including eliding types with _) but not a huge fan of putting params in the "body" and return type depending on the context (instead of just the params).
Comment From: jimmyfrasche
@earthboundkid
In the case of JS, I would say the readability benefit was negative because f = () => {} returns nothing so to return an object (a frequent need), you need to write f = () => ({}). f = () => { a } is a syntax error (hopefully) and f = () => ({ a }) is an object with a key named a with the value of the a variable. Anyway, Go won't have the same problems as JS, but I feel like JS had a pretty significant missed opportunity with the syntax of their arrow functions. Plus the whole business about this which is fine once you learn the rules from book or something but definitely not something you can learn by osmosis.
For Go, I think the takeaway is to avoid rules that are overly subtle. It's better to have something obvious that always works.
This and #12854 would create the same problem. I don't find the ()
a big deal breaker. There is room for improvement though.
The ambiguity could be avoided by changing the syntax to:
func s -> fmt.Println(s)
func s { return fmt.Println(s) }
func s { fmt.Println(s) }
That way, after the argument list, if there's an ->
it must be an expression and if there's a {
it must be a block so the {}
in func -> {}
would be a composite literal unambiguously, for either the reader or the compiler, even with both proposals. Even if only this proposal is accepted it still makes it clearer so it seems an unalloyed good.
Comment From: aarzilli
@tmaxmax
Syntax aside, I choose the following type deduction approach: - function parameter types are exclusively deduced from the assignment context - return type is deduced from the assignment context or, when that's not possible, from the function body
I think this is a bad idea: consider what happens when multiple lightweight functions are passed to a generic function.
For example:
type Iface interface { Method() }
type S struct {}
func (*S) Method()
func example[T any](func() T, func() T) T
example(func { -> &S{} }, func { -> Iface(nil) })
If the first argument is typechecked first the whole expression is an error, if the second argument goes first it's valid and the return type is Iface. Having validity depend on the order of arguments is something that the current type unification algorithm explicitly avoids and this would reintroduce it.
The only way out of this, that I can see, is to have all return statements participate in a new kind of type unification, based on assignability, but that has its own problems:
type Iface2 interface { Method(); Method2() }
example(func { -> Iface(nil) }, func { -> Iface2(nil) })
the only valid solution here is T = Iface
but the compiler would have to try both to decide (as many solutions as there are return statements) which to me sounds combinatorial and therefore slow. Also a virtue of Go is that its error messages are terse and easy to understand and complex type inference, in my experience, leads to neither of those properties.
I also except to adding a special syntax for lightweight functions evaluating a single expression (i.e. the non-multiline version of your syntax): I do not think the added value carries its syntactic weight. Expressions in Go are deliberately underpowered, with just an expression you can't:
- do anything to multiple returns except forward them
- error-check in any way
- do any kind of branching
Go is a language where you write many simple statements instead of deeply nested expression, this was by design, if you dig deep enough you will find the ternary operator flame wars from 15 years ago.
Comment From: DeedleFake
@aarzilli
I think it would be fine, especially to start, to just disallow that case completely. Make it a compile-time error. I don't think it'll come up that often, anyways, and when it does you can just manually specify the generic function's type, i.e. example[int](func { -> 3}, func { v -> v + 1 })
or something.
Comment From: aarzilli
Disallow what? Like disallow inference from the return argument entirely? If so, I agree but, as I've pointed out elsewhere, most people that bother to provide example use cases do it for things that need that type inference. If you mean this particular example, how would you disallow it? What would be the error?
Comment From: DeedleFake
Actually, rereading your example, I'm not sure that it's not already covered by those rules. How is it not functionally identical to https://go.dev/play/p/ZFfIleyS703, which is already handled just fine by the compiler?
Comment From: tmaxmax
@aarzilli Thank you for your input.
I'll address your points in the order in which I find it the easiest.
First, the multiline/single-line difference first: it is something I've removed in a subsequent comment. I've proposed a context-based return deduction for which I haven't found surprising or unintuitive edge cases yet. It has received exclusively feeling-based criticism.
Second,
Go is a language where you write many simple statements instead of deeply nested expression
I am aware of that and appreciate this quality of Go, and the ternary operator discussions are known to me. This proposal, regardless of distinct single-line/multiline forms, doesn't expand the power of expressions in any way, it simply makes a return
implicit in the places where it is redundant. You still can't do nothing from what you've enumerated, for example. I don't think my proposal goes against this principle, especially with the removed special rule, which I agree was a misstep.
Third and last, the issue mentioned with template arguments and return value deduction is a valid criticism, and, as you've also described, there aren't any good solutions to support it. One solution would be to just deduce return types based on context, but that's also not ideal:
xiter.Map(func { x -> strconv.Atoi(x * x) }, s)
won't work without explicit types. The following:
v, err := func { ->
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
return longRunningAction(ctx, ...)
}()
won't work at all. Removing this return deduction would make this syntax not fulfill one of its main purposes. Further, if I am to analyze your example a little further:
func example[T any](a, b func() T)
example(func { -> 5 }, func { -> any(5) })
(I've reduced it a little to something simpler but equivalent), at a first sight it looks very niche – you need multiple function parameters with return types dependent on each other. This would surely occur rarer than the situation presented by xiter.Map
-like helpers, so I believe that removing this capability to favour a niche case – if there is no better solution which satisfies both – is misguided. The solution in code to the issue is also not necessarily unfamiliar, even though it has a more convoluted source; it's the same situation as:
func example(s []any)
example([]int{})
Fixing the functions code is trivial – just convert the first return to the interface value. It might be a challenge, as you describe, to give an intuitive error message. The simplest one would be:
example(
func { -> 5 },
func { -> any(5) }, // compiler error: expected int, got any
)
and is basically what happens now as @DeedleFake pointed out. The difference between the cases lies in programmer expectation – because the types are not specified here, you kind of expect the compiler to make this work. Maybe we could reach this sort of error, though:
example(
func { -> 5 },
func { -> any(5) },
) // compiler error: can't infer T – lightweight function return values conflict (int vs any)
Why? Because only when exclusively lightweight functions are used to deduce T this error would occur AND would be confusing. All the following would work as expected:
func example[T any](a, b func() T)
example(
func { -> 5 }, // compiler error: expected any, got int
func() any { return 5 },
)
func example2[T any](a func() T, b T)
example(
func { -> 5 }, // compiler error: expected any, got int
any(5),
)
These would work like that because that's how lightweight function return type inference is defined:
return type is deduced from the assignment context or, when that's not possible, from the function body
To fulfill this definition, I'd expect that lightweight functions are always the last to be evaluated when type checking. Otherwise, xiter.Map
will also fail:
xiter.Map(
func { x -> x + 1 }, // compiler error: can't infer type of x
numbers,
)
which would again defeat the whole point.
Coming back to the above examples, in all of them it is possible to deduce the types from the context. Using the return value type is a last resort, when any other deduction from the context fails.
With the behavior described above the parameter order doesn't matter – when the type can be deduced, it is deduced, and when there are only lightweight functions the error message does not favour any one of them.
To conclude this, I'm not a fan of condoning the "least painful" solutions. I personally don't see a solution without any pain here – either you don't have lightweight functions at all, either they're useless in important scenarios, or they're inconvenient in this niche case. If we agree to have lightweight functions at all, I'd go with the last option.
Comment From: tmaxmax
To kind of summarize the discussions above, here are the syntaxes discussed: - the arrow syntax from JS (discussed a long time ago but not really ever abandoned, if I'm not mistaken)
(a) => expr
(a, b) => (expr, list)
() => { stmts }
- old
func
literal but with_
placeholders for types:
func() _ { return expr }
func(a, b _) _ { return expr, list }
func(a _) { stmts }
- @jimmyfrasche (comment)
func a -> expr
func a, b -> { return expr, list }
func { stmts }
- @griesemer (comment)
func { a -> expr/expr, list }
func { a, b | stmts }
- @tmaxmax (comment)
func { a, b -> expr/expr, list/stmts }
Here are the flagged disadvantages (subjective or objective, mine or of others):
- arrow syntax:
1. three distinct forms
2. doesn't start with func
1. arbitrary lookup in parser (though it was already prototyped in the parser, so it's technically possible)
1. won't work with #12854 without further disambiguisation
- just placeholders:
1. not short enough
1. is it parseable the way I wrote it or would it need distinct placeholders for every parameter and return type?
- @jimmyfrasche:
1. weird in parameter lists (for examples see above link, a variation of this was already prototyped)
1. two distinct forms
- @griesemer:
1. two distinct forms
1. ugly |
(initially proposed without this separator, to me the explanation on why that wouldn't work is unclear)
- @tmaxmax:
1. involved return type deduction rules required to keep a single form
1. misleading ->
(comment)
1. won't work with #12854 without further disambiguisation
Here are the promoted advantages:
- arrow syntax:
1. shortest
2. unambiguous return type for reader
3. cohesive in parameter lists
- just placeholder
1. most familiar
2. doesn't favor expressions
3. ability to specify some parameter/return types?
- @jimmyfrasche
1. partially resembles original func
literal syntax
2. unambiguous return type for reader
- @griesemer
1. unambiguous return type for reader
2. cohesive in parameter lists
- @tmaxmax
1. single form => easy refactoring, return type void
or value as needed
2. cohesive in parameter lists
Regardless of syntax, the same issues pertaining to semantics (i.e. type inference) must be addressed.
Comment From: tmaxmax
When it comes to the disadvantages of my proposal...
The return type ambiguity comes exclusively when the choice between func(...)
and func(...) return_type
. this occurs when a func(...) return_type
is attempted to be assigned in a func(...)
context. Here's how you choose:
- multiple statements in body? means it has a return
, so compiler error
- single ExpressionStmt
in body? works
- single expression which can't be a statement? compiler error
In other words, if you can write the body of the function as a statement, then that function works as a function without a return value. If this is confusing, then:
func main() {
fmt.Println("Hello world!")
}
is also confusing. In the same way you don't have to always write _, _ = fmt.Println("Hello world!")
it seems logical to me to not have to switch up lightweight function forms.
To add to all that: lightweight functions of type func(...)
will be used in imperative contexts. It will be quite a rare occurence to have a single statement, and it would still work regardless.
Anyway, when it comes to #12854 (ignoring that proposals might not make it in the end, given that this is also a proposal), the issue of disambiguisation will emerge when using it in a context where the return value is available. In xiter.Map
, for example, you'll still type the type. I think the question here is what happens more often:
- converting from expression form to statement form
- returning structs in inferrable contexts
I personally believe it's more annoying to do the first than to add/remove a pair of brackets (removal would be automatic with go fmt
probably).
About the ->
I've already discussed here.
Comment From: jimmyfrasche
@tmaxmax I never said func a, b -> (expr, list)
and I think if you have multiple expressions you need to use a block. I did include multiple returns from a single expression (the call to fmt.Println
) but that's a separate thing. If you need to return multiple things I have no problem with saying you have to use a block.
Comment From: tmaxmax
My apologies, I used this comment from 2020 as a reference. Updated the summary.
Comment From: aarzilli
@DeedleFake
Actually, rereading your example, I'm not sure that it's not already covered by those rules. How is it not functionally identical to https://go.dev/play/p/ZFfIleyS703, which is already handled just fine by the compiler?
I'm not sure what you are saying here. For example what are "those rules"? Nobody has exactly specified how type unification and return type inference interact anywhere. Are you saying that the return type of a lightweight function should be determined solely by its contents, disregarding its context? The problem still persists, you are going to have lightweight functions that are either correct or errors depending on the context where they appear:
// This is fine because both &S{} and Iface(nil) are assignable to Iface
var f func() Iface
f = func { ->
if blah {
return &S{}
}
return Iface(nil)
}
// This is an error because &S{} and Iface(nil) can not be unified
example(nil, func { ->
if blah {
return &S{}
}
return Iface(nil)
})
@tmaxmax
I've proposed a context-based return deduction for which I haven't found surprising or unintuitive edge cases yet. It has received exclusively feeling-based criticism.
I still disagree, Go expressions do too little to justify the extra complexity in the spec. If Go was the kind of language where the spec tries to save you from typing as much as possible there would be a lot more low hanging fruits for tokens that can be made implicit.
Regarding the return type inference see my objection above, you are going to have situations where the correctness of a lightweight function depends on the context where its used.
Another objection is that, I suspect, return type inference is going to become a footgun when used with interfaces. Let's say you write this:
func apply[InType, OutType1, OutType2 any](f func(InType) (OutType1, OutType2), x InType) (OutType1, OutType2) {
return f(x)
}
y, err := f(...)
if err != nil {
return ...
}
y, err = apply(func { x ->
if x == 0 {
return nil, errors.New(...)
}
return ...
}, y)
if err != nil {
return ...
}
This works just fine, because the inferred return type of apply is (whatever, error)
. Later on you decide to use a custom error type and change the line that says return nil, errors.New
to return nil, &CustomErr{...}
. You see no compiler errors so you think it's fine, but it isn't.
Comment From: mibk
As an aside, I’d insist on the
func
keyword. The\
is too cryptic for Go.
There appears to be significant support for retaining the func
keyword. I just wanted to point out that in Go there already is a precedent to omit a keyword: short variable declarations.
To me, it seems this proposal tries to tackle two things at once, but I see them as separate.
- In the case of one-liners, I want the syntax to be as short as possible. I believe it could actually improve readability.
- In the case of multiline anonymous functions, I just want to use the existing, familiar syntax. For these cases, I’d much prefer the “just placeholders” approach:
func() _ { return expr }
func(a, b _) _ { return expr, list }
func(a _) { stmts }
or perhaps use a special symbol to switch to omitting types:
@func() { return expr }
@func(a, b) { return expr, list }
@func(a) { stmts }
(The @ looks ugly, though.)
Comment From: tmaxmax
@aarzilli I've specified the rules in the first proposal comment – the type of the first return
would be used. Apart from the interface/concrete type return situation it wouldn't be surprising. Maybe this means that the necessary implementation effort should be put in to choose the interface.
Are you saying that the return type of a lightweight function should be determined solely by its contents, disregarding its context?
Context shouldn't be disregarded and I don't see what in @DeedleFake's reply would support that.
Go expressions do too little to justify the extra complexity in the spec. If Go was the kind of language where the spec tries to save you from typing as much as possible ...
I believe they do enough to justify the extra form without a return.
slices.SortFunc(s, func { a, b -> strings.Compare(a.Name, b.Name) })
The function exists solely to evaluate that expression. It revolves around the expression. The expression is important.
Even if you do have an explicit return, you still have the same issues around return inference. I don't see how what you're saying there has anything to do with the return inference rules.
you are going to have situations where the correctness of a lightweight function depends on the context where its used.
This is by design. There is also no language I know of where this isn't true about lightweight functions. And frankly I don't see what issue it causes. You provide an example of that but it is not convincing.
You see no compiler errors so you think it's fine, but it isn't.
You don't see any compiler error because it is fine. *CustomError
is assignable to error
, so the second return value of that apply
call is assignable after the change. If it wouldn't be, you'd get an error, but here the code is the same. And types have changed. You'd also get a compiler error if the return of the second branch wouldn't be of *CustomError
type.
I think the example you provided perfectly displays the abstraction capacity of type inference – it works regardless of the concrete type returned. These functions are not only about typing less, it's about abstracting types away where they don't matter. Type inference is not just about saving keystrokes, it's a way of abstraction. Type safety is ensured regardless. That example is a win in my book.
Comment From: aarzilli
@aarzilli I've specified the rules in the first proposal comment – the type of the first
return
would be used. Apart from the interface/concrete type return situation it wouldn't be surprising. Maybe this means that the necessary implementation effort should be put in to choose the interface.
Apologies, I didn't see that part. Also if only the first return counts some seemingly innocuous refactorings like:
f := func { x ->
if x != 0 {
return A
}
return B
}
converted to:
f := func { x ->
if x == 0 {
return B
}
return A
}
can either make the compilation fail or the return type silently change. I don't think things like this should matter.
Are you saying that the return type of a lightweight function should be determined solely by its contents, disregarding its context?
Context shouldn't be disregarded and I don't see what in @DeedleFake's reply would support that.
If you do that you get into a situation where the same lightweight function is either correct or contains a type error depending on which context it is used in, see what I wrote to @DeedleFake.
Go expressions do too little to justify the extra complexity in the spec. If Go was the kind of language where the spec tries to save you from typing as much as possible ...
I believe they do enough to justify the extra form without a return.
You can look at the code in the standard library from the old experiment: https://go-review.googlesource.com/c/go/+/406076/ there's very few functions that would use the single-expression syntax, around 10%, maybe less, mostly sorts. I don't think that's enough.
You don't see any compiler error because it is fine
It really isn't. Because the lightweight function now returns a concrete type and err is an interface variable the test if err != nil
will always be true. For example: https://go.dev/play/p/jN0Qz7B4APz
Comment From: tmaxmax
If you do that you get into a situation where the same lightweight function is either correct or contains a type error depending on which context it is used in
As I've stated in the previous comment, I don't see why that is a problem and how it could be different. That is expected. You are not explaining why it is a problem or how it can be confusing.
You can look at the code in the standard library from the old experiment
At that point there wasn't really any usage of slices
and no xiter
was on the table. It also depends on the kind of code you write in general and the style you write in – at work we'd certainly use this form for most usages of function literals.
It really isn't
I've missed the nuance. You're right, that code has a bug. Perhaps a somewhat more realistic scenario would be:
res, err := someFailingFunc(...)
if err != nil { /* ... */ }
v, err := withBackoffAndRetry(ctx, func { ctx ->
v, err := possiblyFailing(ctx, res)
if err != nil {
return v, &OpError{err}
}
return v, nil
})
func withBackoffAndRetry[T, E any](context.Context, func(context.Context) (T, E))
The problem occurs when a function returns a concrete type which implements an interface for which a nil
concrete value is not considered a "valid" state by the usage contract.
The other problem you mention – refactorings – is also only a problem when you return interfaces or distinct types with the same underlying type, or, in other words, values which can be implicitly converted to the return type.
The second problem is solvable – do the work to determine the common type. This wouldn't imply also solving the func example[T any](a, b func() T)
situation, functionally that's different – it's about return type inference for a single lightweight function. For interfaces you'd return the most generic one which is satisfied by all return values; for named types it's the named type when either values of said name type or literals are returned, otherwise an error.
The first problem, sadly, really isn't. That's the type system we have. It can still happen today:
// gqlerror.List implements `error`, is nil when there are no errors,
// appears in code for GraphQL APIs.
// Each error in the list corresponds to the element at the same position in the input slice.
// Useful to return instead of an `error` to make it explicit that there may be an error for each element.
func example(elems []SomeType) (results []SomeOtherType, errs gqlerror.List)
var err error
res, err := example(elems) // you don't always need to inspect the errors separately
if err != nil { /* ... */ } // bug here
So nothing new. Something my example and your example have in common is that they are contrived – yours is too abstract and I honestly can't think of real code sharing the same structure, mine with the backoff would probably not have an E
generic parameter but use error
directly, so the return type will still be the error
interface even if you return a concrete value, and this last one is very specific – it appears exactly once at work in a very big codebase.
Lastly, the following:
y, err = apply(func { x ->
if x == 0 {
return nil, error(&CustomError{...})
}
return ...
}, y)
y, err = apply(func(x typeOfY) (typeOfY, error) {
if x == 0 {
return nil, &CustomError{...}
}
return ...
}, y)
are not that much different. I argued to use only the type of the first return because that would be the closest to the beginning of the function body and would be very similar to partially specifying the return value. If you were to use all branches you'd have cases where a subsequent return would decide the return type for all branches, and in bigger bodies it might be initially confusing what determines the return type.
Sadly, because of this edge case with interfaces you can't abstract the type away, because implicit contracts vary between interface types so you have to know which accept concrete nil
s and which don't. That's a fly in the ointment. It must be noted though that it is not caused by lightweight functions: these functions may increase the frequency this problem is encountered at.
Again, there are three options: - no such lightweight functions: disadvantages anyone who wants this in any shape or form - no return type inference based on body: disadvantages everyone - with such return type inference: new situation where classic footgun may emerge, disadvantages some, sometimes
I still believe that having return type inference based on body causes the least amount of pain.
In light of this maybe it would be worth exploring not adding a completely new syntax and just allowing type ellision in existing function literals. One could also specify only a part of the types:
y, err = apply(func(x _) (_, error) {
if x == 0 {
return nil, &ConcreteError{...}
}
return ...
}, y)
At the expense of very short expression functions, we reap all benefits of type inference and have an elegant way to solve these edge cases. I'd even go as far as to allow:
func(a, b _) { return strings.Compare(a.Name, b.Name) } // func(_, _) int
func() { return 5 } // func() int
though this might be controversial. Stuff like:
func example[T, U, V any](func(T) (U, V))
example(func(x int) { return x, 1.0 }) // T = U = int, V = float64
could work aswell.
We could also make the situation where the compiler must decide on a return type an error and force the programmer to explicitly specify it:
func() _ { // perhaps not controversial
if cond {
return 0, &ConcreteError{...}
}
return 1, error(nil)
} // compiler error: type of return value 1 ambiguous, please specify explicitly.
Named returns would also be supported – just type _
after the name(s):
func(x, y _) (res, err _) { /* ... */ }
func(x, y _) (res _, err error) { /* ... */ }
Comment From: aarzilli
I argued to use only the type of the first return because that would be the closest to the beginning of the function body and would be very similar to partially specifying the return value
BTW this wouldn't work even if the problem with interfaces didn't exist. For a lot of go functions the first return statement is something like if err != nil { return nil, fmt.Errorf... }
and you'll only find out the type of the first return variable on the last return.
But I think the problem with interfaces kills return type inference completely: most go functions return (something, error)
and you can't write any of them using the lightweight syntax because they are fragile, any refactoring is liable to introduce an unwanted typed nil. Consider:
f := func { ->
if blah {
return strconv.Atoi(...)
}
if blah2 {
return 0, &CustomErr{...}
}
return ..., nil
}
this will work until someone goes in and deletes the first if, then it will be subtly broken.
It's probably better to spend some effort thinking about a syntax that doesn't need return type inference but also doesn't need all types to be specified if the return type can't be inferred from context. The placeholders syntax is the only one I see that is promising in this regard (even though I think it's the ugliest of all).
Comment From: tmaxmax
The reason why I said it would work was that you would do:
func { ->
if cond {
return (*pkg.YourReturnType)(nil), fmt.Errorf(...)
}
return val, nil
}
If other return branches would have different return type, it would be an error and you'd have to unify them. This still won't magically turn this:
func { ->
if cond {
return (*pkg.YourReturnType)(nil), &OpError{...}
}
if otherCond {
return nil, &OpError{...}
}
return val, nil
}
into a `func() (*pkg.YourReturnType, error). You'd have to do:
func { ->
if cond {
return (*pkg.YourReturnType)(nil), error(&ConcreteError{...})
}
// ...
return val, nil
}
but this is just a much worse normal function literal. In this case you could use the normal literal without losing anything, but if you have parameters then you lose type inference. The refactoring point also still stands – even if you deduce the return type based on all branches, if you delete the branch with the explicit return types the code may break. It's brittle and confusing.
So yeah, with this sort of code it is a lost fight. Other languages pull it off probably because they don't have the interfaces issue and function types are covariant over their return type. Neither is the case with Go.
When it comes to that syntax, technically what I proposed could support parameter and return types:
func { (a int, b) (_, error) -> strconv.Atoi(a) + b, nil }
Compared to placeholder syntax:
func(a int, b _) (_, error) { return strconv.Atoi(a) + b, nil }
there isn't much difference. I think no syntax will fare better when there are multiple return types – you'd still have to use a placeholder. It might be admittedly ugly, it might be a little longer for simple use cases, but I think it gets us most of the way there – it is shorter, and would support both inferred and explicit types.
To help some situations you could have no placeholder to assume completely inferred return types:
slices.SortFunc(s, func(a, b _) { return a.Size - b.Size })
It is backwards compatible with functions which don't return anything, because their type will remain the same anyway. Keep in mind that return type inference based on body would also not be supported, so this would work only in contexts where a return value is expected. While some may argue that it is confusing, I think the confusion is at most minimal – the context provides enough information to expect a return value. Mapping helpers would require explicit types:
xiter.Map(func(_ _) int { return 5 }, it)
That double placeholder looks... interesting though. But this is a worst case syntax.
Comment From: DmitriyMV
That double placeholder looks... interesting though. But this is a worst case syntax.
Can't we omit one placeholder just like we currently do with regular func(int) int { return 5 }
functions?
Comment From: griesemer
Thanks @tmaxmax for the summary.
As a reminder, I did run an experiment in May 2022 and posted the results. One conclusion from those experiments was that what looks appealing syntactically for some examples may not really work well in typical Go code. Whatever syntax proposals are discussed here, eventually they need to be reviewed in context of existing code. (The experiment provided all the CLs, it shouldn't be too hard to adjust them as needed if anybody is so inclined).
One of the observations from that experiment was that a lot of function literals appear in tests, and they require the signature
func(t *testing.T)
Also, a lot of function literals contained more than just an expression result.
Some of the conclusions we drew from looking at the CLs of rewritten code was that:
- The syntax
func t { ... }
where the parametert
was simply not parenthesized (to distinguish this case from a regular function literal) looked odd. It made thet
seem like an argument to the function being called. - Similarly, the notation
(...) => { ... }
may work in some cases, but looks a bit odd in other cases.
Also, the primary benefit of the compact notation was to be able to leave types away, and much less so to have a compact function body because most of the time that function body consisted of more than just a return expression.
Some of the conclusions I took from this is that:
- Keeping the
func
keyword is an important syntactic feature that we probably want to keep it in both regular and compact form. - The incoming parameters must be parenthesized in some way so they don't look like arguments to the called function.
- A
=>
or->
based syntax (withoutfunc
keyword) is just not that helpful in Go.
Taking these points into account, and because we can't use (
)
(but see below) and probably don't want to use [
]
for parenthesizing parameters, we seem to end up with a form that starts like this:
func { a, b
where a
, b
are incoming parameters parenthesized by the {
}
of the function body.
(More recently it was suggested that we use a normal signature but _
to indicate that the types are inferred. I believe it's a mistake to repurpose _
in this subtle manner. Also, consider the confusion when we want to name some of the parameters _
because they are not used.)
Given the suggested start above, the question remains what should follow next. @tmaxmax is in favor of just using ->
(if I read correctly), and using context to determine if a (final) function call's result in the literal should be ignored or considered for the result type of the function literal. I've been suggesting using |
for the case where we just have statements, and ->
for the case where we just have a result expression.
Having two different tokens |
vs ->
where the latter is essentially just a shortcut for |
return
in retrospect doesn't seem very Go like: ideally we don't provide multiple (almost identical) ways to achieve the same in Go. The notable exception are variable declarations/initializations, but those are among the most common constructs in code. Function literals don't rise to the same level of prominence.
Taking all this into account, and trying to keep it as simple as possible, maybe a workable syntax is as follows:
"func" "{" [ IdentitifierList separator ] StatementList "}" .
This is about as simple as it gets: the {
immediately following the func
indicates a short/compact/light-weight function literal. If there are incoming parameters, they are listed before any statements, separated by a separator. The statements are ordinary Go statements. There are no special cases for only expressions, etc; one needs to write a return
statement if one wants to return a result.
The separator would need to be decided. I've argued earlier that using ;
is fragile and doesn't allow for good error messages. From the discussion so far this leaves |
or ->
.
Personally I prefer |
because it's a token that already exists in Go, it's short, and it doesn't imply some kind of result that's following. This would lead to code such as this:
func {} // func() {}
func { | } // invalid: identifier list cannot be empty
func { return 42 } // func() T { return 42 }
func { x, y | return x < y } // func(x T1, y T2) bool { return x < y }
func { _, y | return y < 0 } // func(_ T1, y T2) bool { return y < 0 }
func { s | fmt.Println(s) } // func(s T) { fmt.Println(s) }
func { a, b, c | } // func(a T1, b T2, c T3) {}
func { t | /* test code */ } // func(t *testing.T) { /* test code */ }
For comparison, using ->
:
func {} // func() {}
func { -> } // invalid: identifier list cannot be empty
func { return 42 } // func() T { return 42 }
func { x, y -> return x < y } // func(x T1, y T2) bool { return x < y }
func { _, y -> return y < 0 } // func(_ T1, y T2) bool { return y < 0 }
func { s -> fmt.Println(s) } // func(s T) { fmt.Println(s) }
func { a, b, c -> } // func(a T1, b T2, c T3) {}
func { t -> /* test code */ } // func(t *testing.T) { /* test code */ }
The next step would be to run an experiment over a larger code base and see if any of these two notations is palatable.
Comment From: ianlancetaylor
Thanks for the summary. Just checking: you wrote
func { x, y | return x < y } // func(x, y T) bool { return x < y }
func { _, y | return y < 0 } // func(_, y T) bool { return y < 0 }
Should that be
func { x, y | return x < y } // func(x T1, y T2) bool { return x < y }
func { _, y | return y < 0 } // func(_ T1, y T2) bool { return y < 0 }
That is, did you mean to restrict the parameters to have the same type, or can they have different types, as inferred from the left hand side of the assignment.
Comment From: jimmyfrasche
It looks more odd to me to have the parameters in the {
then to have them unbracketed but I'm sure I'd get used to it.
Could you use ||
iff there are parameters, so
func {}
func |a| {}
func |a, b| {}
(I suppose <>
are also usable in this specific case)
I do think there is a real, practical savings in being able to handle the special case of returning a single expression but that it must have its own form. Since the syntax above composes with the ->
or {}
syntax I posted recently it could be accepted later after the numbers are in for how often { return expr }
shows up in practice with short funcs and generics.
Comment From: fzipp
I can’t remember my suggestion https://github.com/golang/go/issues/21498#issuecomment-1133173989 receiving any feedback that deemed it unfeasible:
func(a, b, c): { return a + b*c }
func(a, b, c): a + b*c
Is scanning to the closing parenthesis to see if it is followed by a colon or not too much parser look-ahead to make the distinction?
Otherwise, I only see advantages: the func
keyword is present, the parameters are in parentheses at the expected place, and the colon draws a loose analogy to another syntax construct that also involves type inference, namely the := operator.
Comment From: entonio
Could someome please expound on what the problem is with @jimmyfrasche 's 1-statement syntax func a, b -> c
? As a user, half of the interest of the short syntax option is not to have to write {}
. Wasting this economy in a large or a majority of cases should require extensive argumentation.
It's a problem to have two short syntaxes in JavaScript, but that's because the language doesn't have type, one may easily misread which is being used, and the compiler can't help. That's the opposite of Go.
Comment From: DmitriyMV
Answered below, ignore.
Why not `:` inside `{ ... }`? This looks short enough and even quite readable with proper formatting?
func {} // func() {}
func { : } // invalid: identifier list cannot be empty
func { return 42 } // func() T { return 42 }
func { x, y: return x < y } // func(x, y T) bool { return x < y }
func { _, y: return y < 0 } // func(_, y T) bool { return y < 0 }
func { s: fmt.Println(s) } // func(s T) { fmt.Println(s) }
func { a, b, c: } // func(a, b, c T) {}
func { t: /* test code */ } // func(t *testing.T) { /* test code */ }
And it also, IMHO, a bit cleaner in multi-statement variant:
t.Run("my-sub-test", func { t: {
t.Helper()
t.Parallel()
t.Logf("Hello world!\n")
}})
compared to
t.Run("my-sub-test", func { t | {
t.Helper()
t.Parallel()
t.Logf("Hello world!\n")
}})
Another question is that what do we do with xiter.Map[IN, OUT any](fn func(IN) OUT, iter.Seq[IN]) iter.Seq[OUT]
and others (raised by @aarzilli) in cases where there are different returning types inside lightweight anonymous function?
Comment From: griesemer
@ianlancetaylor Re: your comment: of course. Fixed the comments in the original post. Thanks.
Comment From: fzipp
Why not
:
inside{ ... }
?
This was already discussed: it could be confused with a label. Also, the goal of my suggestion is to keep the parameters outside the brackets and to keep the round parentheses, so that it looks as much as possible like a long-form function literal.
Comment From: DmitriyMV
I'm not entirely convinced about https://github.com/golang/go/issues/21498#issuecomment-2450361209 since we are inside func { ... }
without ()
so it's visually distinctive since it's the first "statement". But I don't have any hard feelings about |
either.
Comment From: griesemer
@Jimmy2099 We could have the |
outside the {
}
but it wouldn't be shorter or more compact. Really, we just need one token to distinguish func(a, b, c){ ... }
somehow so the compiler knows it's not a regular function literal. I just don't know that we have seen a compelling idea yet (as in: "an idea that a lot of people liked or at least were not strongly against").
It seemed to me that there was an opening recently towards a simple notation that moved the parameters inside the "{}" that is mostly acceptable and I tried to steer it a bit toward something like func{ a, b, c | ... }
. But if it turns out that there's still too many preferences for different syntaxes, then of course we can't really make progress.
Comment From: griesemer
@fzipp Your suggestion could work, like a thousand others (see my comment above). Does it look Go-like? Not so sure myself. Does it look ok in the context of actual Go code? I don't know but the ":" looks out of place to me.
Comment From: griesemer
@entonio Look at the experiments I mentioned. func a, b -> c
just doesn't look good in the context of Go code. The reason is that it looks like a
, b
are arguments to the called function because if missing bracketing.
Comment From: jimmyfrasche
@griesemer my goal was not to make it shorter or more compact. I'm adding an extra token! I had two goals: clarity and extensibility 1. move the parameters out of the block so they're where they normally are 2. allow the possibility of adding a second form shorthand for returning a single expression
Even if 2 never happens 1 is worth it because func |a, b, c| {}
, while obviously different, is more similar to func (a S, b T, c U) {}
than func {a, b, c | }
because the parameters are in the same place and the {}
means the same thing.
Comment From: entonio
@entonio Look at the experiments I mentioned.
func a, b -> c
just doesn't look good in the context of Go code. The reason is that it looks likea
,b
are arguments to the called function because if missing bracketing.
Ok, you mean that it looks like func a
is an argument and b -> c
is another one. It took me a while to understand it, probably because it doesn't look like that to me: the appearance of func
makes me enter another parsing mode.
Either way, my chief objection is to requiring {}
for the single expression functions. Has func(a,b) c
been discussed?
(I'm not really here to propose any specific syntax, only to politely ask that the baby isn't thrown out because of considerations about the conductivity of the bathwater. I didn't want to enter the discussion because I'm not investing the same effort you all are, but I felt that @jimmyfrasche was left alone saying what to me seems The Point.)
Comment From: tmaxmax
@DmitriyMV to support xiter.Map
's case, based on the discussion above with @aarzilli, we'd need to explicitly specify the return type. Return type inference based on function body sadly, as much as I wish it did, in the context of Go, is very brittle and presents many edge cases, mainly due to implicit contracts related to certain interfaces (for example, an error
with a concrete type and nil
concrete value isn't really valid, if err != nil
fails) and lack of covariance.
This is why we need a syntax which supports at least specifying the return type.
For the func { a, b -> stmts }
syntax we can do it as follows:
func { (a, b) ReturnType -> stmts }
func { () ReturnType -> stmts }
// or
func { a, b: ReturnType -> stmts }
func { : ReturnType -> stmts }
To me the second one looks too much as if b
had that return type, but that's just my opinion. I'd get used to it. I used arrows because I like them more than |
, and I think it's even better that the ->
symbol doesn't exist in the Go grammar yet and would become a distinctive sign of lightweight function literals, but I could get used to whatever honestly.
Partially specified return types should also be possible:
func { (a, b) _, error -> stmts }
We'd still need a placeholder for these. We could also add parameter types just for consistency:
func { a int, b string: _, error -> stmts }
func { (a int, b string) _, error -> stmts }
but at this point in my opinion any syntax will look very similar to:
func(a int, b string) (_, error) { stmts }
func(a, b _) (_, error) { stmts }
especially if we agree to drop support for implicit return of expression value, when the lightweight function's body is just an expression, as @griesemer proposes. If we drop that, just retrofitting type inference onto existing function literals seems the best solution, because there isn't really any shorter syntax. I've talked more about this here and here.
In short, if we drop: - return type inference based on body - implicit return of expression results
I don't see a better syntax than the already existing one but with placeholders. That one would be the most familiar, most flexible – especially because it naturally allows specifying both return and parameter types –, and would usually be almost equally short.
@griesemer
@tmaxmax is in favor of just using
->
(if I read correctly), and using context to determine if a (final) function call's result in the literal should be ignored or considered for the result type of the function literal.
I'm in favour of doing that only when the function body is a single ExpressionStmt
. If it is an expression which can't be a statement, the return value must be the expression's value type; if it is some other statement, there is a return only if there are return
statements. The following wouldn't return anything (just normal statements):
func { ->
fmt.Println("Hello")
fmt.Println("World")
}
The following two would both return if the context doesn't expect a func(...)
(they have a single ExpressionStmt
)
func { x -> fmt.Println(x) }
func { x ->
fmt.Println(x)
}
The following two would return just like a normal literal (normal statements):
func { x -> return fmt.Println(x) }
func { x ->
fmt.Println("Hello")
return fmt.Println(x)
}
The following would always return regardless of context and it would be a compiler error to use them in a context where no return is expected:
func { a, b -> a.Size - b.Size }
func { a, b ->
real(a) - real(b)
}
Whitespace doesn't matter. What makes a difference depending on context is whether the function body is comprised of a single statement and whether that is an ExpressionStmt
or not. I'm not advocating for implicit returns in general.
Comment From: torbenschinke
But if it turns out that there's still too many preferences for different syntaxes, then of course we can't really make progress.
That would be very frustrating, especially if everybody agrees that any shortform actually improves the language. Probably any reasonable variation has been shown. I would expect, that the core team picks one of the proposed solutions it finds best, instead of not making progress at all. This thread won't represent the majority of the community anyway.
Comment From: DmitriyMV
@tmaxmax
Your idea about function result based on what function expects doesn't work well with multiple returns. Consider the following:
func Map[V, R any][fn func(V) R, it iter.Seq[V]) iter.Seq[R]
func Map2[V1, V2, R1, R2 any][fn func(V1,V2) (R1,R2, it iter.Seq2[V1,V2]) iter.Seq2[R1,R2]
func ForEach2[V1, V2 any][fn func(V1, V2), it iter.Seq2[V1, V2])
````
With those signatures in mind it means:
```go
i1 := Map(func { v -> fmt.Println(v) }, slices.Values([]int{1,2,3,4,5,6})) // Doesn't compile, since Map expects one return, gets two.
i2 := Map2(func { v1, v2 -> fmt.Println(v1, v2) }, slices.All([]int{1,2,3,4,5,6})) // Compiles, returns new iterator with iter.Seq2[int, err]
ForEach2(func { v1, v2 -> fmt.Println(v1, v2) }, slices.All([]int{1,2,3,4,5,6})) // Compiles, executes anonymous function, returns nothing
This hinders refactoring, since breaking changes are no longer actually breaking and can produce silently incorrect code. It also means I can no longer know if passed callback result is ever used by just looking at the anonymous function signature.
If you don't think it's a problem, looks at this variant:
num := Execute(func { v1, v2 -> fmt.Println(v1, v2) },
Mutate(func { v1, v2 -> fmt.Println(v1, v2) }, slices.All([]int{1,2,3,4,5,6})),
)
Another thing, that if we are talking about expressions and multiple returns I don't think xiter.Repeat2( func { -> 42, 43 }, 100)
is readable by Go standards.
Comment From: tmaxmax
I'm not sure I follow your examples and the point about refactoring. Those examples that you give seem to work as expected in every case – I'm not seeing what doesn't work there with multiple returns. What's an example of refactoring which introduces bugs because of this behavior? The examples also resemble nothing I'd ever write – you won't use Println
in a Map
helper, and the Execute/Mutate
one just looks again like a Map/ForEach
. And why would Go have a ForEach
helper? You have for
loops for that, which work nicely with iterators.
The only refactoring that changes the meaning of a lightweight function which I can imagine is when you'd change the surrounding context but not the function itself. So, changing Map2
to Map
or ForEach
– the first change is a compiler error, so nothing's broken, the second just ignores the return value which is fine for functions. Unless you have a clear example of this I'm not really following.
Besides all that, above we've discussed that return type inference based on body should not work. This means that in every one of your examples you'd have to specify the return type of the functions. Which means the examples would look like:
// Very obvious that it won't work.
i1 := Map(func { (v) int -> fmt.Println(v) }, slices.Values([]int{}))
// Very obvious that it works.
i2 := Map2(func { (v1, v2) int, error -> fmt.Println(v1, v2) }, slices.All([]int{}))
// Function expects no return, `fmt.Println` can be used in these contexts, works.
ForEach2(func { v1, v2 -> fmt.Println(v1, v2) }, slices.All([]int{}))
num := Execute(func { v1, v2 -> fmt.Println(v1, v2) },
Mutate(func { (v1, v2) int, error -> fmt.Println(v1, v2) }, slices.All([]int{1,2,3,4,5,6})),
)
and now it's very clear what returns and what doesn't. You wouldn't have to specify return values when they are deducible from context (which is actually not the case for Map
-like helpers):
slices.SortFunc(nums, func { a, b -> b - a })
but here it's also very clear what happens because of the context.
The last example you provide would look like:
xiter.Repeat2(func { () int, int -> 42, 43 }, 100)
and, even with return type inference based on body, I personally don't dislike it. I'd happily read or write that.
"Readable by Go standards" is a very subjective and relative statement – everyone has an at least slightly different opinion on what the "Go standard" is, and there have been already enough contradictory discussion on that in this thread and other threads. At this point I don't think anyone can use such statements as arguments – the most you can say is "I don't find this readable".
Comment From: DmitriyMV
Besides all that, above we've discussed that return type inference based on body should not work.
Current @griesemer summary suggests otherwise.
This means that in every one of your examples you'd have to specify the return type of the functions.
What about this case?
var a int
var b error
a, b = func { -> 42, &os.LinkError{}}()
but here it's also very clear what happens because of the context.
It's actually not. Per yours "is a very subjective and relative statement" I don't find this readable. At all.
We, currently, don't have any expressions as part of the function body directly, we have them only inside blocks -> statements. I see no reason why anonymous functions should be constructed differently. Either we allow this for all functions types, or we creating new subclass of function types.
Overall, I don't see any variant of this proposal being accepted without return
being in the final proposal. At this point I'm strongly against omitting return
in any case as it creates unnecessary for complexity for minimal gains. YMMV ofcourse, as well as Go team (who will make the final decision).
Comment From: tmaxmax
Current @griesemer summary suggests otherwise.
That summary states precisely nothing related to lightweight function semantics – how did you reach that conclusion?. No stance is taken and nothing is expressed with regards to return type inference in that summary. It seems to me that Griesemer wants to discuss syntax first, independent from semantics.
What about this case?
That's completely another beast and I'm not sure why you'd bring it up. The function literal itself is not in an assignment context where its type can be inferred, only its call results are. That would be a compiler error. It's a very big difference between the following:
var f func() (int, error) = func { -> 42, &os.LinkError{} }
and what you've written there. In your case you would have to specify the return type because the function literal itself is in an assignment context where its type can't be inferred – specifically it is in no assignment context.
It also feels dismissive and unproductive to just throw another (arguably irrelevant) example and answer none of the questions related to your previous comments that I've asked. You've addressed neither how and why exactly your examples depict how implicit returns don't play well with the scenarios you've provided, nor what sorts of refactorings would lead to code breakage.
It's actually not. Per yours "is a very subjective and relative statement" I don't find this readable. At all.
Understandable.
We, currently, don't have any expressions as part of the function body directly, we have them only inside blocks -> statements. I see no reason why anonymous functions should be constructed differently. Either we allow this for all functions types, or we creating new subclass of function types.
Just because this doesn't exist currently doesn't imply that there would be no good reason to exist. I'm trying to argue exactly for what you are against – introducing a new subclass of function types from a syntactic standpoint. Not having things for the sake of not having them isn't good argumentation. You are also not bringing any arguments as to why you see no reason to do this, you're just stating there isn't.
I'm personally advocating a form with implicit return because I feel that most of the times I'm using function literals they are comprised of a single return
statement. I will take some time to actually confirm this by running an experiment on the work codebase and collecting some statistics, the same way it was done on the standard library.
Regarding the standard library experiment, I have a feeling it might be skewed in favour of a certain coding style, which might not prevail in all codebases. For what I'm working on, neither of the following observations feel true: - "One of the observations from that experiment was that a lot of function literals appear in tests" - "because most of the time that function body consisted of more than just a return expression"
With my premises I will naturally argue for an implicit return syntax. I think a future experiment should look at multiple kinds of codebases, not just the standard library, in order to get an accurate perspective.
In light of @torbenschinke's comment, if we still want to keep a democratic approach to this, I believe there should be a process change in how the discussion around lightweight annonymous functions is organized.
Right now we are discussing multiple subjects and opinions at once in an issue. We are also discussing a lot, so most of the comments end up being not read by others, especially because everything is presented linearly from the beginning to the end in a single thread. It's close to impossible to have a broad perspective on everything that's being discussed, to take all previous context in consideration, and to weigh general sentiment about subjective issues. Everytime something is discussed which is disagreed by another person, that person comes and states something against it, hoping to divert the discussion away from the "dangerous" topic. We are in a situation where everyone tries to be as loud as they possibly can.
I believe it would really help if a GitHub Discussion for this proposal would be opened, where there would be distinct threads for discussing semantics, and presenting various syntaxes. This would keep at the top level every subject and syntax proposed, would give way more visibility into what has already been proposed and would allow voting for or against all syntaxes.
This way new people willing to engage can quickly have all important context – and not end up proposing something already discussed because they don't like what's currently being discussed –, people not willing to propose things themselves could vote for what they like and what they don't, and parallel discussions would be kept organized and focused – you won't have people who just want something different divert the discussion. This way you could develop multiple syntaxes, you could develop the expected semantics, have people voice their subjective opinions in a non-distracting manner and, at the end, be able to weigh people's sentiment and choose accordingly a well-formed solution.
This way, depending on who's reading, sentiment will come in waves and always swing the discussion away from the direction it was on. You'll always end up determining that "there's still too many preferences for different syntaxes".
In the current state this discussion will end up being not more than a time sink for everyone involved. No one can develop their ideas, and because of this no one can compare developed ideas, and because of this there is always something "wrong" with each idea.
Comment From: jrop
But if it turns out that there's still too many preferences for different syntaxes, then of course we can't really make progress.
I may not speak for everyone else here, but what I feel would be a major step forward, even without a shorter form syntax for the time being, would be inference of types where it could cut down on repetition (think: Map/Filter/Reduce/etc. data pipelines).
mySum := mySlice.
Map(func(x) { return x * 2 }). // type of x and the return type are inferred
Filter(func(x) { return x % 2 == 0 }).
Reduce(0, func(acc, curr) { return acc + curr })
Now, I'll throw my 2c in here. A short-form syntax that I have pondered for a few years (this is adaptable to various languages), that is elegant in my mind is that of: func ... = <expr>
. That is, normal function notation, followed by an =
sign is equivalent to a single expression return. The above would become:
mySum := mySlice.
Map(func(x) = x * 2). // type of x and return type are inferred
Filter(func(x) = x % 2 == 0).
Reduce(0, func(acc, curr) = acc + curr)
Edit: The benefits of this syntax are: 1. Easy to parse (an existing objective of Go) 2. Types can be made explicit for parameters or the return very easily, and it looks familiar to the function syntax that everyone already knows. Same for generics 3. Go's function syntax is already light, this just makes the single-expression return more ergonomic 4. Doing things this way makes it easy to iterate on this feature in two phases: - implement inference, then - optionally support this short-form syntax
Comment From: atdiar
I like the notation @griesemer proposed. It's not too far from mathematics.
The issue from experience is that not having the full signature is annoying when writing such function from scratch.
For instance, if I don't remember the function signature of a callback that returns a bool, it's easy to forget to return the bool in question.
Same way, let's imagine a program had to write such code, it would be necessary to add a layer of indirection for the program to verify the function signature and its semantics at each site.
So far, seems like we are stuck with the one current way of writing a function. It has definite advantages, one being that it's already implemented. Even though eliding the input types is still as attractive in the callback case.
Comment From: jimmyfrasche
I went through https://en.wikipedia.org/wiki/Examples_of_anonymous_functions I've left out languages that only use the same syntax as regular functions but without a name (like Go) and languages where the execution model is too different (APL) or the syntax is simply too strange to merit consideration (C++) or any syntax that does not let you name arguments. When possible I validated the below against further references for the language but it's possible there are errors and inaccuracies.
I'm using expr
for a single expression, block
for an explicit block, and exprOrBlock
for when either is allowed. Some languages do not make this distinction so to simplify notation I have used expr
for only one line allowed and exprOrBlock
for multiple lines allowed. Where I've written (args)
some languages allow a single argument without the ()
but I have not tracked that. An OR
means there are multiple syntaxes. Some languages had additional syntaxes not listed here as I only listed the variants that met the criteria in the previous paragraph.
Langs in the top 50 of https://www.tiobe.com/tiobe-index/ for Oct 2024 will have their rank noted after their name. There are issues with the ranking but it's good enough to establish a rough sense of popularity/ubiquity.
C# (5): (args) => exprOrBlock
CFML: (args) => exprOrBlock
D (41): delegate (args) block
Dart (31): (args) => expr
OR (args) block
PascalABC.Net: args -> exprOrBlock
Elixir (47): fn args -> exprOrBlock end
Erlang (46): fun(args) -> exprOrBlock end
Haskell (32): \ args -> exprOrBlock
Java (3): (args) -> exprOrBlock
Javascript (6): (args) => exprOrBlock
Julia (28) (args) -> exprOrBlock
Kotlin (21): { args -> exprOrBlock }
Lisps/schemes (33): (lambda (args) exprOrBlock)
Clojure: (fn [args] exprOrblock)
Matlab/octave (12): @(args) expr
Maxima: lambda([args], expr)
Ocaml: fun args -> exprOrBlock
F# (50): fun args -> exprOrBlock
SML: fn arg => exprOrBlock
Nim: I honestly could not figure out what is and is not supported. The documentation is too much of a mess. There seems to be => and -> macros that at least support expressions. I will not count this one in any analysis.
PHP (15): fn(args) => expr
Python (1): lambda args: expr
Ruby (18): { |args| exprOrBlock }
OR do |args| exprOrBlock end
Rust (13): |args| exprOrBlock
Scala (26): (args) => exprOrBlock
Smalltalk: [:arg1 :arg2 | exprOrBlock ]
Swift (20): { args in exprOrBlock }
Vala: (args) => exprOrBlock
VB.NET (7): Function(args) expr
OR Sub(args) block End Sub
That's 28 languages. 20 in the top 50, 10 in the top 20, and 5 in the top 10.
17 use ->
or =>
in the syntax. 2 of those limit the RHS to an expr and the rest allow either an expr or a block. 9 use the "(args) =>
/->
" syntax whereas 8 have a token before args and only 3 of those include a terminal token. In the top 20 langs, 4 have an arrow syntax, 3 use =>
, 3 allow an expr or a block and only 1 requires an initial token.
Python's lambda arg1, arg2, arg3:
is similar to the func arg1, arg2, arg3
syntax so it may appear odd in a novel context but given Python's ubiquity it is one of the more common syntaxes. see also https://github.com/golang/go/issues/21498#issuecomment-2463394225
4 languages bracket the entire anonymous function with {}
or []
, none in the top 10 with 3 in the top 50 (18, 20, 21).
Use of ||
to bracket arguments occurs in 2 languages both in the top 20. Use of a single |
is limited to Smalltalk which is not top 50 but of great historical importance and influence.
Not counting lisp/scheme/clojure, 8 require an initial and terminal token (one only requires the terminal token in block form). 6 of these languages are top 50 and 3 top 20.
Comment From: DeedleFake
Something worth noting is that a fair number of those languages, such as F#, are functional, and in many cases those languages effectively only have expression bodies for all functions, not just anonymous ones, though they often have a way to do some limited form of processing, such as binding expressions to names, before executing and returning the last expression in the body. Elixir, for example, doesn't even have a return
keyword, and I believe the same is true for F# and probably Ocaml as well, among others. Those languages don't differentiate body types for anonymous functions because they don't really have different body types anyways.
Ruby is a bit of a weird one because it has both blocks and lambdas and they're not quite the same. The syntax given above is only for blocks. Standard lambda syntax actually just calls a method and passes it a block, so it uses the above, but there's a shorthand that looks like ->(args) { exprOrBody }
. Also, the bodies of all Ruby functions, including lambdas and blocks, return the last expression automatically if reached but can also be returned from manually, similarly to Rust but without the weird semicolon trick.
Edit: Oh, also, Kotlin and Ruby both do weird things with using anonymous functions as arguments to methods. Kotlin is simpler in that it allows you to put the anonymous function definition outside of the parenthesized argument list if it's the last argument, i.e. someMethod(a, b) { v1, v2 -> v1 + v2 }
. Ruby, on the other hand, requires that blocks be used in exactly that way. That's why it has both blocks and lambdas, as a block definition is not an expression.
Comment From: jimmyfrasche
@DeedleFake I did note that I was counting everything-is-an-expression languages as exprOrBlock
. I did so as you can use one line or multiple lines which is roughly analogous to what's being discussed here. If there had been a functional language that restricted anonymous functions to one line as some quirk of its grammar then I would have noted that as expr
.
I was under the impression that Ruby's ->(args) block
wasn't syntax but regular code named ->
that took a block, which is why I didn't include it. (I could be wrong though, I've never had cause to use Ruby)
That is an excellent point about Kotlin/Ruby/Smalltalk, though: the reason they take the arguments in the block is because they allow it to make regular functions look like syntax for DSLing, which is something that Go will presumably never do so it would be very strange to use that style without the promise of that corresponding feature.
Comment From: DeedleFake
I was under the impression that Ruby's
->(args) block
wasn't syntax but regular code named->
that took a block, which is why I didn't include it. (I could be wrong though, I've never had cause to use Ruby)
The ->(args) { exprOrBlock }
syntax is a shorthand for the lambda
method which takes a normal block and returns an anonymous function that calls that block. That looks like lambda { |args| exprOrBlock }
. The odd thing about the ->
syntax is that the argument definitions are in a completely different place from everything else in Ruby.
Comment From: jimmyfrasche
Right, but is it syntax in the sense that the parser/interpreter has to do something special or it that something you can do with standard metaprogramming? Could anyone write ->
or is it built into the language in some way?
Comment From: DeedleFake
I'm pretty sure that it's a special-case in the language.
On a side note, while looking into it, I found that apparently the parentheses are optional, though, so -> v1, v2 { v1 + v2 }
is legal, which looks quite a bit like some of the suggestions from elsewhere in this massive chain of comments. Parentheses are optional in Ruby for function calls, but I didn't realize that that was also true for function definitions.
Comment From: jimmyfrasche
I can find that lambda
is not special syntax and that ->
was released in Ruby 1.9 but can't find anything official that says one way or the other but it does appear to be syntax as I cannot find it listed as a method anywhere like I can for things like ==
or +
.
Comment From: jimmyfrasche
Actually, all of the below bracket asymmetrically with an initial word and final symbol instead of paired symbols like (
and )
Python (1): lambda args: expr
Elixir (47): fn args -> exprOrBlock end
F# (50): fun args -> exprOrBlock
Ocaml: fun args -> exprOrBlock
SML: fn arg => exprOrBlock
Python is still the major data point in that class but the MLs are popular as teaching languages so they may be more familiar than they are used.
If the criterion is extended to any two tokens that don't form a pair, the list expands by:
Swift (20): { args in exprOrBlock }
Kotlin (21): { args -> exprOrBlock }
Haskell (32): \ args -> exprOrBlock
Smalltalk: [:arg1 :arg2 | exprOrBlock ]
The (args) => exprOrBody
still looks like the syntax that is being converged to, though.
Comment From: jcsahnwaldt
Thanks a lot for the survey, @jimmyfrasche! Minor correction: The Wikipedia article is incomplete, C# lambdas also have exprOrBlock
syntax, not just expr
.
Comment From: jimmyfrasche
I attempted to double check that but did not find the good link you provided, thanks! Updated.
Comment From: hherman1
Is func args {}
plausible?
So it’s just the normal syntax minus parentheses. the rule would be that if you don’t have parentheses you don’t need types for your arguments or return value, but you could only use it in a place where the types can be inferred
Comment From: griesemer
Thanks for your language overview, @jimmyfrasche, that is helpful.
I just note that the =>
(or ->
) notation was not well received in the context of my experiments two years ago. Also, at least in the code base at that time, most function literals were significantly larger than just returning a single expression.
Comment From: jimmyfrasche
One thing that I do not have for the overview is the date that the syntax was added to each language. I think if you grouped them by that that the overwhelming trend would be that newer entries were more likely to be =>
. I have a real sense that this is eventually just going to end up being THE syntax for lambdas the way the syntax for string concatenation is just overwhelmingly and unquestionably +
. Even if my hunch is correct, that's not a strong argument for choosing the syntax but neither is it toothless.
I also did a brief unscientific survey earlier this year for what properties the syntax should have https://github.com/golang/go/issues/21498#issuecomment-1946975344 The =>
syntax has all the relevant popular (as of today) properties: omits func
, omits types, allows expression or block.
I recently skimmed the thread and didn't see a lot of backlash to =>
. There was not universal praise or preference for it. There were a lot of alternate designs. There were few people saying not to do that specifically that weren't also saying we shouldn't do anything in general. That's also heavily biased toward people who have read this thread and replied to it, which is not the majority of the community.
I doubt there will be any syntax that makes everyone happy, and I get the sense that most people would be satisfied with just having something. Maybe the viable designs should be put to a wider audience, perhaps as a survey where each alternate design collects additional info like how many years have you used other languages with this syntax, etc. It would be good to see the correlation between favorability and familiarity with these. If a lot of people like a design on paper but everyone who's actually used it strongly dislikes it, that would be as good to know!
Comment From: DeedleFake
I much prefer ->
to =>
. I think it reads 10 times better despite looking like such a small difference.
Comment From: jimmyfrasche
I have no preference aesthetically but would lean toward =>
due to it being more popular in other languages and clashing less with channel operations: -> <-
vs => <-
.
Comment From: jimmyfrasche
Fact checking myself somewhat (I didn't say anything wrong per se but there is some nuance):
- =>
is used in 8 langs
- ->
is used in 9 langs
- =>
is in 3 top 20 langs
- ->
in in 1 top 20 langs
- =>
is in 5 top 50 langs
- ->
is in 7 top 50 langs
- =>
is in 5 (args) => exprOrBlock
- ->
is in 3 (args) -> exprOrBlock
Comment From: griesemer
Writing something like (x, y) => x + y
does feel pretty natural. That said, single-expression function literals just don't seem that common in existing Go code. Maybe that's about the change but we don't know yet. Writing (x, y) => { ... }
does feel odd to me for Go because we don't use =>
to indicate a function, we use a keyword.
(As an aside, in the very early days, when we were discussing the syntax of function signatures, we were briefly considering an arrow to separate between incoming and outgoing parameters, but Ken, Rob, and I opted against it at that time. I believe the keyword was still there, irrespectively.)
One problem with using =>
over ->
is that something like this (x, y) => x <= y
or (x, y) => x >= y
(which I suspect might not be uncommon for filter functions) looks confusing. The single arrow reads better: (x, y) -> x <= y
or (x, y) -> x >= y
, respectively.
Comment From: griesemer
@hherman1 Yes, your suggestion is plausible, was prototyped, and applied to the standard library: CL 406076. See my comment from 2022.
The problem with any notation that doesn't use any form of bracketing around incoming parameters is readability: lightweight function literals will be passed as arguments to other functions. If their parameters are not bracketed, except for the func
keyword it looks like those parameters are additional arguments to the called function. I believe we need a form of bracketing to avoid that.
This has been discussed repeatedly in this thread.
Comment From: tmaxmax
I've implemented a parser for the func { params -> stmts }
and similar forms ("->"/"|" separator, always keeping "return" or sometimes eliding it) discussed above, and ran an experiment on the Go standard library code and my work codebase.
Here's a PR with the implementation: tmaxmax/go-1#1. In the PR I've detailed some additional relevant discoveries.
Here are PRs with results and statistics: - tmaxmax/go-1#3 - tmaxmax/go-1#4 - tmaxmax/go-1#5 - tmaxmax/go-1#6 - tmaxmax/go-1#7 - tmaxmax/go-1#8 - tmaxmax/go-1#9
The standard library has a size of 2231483 lines of Go code (including comments and blanks) as counted with:
$ find ./src -name '*.go' ! -path '*testdata*' ! -path './src/vendor*' | xargs -n 6000 wc -l
Here's a sample of the statistics I've collected for the standard library code:
found 7132 function literals, 4480 (62.8%) rewritten into lightweight form
1804 lightweight function literals (40.3%) are in test files
1852 lightweight function literals contain a single statement
(from which 333, 18.0% are long)
684 lightweight function literals contain a single return statement
(15.3% of all rewritten literals, 36.9% of single statement literals)
return value count histogram for single-return lightweight function literals:
[0 610 72 2 0 0 0 0 0 0]
By "long" function literal I mean a lightweight function literal containing a single statement long enough to make the function not fit nicely on a single line.
Here are the ways in which these observations may diverge from those made in the initial experiment:
- technically speaking the majority of lightweight functions are not in test files (percentage under 50%); the density of lightweight functions in tests is about 2.5 times greater than in normal code, though (3.8/kLoC vs 1.5/kLoC; read as "n occurences of lightweight functions over every one thousand lines of code")
- while out of all lightweight functions only 14.2% benefit from the elision of the return
keyword, it must be noted that the keyword was elided for 30.3% (percentages obtained using probabilities) of the functions which span a single line
For comparison, on my work codebase (a monolithic backend for a SaaS product), which amounts to 265785 lines of Go code using the same measurement, the following statistics are reported:
found 1805 function literals, 1642 (91.0%) rewritten into lightweight form
228 lightweight function literals (13.9%) are in test files
1094 lightweight function literals contain a single statement
(from which 90, 8.2% are long)
1058 lightweight function literals contain a single return statement
(64.4% of all rewritten literals, 96.7% of single statement literals)
return value count histogram for single-return lightweight function literals:
[0 1049 9 0 0 0 0 0 0 0]
Here are the significant ways in which the results from my work codebase diverge from those from the standard library: - ratio-wise 3 times more lightweight functions would be used (6.2/kLoC vs 2/kLoC) than in the standard library - 4.2 times more function literals have a single return (64.4% vs 15.3%) and the vast majority of them can be written on a single line (only 7.9% of single-return lightweight functions would still be broken into multiple lines) - 2.9 times less function literals are used in tests compared to the standard library (13.9% vs 40.3%) - density-wise across this codebase lightweight functions would be used 1.6 times more in normal code than in test code (6.7/kLoC vs 4.3/kLoC) - for reference, Go's codebase is 21.5% test code and this codebase is 20.2% test code - 91% of all literals could be lightweight, compared to 62.8% in the standard library - the case for lightweight functions is stronger here, as normal literals would be in the extreme minority, whereas in the standard library the situation is more nuanced - consider especially the fact that in the standard library usage of lightweight functions across non-test code is much sparser than in this codebase (1.5/kLoC vs 6.7/kLoC); it's much easier to justify lightweight functions when there is very frequent usage in normal code rather than relatively frequent usage in test code
Furthermore, out of all these single-statement function literals:
- 427 usages would benefit from return type deduction based on function body
- all of these cases are exclusively usages slicesx.Map
(392), set.FromFunc
(33) or iter.Map
(2) helpers
- these helpers do the same things but for different data structures: slices, map[T]struct{}
and an iterator implementation which predates Go's iterators used only for single-pass heavy inputs (so no slices converted to iterators)
- they are used to convert data at API boundaries (for example, DB to API models) or pick relevant properties only (for example, take only the IDs out of some input models, because some API requires only IDs).
- there are no "chained" usages in the codebase (stuff like slicesx.Filter(slicesx.Map(...) ...)
); we actively avoid writing that kind of code.
- all these usages are single-line, single-return, making up 42% of all single-line, single-return lightweight functions
- almost all other single-line usage is, in order of occurence:
1. slices.ContainsFunc
or an internal slicesx.Has
which predates the standard library counterpart
1. slices.SortFunc
or slices.SortStableFunc
1. lazy values/factory functions
- in my view all of these would benefit from eliding the return
keyword
- there is exactly 1 occurrence of a lightweight function which may have the danger to turn into the pathological case discussed above; in its current form deduction would succeed without surprises (only interfaces are returned)
I've started this experiment mainly to confirm my feeling that the trends observed in the standard library do not necessarily apply to all Go codebases. Numbers show this to be true. We should consider more datapoints in our decision.
And luckily I've hopefully made it easy enough to test that. To run the experiment on your codebase:
- checkout the funclight
branch of my repository – it is based on the 1.23 release so it should work with all current code
- cd src && ./make.bash
- use the gofmt
binary outputted to the bin
folder to format your codebase
We could also run it on some other relevant open source codebases and see the results.
Comment From: jimmyfrasche
@griesemer
Writing something like
(x, y) => x + y
does feel pretty natural. That said, single-expression function literals just don't seem that common in existing Go code. Maybe that's about the change but we don't know yet. Writing(x, y) => { ... }
does feel odd to me for Go because we don't use=>
to indicate a function, we use a keyword.
(x, y) => x + y
is also a function so I don't see how that's okay but the other is not? I've written plenty of code with =>
in javascript and the majority of it is the args => {}
variety. It's as fine a syntax as any.
You could require a func
prefix like func(args) => expr
or func(args) => block
. It's a bit redundant but it does let you know you're reading a func before you get to the =>
but otoh it makes it look like a regular func
until the =>
so you will expect types before you realize it's the other form. You could say that in (a, b, c, d) =>
you don't know that tuple is a parameter until you get to =>
which is fair but you get to =>
fairly quick and it's not like it looks like anything else particular in the meantime.
One problem with using
=>
over->
is that something like this(x, y) => x <= y
or(x, y) => x >= y
(which I suspect might not be uncommon for filter functions) looks confusing. The single arrow reads better:(x, y) -> x <= y
or(x, y) -> x >= y
, respectively.
c -> <-c
looks weirder to me than (x, y) => x <= y
but I'm used to =>
so that just reads as function of x and y returning x <= y
to me. I suppose c -> <-c
would be far, far less common, regardless. ->
is fine, too.
You could use ~>
if you wanna really stand out. :laughing:
Comment From: tmaxmax
Some other thoughts I've had:
That is an excellent point about Kotlin/Ruby/Smalltalk, though: the reason they take the arguments in the block is because they allow it to make regular functions look like syntax for DSLing, which is something that Go will presumably never do so it would be very strange to use that style without the promise of that corresponding feature.
@jimmyfrasche The conclusion doesn't necessary follow from the premise – just because other have done it doesn't mean we should also do it. What is important here is that we find a syntax which fits in the current Go code, and if this syntax fits the bill then it is a good choice.
About (x, y) -> exprOrBlock
syntax, I think it would look nice. I've never really had a problem with it. Though if we allow eliding the return
keyword then you'd have two more distinct forms:
(x, y) -> x + y
(x, y) -> (x + y, x - y)
Of course, unless we choose not to support expression lists and force people to use return
. This restriction seems arbitrary to me. The func { params -> exprOrStmts }
doesn't have this particularity.
If we decide to not implement return type inference based on body, extending the syntax with at least return types might be appropriate – otherwise there'll be no support for map-like helpers. Here is how I'd imagine it:
(x) string -> strconv.Itoa(x * x)
func { (x) string -> strconv.Itoa(x * x) }
(x) (string, error) -> strconv.Atoi(x)
func { (x) string, error -> strconv.Atoi(x) }
() (int, error) -> fmt.Println("Hi")
func { () int, error -> fmt.Println("Hi")
(x) string -> {
x *= x
return strconv.Itoa(x)
}
func { (x) string ->
x *= x
return strconv.Itoa(x)
}
A colon could also be used instead of brackets for the func { params -> ... }
syntax:
func { x: string -> strconv.Itoa(x) }
From this standpoint I'm not really sure which one's better.
As a final note, I've updated my experiment comment to fix some issues which skewed the results. I've also added some more numbers to create a better image of the results.
Comment From: entonio
The problem with any notation that doesn't use any form of bracketing around incoming parameters is readability: lightweight function literals will be passed as arguments to other functions.
I believe that a number of us are considering func
and ->
as the bracketing tokens.
Comment From: jimmyfrasche
@tmaxmax
The conclusion doesn't necessary follow from the premise – just because other have done it doesn't mean we should also do it.
My point is that if you copy a form fitted for a specific situation without copying that situation as well it seems out of place. They are two pieces designed to fit together so it's weird to have just one. Putting the parameters in the block is a solution to a problem we do not have.
About
(x, y) -> exprOrBlock
syntax, I think it would look nice. I've never really had a problem with it. Though if we allow eliding the return keyword then you'd have two more distinct forms:
Again, you don't have to do that. You can say that the expr syntax is only for the special case of a single expression. That's reasonable. It makes really simple cases very short and everything else comfortably short.
If we decide to not implement return type inference
If we're not doing inference the regular func
syntax is fine so there is no need to consider this.
Comment From: earthboundkid
You could use fat arrow and skinny arrow, with one being the form for return expression and the other being the form for statement block. The downside of that is moving from a single expression to a statement block is more typing, but it's very clear for readers.
Comment From: jimmyfrasche
That would require two tokens and remembering which is which.
Comment From: DeedleFake
I feel like we're retreading ground that was covered, at least in part, before the func { a, b -> expr }
and func { a, b; statements }
syntaxes were proposed and that they were designed specifically to address.
Comment From: jimmyfrasche
Straw poll for favored kind of syntax. This is just about general preference. Vote for as many as you like.
Assume that they all have equal expressive power and that any issues will be worked out.
- :rocket:
=>
or->
- :tada: arguments in the brackets like
{ a, b, c |
- :heart:
func a, b, c
- :eyes: something else
Comment From: tmaxmax
Reasons how I see func { params -> exprOrStmts }
being better:
1. Easiness to parse.
The following form can be parsed today, from my experience working with the Go parser, without adding any additional complexity to the code:
FuncLight = "func" "{" [ IdentifierList ] "->" [ Expression | StatementList ] "}" .
- Has
func
at the beginning, which was something people where very vocal towards in the past - Less annoying to work with:
- typing it the first time is less annoying
- for the
(params) -> expr | { stmts }
form you always have to jump over the closing bracket in order to type further. For a normal user this implies pressing the right arrow key, for a Vim user this implies going to Normal mode, pressing L and going back to Insert mode. - for the
func { params -> exprOrStmts }
you never have to do any cursor movements or switch between modes in modal editors – one press of Shift for the braces and you're good to go
- for the
- changing it is less annoying
- inserting new statements does not imply wrapping everything in braces
- removing all statements except one does not imply removing the braces (albeit
gofmt -s
could rewrite that)
- personal note: these are reasons why I've never really liked the JS syntax
- typing it the first time is less annoying
Here are various examples of both, picked up from the standard library:
nextToken := func { ->
cntNewline--
tok, _ := buf.ReadString('\n')
return strings.TrimRight(tok, "\n")
}
nextToken := () -> {
cntNewline--
tok, _ := buf.ReadString('\n')
return strings.TrimRight(tok, "\n")
}
slices.SortFunc(r.fileList, func { a, b -> fileEntryCompare(a.name, b.name) })
slices.SortFunc(r.fileList, (a, b) -> fileEntryCompare(a.name, b.name))
// assuming expression list return
compressors.Store(Store, Compressor(func { w -> &nopCloser{w}, nil }))
compressors.Store(Store, Compressor((w) -> (&nopCloser{w}, nil)))
// without
compressors.Store(Store, Compressor(func { w -> return &nopCloser{w}, nil }))
compressors.Store(Store, Compressor((w) -> { return &nopCloser{w}, nil }))
b.Run("same", func { b -> benchBytes(b, sizes, bmEqual(func { a, b -> Equal(a, a) })) })
b.Run("same", (b) -> benchBytes(b, sizes, bmEqual((a, b) -> Equal(a, a)))) // would this be allowed? `benchBytes` returns nothing
// or, on multiple lines
b.Run("same", func { b ->
benchBytes(b, sizes, bmEqual(func { a, b -> Equal(a, a) }))
})
b.Run("same", (b) -> {
benchBytes(b, sizes, bmEqual((a, b) -> Equal(a, a)))
})
removeTag = func { v -> v &^ (0xff << (64 - 8)) }
removeTag = (v) -> v &^ (0xff << (64 - 8))
forFieldList(fntype.Results, func { i, aname, atype -> resultCount++ })
forFieldList(fntype.Results, (i, aname, atype) -> { resultCount++ })
arch.SSAMarkMoves = func { s, b -> }
arch.SSAMarkMoves = (s, b) -> {}
return d.matchAndLog(bisect.Hash(pkg, fn), func { -> pkg + "." + fn }, note)
return d.matchAndLog(bisect.Hash(pkg, fn), () -> pkg + "." + fn, note)
ctxt.AllPos(pos, func { p -> stk = append(stk, format(p)) })
ctxt.AllPos(pos, (p) -> { stk = append(stk, format(p)) })
i := sort.Search(len(marks), func { i -> xposBefore(pos, marks[i].Pos) })
i := sort.Search(len(marks), (i) -> xposBefore(pos, marks[i].Pos))
sort.Slice(scope.vars, func { i, j -> scope.vars[i].expr < scope.vars[j].expr })
sort.Slice(scope.vars, (i, j) -> scope.vars[i].expr < scope.vars[j].expr)
queue = func { work -> workq <- work }
queue = (work) -> { workq <- work }
desc := func { -> describe(n) }
desc := () -> describe(n)
check := hd.MatchPkgFunc("bar", "0", func { -> "note" })
check := hd.MatchPkgFunc("bar", "0", () -> "note")
var unparen func(ir.Node) ir.Node
unparen = func { n ->
if paren, ok := n.(*ir.ParenExpr); ok {
n = paren.X
}
ir.EditChildren(n, unparen)
return n
}
var unparen func(ir.Node) ir.Node
unparen = (n) -> {
if paren, ok := n.(*ir.ParenExpr); ok {
n = paren.X
}
ir.EditChildren(n, unparen)
return n
}
var do func(Node) bool
do = func { x -> cond(x) || DoChildren(x, do) }
var do func(Node) bool
do = (x) -> cond(x) || DoChildren(x, do)
if withKey(func { key -> C._goboringcrypto_EVP_PKEY_set1_RSA(pkey, key) }) == 0 {
return pkey, ctx, fail("EVP_PKEY_set1_RSA")
}
if withKey((key) -> C._goboringcrypto_EVP_PKEY_set1_RSA(pkey, key)) == 0 {
return pkey, ctx, fail("EVP_PKEY_set1_RSA")
}
b.Run("2048", func { b -> benchmarkDecryptPKCS1v15(b, test2048Key) })
b.Run("3072", func { b -> benchmarkDecryptPKCS1v15(b, test3072Key) })
b.Run("4096", func { b -> benchmarkDecryptPKCS1v15(b, test4096Key) })
b.Run("2048", (b) -> benchmarkDecryptPKCS1v15(b, test2048Key))
b.Run("3072", (b) -> benchmarkDecryptPKCS1v15(b, test3072Key))
b.Run("4096", (b) -> benchmarkDecryptPKCS1v15(b, test4096Key))
slices.SortFunc(list, func { a, b -> bytealg.CompareString(a.Name(), b.Name()) })
slices.SortFunc(list, (a, b) -> bytealg.CompareString(a.Name(), b.Name()))
testMul("Mul64 intrinsic", func { x, y -> Mul64(x, y) }, a.x, a.y, a.hi, a.lo)
testMul("Mul64 intrinsic symmetric", func { x, y -> Mul64(x, y) }, a.y, a.x, a.hi, a.lo)
testDiv("Div64 intrinsic", func { hi, lo, y -> Div64(hi, lo, y) }, a.hi, a.lo+a.r, a.y, a.x, a.r)
testDiv("Div64 intrinsic symmetric", func { hi, lo, y -> Div64(hi, lo, y) }, a.hi, a.lo+a.r, a.x, a.y, a.r)
testMul("Mul64 intrinsic", (x, y) -> Mul64(x, y), a.x, a.y, a.hi, a.lo)
testMul("Mul64 intrinsic symmetric", (x, y) -> Mul64(x, y), a.y, a.x, a.hi, a.lo)
testDiv("Div64 intrinsic", (hi, lo, y) -> Div64(hi, lo, y), a.hi, a.lo+a.r, a.y, a.x, a.r)
testDiv("Div64 intrinsic symmetric", (hi, lo, y) -> Div64(hi, lo, y), a.hi, a.lo+a.r, a.x, a.y, a.r)
baseHandler := http.HandlerFunc(func { rw, req ->
fmt.Fprintf(rw, "basepath=%s\n", req.URL.Path)
fmt.Fprintf(rw, "remoteaddr=%s\n", req.RemoteAddr)
})
baseHandler := http.HandlerFunc((rw, req) -> {
fmt.Fprintf(rw, "basepath=%s\n", req.URL.Path)
fmt.Fprintf(rw, "remoteaddr=%s\n", req.RemoteAddr)
})
// no expression list return
req.GetBody = func { -> return NoBody, nil }
req.GetBody = () -> { return NoBody, nil }
// with expression list return
req.GetBody = func { -> NoBody, nil }
req.GetBody = () -> (NoBody, nil)
and so on.
Some neutral examples from my work codebase:
assetPaths := slicesx.Map(assets, func { a -> a.Path })
assetPaths := slicesx.Map(assets, (a) -> a.Path)
slicesx.Map(o.Features, func { f -> string(f) }) // an enum of some sort
slicesx.Map(o.Features, (f) -> string(f))
slicesx.Map(items[i:], func { it -> it.price.Mul(it.quantity).Round(2) })
slicesx.Map(items[i:], (it) -> it.price.Mul(it.quantity).Round(2))
slices.SortFunc(tags, func { i, j -> strings.Compare(i.key(), j.key()) })
slices.SortFunc(tags, (i, j) -> strings.Compare(i.key(), j.key()))
Comment From: jimmyfrasche
@txmaxmax
Easiness to parse
A good property indeed, and we shouldn't have anything that's hard to parse, but it's not top of the list.
Has
func
at the beginning, which was something people where very vocal towards in the past
Some people said that but I believe more people voted against it in https://github.com/golang/go/issues/21498#issuecomment-1946975344
I don't have a problem with involving func
personally but it's not a must. I've used =>
a lot in other langs and it's perfectly fine.
Less annoying to work with: typing it the first time is less annoying [...] changing it is less annoying
writability and editability are good properties, especially in a convenience syntax, but readabiltiy is still the main one. One of the reasons a short form is good is because by removing all the redundant information you focus on what's important. For a lot of the places I want short functions I get the regular function written out by my editor so it's not like it would really be saving me many key presses.
It's true if you have x -> x
and you want to change it to a block there's a small bit of typing to change it to x -> { fmt.Println(x); return x }
but the important bit is that the first is very clearly a simple expression while something more complicated is happening in the last one.
Most of the time expression lambdas are something like filter(x -> x < 0)
and block lambdas are stuff like t.Run(t -> { //...
so it's pretty clear which one you want upfront and it's unlikely to change often and even if it does change we're still not talking about a tremendous amount of physical effort .
I think any syntax that does not have separate forms is bad. It makes it harder to tell what is happening at a glace when it should be making it easier to tell what's happening at a glance.
If there is some fatal flaw in arrow functions the next best syntax is python/ocaml/sml/f# style, extended to have expression or block forms:
func x, y: expr
func x, y { block }
When you count func
as the opening bracket, that has all the desired properties. (:
could also be =
).
In some ways I think it's superior, but it's just not as common or as popular as arrow functions so there needs to be a real showstopping good reason not use arrow functions before anything else is considered.
Comment From: tmaxmax
One of the reasons a short form is good is because by removing all the redundant information you focus on what's important.
The func { -> }
syntax would make a further distinction redundant – whether the statement inside evaluates to something or not. When you see the code:
slices.SortFunc(s, func { a, b -> strings.Compare(a.Name, b.Name) })
the only thing that matters now is that you're sorting by the name of whatever values are there. If a lightweight function form which distinguishes between expression form and statement form were used, then suddenly the type the expression evaluates to, the type of the return value of the function become important. It is not important that the sort predicate returns int
– all I care is that I sort by name. The func { -> }
syntax provides a higher level of abstraction than the arrow syntax, in the same way type inference is a form of abstraction.
The point would be even further driven if that code were to look like:
slices.SortBy(s, { a, b -> strings.Compare(a.Name, b.Name) })
Of course, it will never look like this in Go (though the more I read the parser code, the more this looks possible to parse). And maybe that's the issue with this syntax – that in Go it will never be brought to its full potential. I'm not referring to the DSL-like stuff Kotlin and Swift do with having these literals outside of the call parens as a block; this looks like syntax abuse to me and I'd honestly never write code like this:
slices.SortBy(s) { a, b -> strings.Compare(a.Name, b.Name) }
I'm referring to the following:
slices.SortBy(s, { a, b ->
aName := normalize(a.Name)
bName := normalize(b.Name)
strings.Compare(aName, bName)
})
Implicit returns won't be a thing in Go, and without implicit returns the supposed additional abstraction capacity of the syntax is half-baked. If you think about it, without implicit returns this syntax still has two forms – when converting between one expression and multiple statements you'd have to add/remove the return
keyword. Which is basically the same as adding/removing braces but way easier to forget.
The annoyance I'd have with adding/removing braces would be the same when seeing yet again the compiler error "function returns () but call site expects (int)" or similar.
This one value proposition I keep vouching, on further thought, doesn't seem to stand.
I think any syntax that does not have separate forms is bad. It makes it harder to tell what is happening at a glace when it should be making it easier to tell what's happening at a glance.
I don't think this is bad in absolute terms, as you seemingly put it, for the reasons I've described above. I think the real issue here is that this sort of abstraction couldn't really be supported by Go. Introducing implicit returns just for lightweight functions is probably not a viable direction for the language.
The point I'm trying to make here is that the discussion is more nuanced than "single form is bad". I personally find a distinct elegance in these sorts of abstractions – for example, in the way an OCaml function is fully typed just by using the right operators. Go as a language took another direction, though, which implies rather a lack of abstraction in order to favour explicitness. Go tries to find a pragmatic middle ground between implicit and explicit, favouring the latter. For some people, for example, this feature would cross their acceptable threshold of implicitness – this is why there are people opposed to the feature altogether. It's also why arguments like "but you still use :=
every day" aren't persuasive – :=
doesn't cross their acceptable threshold of implicitness. This threshold is in the end subjective.
Philosophically at least I can agree that the arrow functions seem to be a better fit.
Now, purely from an aesthetic standpoint, if I compare the two syntaxes above it's really a give and take:
- In some scenarios I appreciate having the func
keyword: test functions, callbacks in longer parameter lists, in general function literals used in more imperative contexts
- In some other scenarios that func
stutters: namely in contexts of a http.HandlerFunc
, slices.SortFunc
etc., for those recursive functions where there's a func
just above, or for lazy values
- the Go libraries have made it a priority to signal where comes a func
and where it does not – having that func
again is just superfluous
- In most cases I like the separation the braces provide, clearly distinguishing the function body from the rest of the code
- the expression version of the arrow syntax has this habit of transforming the call site into a )
soup, given that it's not wrapping the body in any way; the braces do a very nice job at preventing that
- In some cases the braces are just noise – look at the simple mappings or sorts
- though the moment there's a function call in the predicate I start to prefer it, just so the parens don't stick together
- The func { -> }
syntax has the pathological case func { work -> workq <- work }
, which reminds me of the -->
"operator" from C++. No.
- The arrow functions kinda get lost in longer parameter lists, especially when they are short
They both have their strengths and weaknesses. What makes me lean towards the func { -> }
syntax is that it avoids the )
tails, which I personally dislike more than the stuttering func
s.
About the func a, b: expr
and func a, b { stmts }
syntax, it's been already established that they don't look good in parameter lists. Personally I dislike the Python syntax the most and the OCaml lambda function syntax is the only syntax from that language I find really odd and unfitting.
In the end, my conclusion would be that the arrow function does the job in a straightforward but mildly annoying way, with various inconveniences. The func { -> }
syntax, albeit more elegant looking, may indeed confuse with respect to the code meaning – whereas the arrow function, albeit sometimes requiring to disentangle chains of closing brackets, will never do that.
The arrow function is boring, familiar, does what it's asked to do and nothing more. Just like Go. And given public preference, maybe it's the way to go.
Relevant links so they don't get lost:
- my recent experiment with func { -> }
, analysis of usage in the standard library and a codebase of a SaaS product
- initial experiment with () => {}
and func a, b {}
, observations pertaining to usage in the standard library
- comparison of () -> {}
and func { -> }
- vote for preferred syntax
- older vote for preferred syntax
Comment From: DeedleFake
The point would be even further driven if that code were to look like:
go slices.SortBy(s, { a, b -> strings.Compare(a.Name, b.Name) })
Of course, it will never look like this in Go (though the more I read the parser code, the more this looks possible to parse).
I actually proposed this before: https://github.com/golang/go/issues/21498#issuecomment-885902408. My idea at the time for avoiding the ambiguity problems was to only allow it in function arguments, as there is currently nothing that starts with {
as the first token in that context. Although, thinking about it again, it would probably be fine in any expression context, which is all of the places that short functions make sense anyways.
Comment From: entonio
My reasons to favour the
func x, y -> expression
syntax:
- it goes from left to right without backtracking (anything involving parenthesis or brackets will result in IDE inserting the matching tokens but leaving the cursor after it, it could be different but it's what always happens;
- it requires only two symbols (the arrow) which are among the easiest to type in any keyboard layout (you don't begin to imagine the hoops non-US programmers have to go through to insert
{}
, of course we do it quickly and easily after a while, but it's never pleasant (that's also why I prefer->
over=>
); - visually it relies more on text and context than on symbols, which makes it more go-like in my book;
- it's distinct from the syntax that takes a block, which to me is an advantage of readability and even structural discipline (as I feel that the two kinds answer distinct problems, and if in some piece of code the single expression syntax isn't applicable maybe it's a sign that the structure of that piece of code should be different); (this is the single most important point to me)
- I find the
func
keyword, balanced with the->
, establish the mental switch to 'this is a function' better than the more polysemic()
, which moreover will often come after another(
.
Some of the above are specific to how I expect go code to read, others are more general.
As to the disadvantages that have been mentioned, my opinion is that they're hypothetical rather than practical.
We can do polls, but those will only represent the people in this thread who happen to see the specific poll. I think the solution should be justified not only on popularity, but also on go-ish-ness.
Comment From: jimmyfrasche
@tmaxmax
The
func { -> }
syntax would make a further distinction redundant – whether the statement inside evaluates to something or not.
That is not redundant information. It may not always matter or be important but when it does it is.
About the
func a, b: expr
andfunc a, b { stmts }
syntax, it's been already established that they don't look good in parameter lists.
Some people have stated that and it's certainly a -1 and moves it down the list but it's not a fatal flaw imo.
It is a bit inaesthetic but it's fine when you get used to it. It only looks wrong if you don't understand the syntax and are parsing it wrong in your head, but if you're looking at code and you don't understand the syntax you'd then need to look that up so the problem solves itself.
No one likes Python's lambda but the reason isn't because of the commas in f(x, lambda a, b: a*y + b)
. Anyone familiar reads that as the application of f
taking two arguments one of which is a lambda of two arguments. It doesn't require further bracketing. Python's lambda is disliked because the RHS can only be an expression.
Comment From: tmaxmax
@jimmyfrasche
That is not redundant information. It may not always matter or be important but when it does it is.
This feels dismissive of everything else I've written. I've even argued in that text against hiding this information in the context of Go.
Some people have stated that and it's certainly a -1 and moves it down the list but it's not a fatal flaw imo.
The other forms don't have this flaw, so why should we accept it?
Python's lambda is disliked because the RHS can only be an expression.
I also dislike it for the unparenthesised parameter list. Some others do. "Getting used to it" is not really an argument, as one can get used to anything if it is required.
@entonio Ignoring that this syntax or similar has been discussed already, taking each of the points you make in order:
1. that's not my experience: in any editor I've used the cursor was always placed inside the symbol pair; i'll agree it's easier to type anyway
2. what do you do with the block form? You still need {}
for that. Is this a reaction only to the func { -> }
syntax? How about a comparison with the arrow syntax, which is even more relevant than that one?
3. http.HandleFunc(func w, r -> ...)
why have func
twice? It depends on the context whether it helps or not, seems again dismissive of my previous comment, where I've addressed the usefulness of having the keyword or not
4. how's it different in this regard to the arrow function syntax? Again seems dismissive of my previous comment and of general sentiment – neither I am (anymore), nor the general public is strongly advocating for func { -> }
but for the arrow syntax
5. again – http.HandleFunc(func w, r -> ...)
– there are quite a few places where the additional func
stutters and the mental switch is already made through the assignment scope's name
- on the left side you can also never have more than two required (
; the right side is the issue, and neither the arrow syntax, nor this syntax you propose solve that
my opinion is that they're hypothetical rather than practical.
And your opinion is based on what? Have you seen the code? Do you like the following:
f.walk(arg, ctxExpr, func f, arg, context { ... })
forFieldList(fntype.Results, func i, aname, atype -> argField(atype, "r%d", i))
testDiv("Div64 intrinsic", func hi, lo, y -> Div64(hi, lo, y), a.hi, a.lo+a.r, a.y, a.x, a.r)
slices.SortFunc(list, func a, b -> bytealg.CompareString(a.Name(), b.Name()))
Arrow functions at least keep things cohesive:
f.walk(arg, ctxExpr, (f, arg, context) -> { ... })
forFieldList(fntype.Results, (i, aname, atype) -> argField(atype, "r%d", i))
testDiv("Div64 intrinsic", (hi, lo, y) -> Div64(hi, lo, y), a.hi, a.lo+a.r, a.y, a.x, a.r)
slices.SortFunc(list, (a, b) -> bytealg.CompareString(a.Name(), b.Name()))
but also on go-ish-ness
What is "go-ish-ness"? This whole thread is sprinkled with debates over that.
I get it, some like syntax A, some like syntax B, and they really want to see them live. But this isn't the way. At least have the courtesy to take in consideration previous arguments, try as much as possible to base your opinions on something concrete and to accommodate others' critiques or concerns in your proposals. There's no hope for consensus otherwise.
Comment From: skasti
Please, just pick a style and implement it.
If someone just picked a style 7-ish years ago, we would either have been used to it, or it could have been revised many times by now. You don't always have to please everyone the first time 😅
Comment From: ianlancetaylor
@skasti In Go we aim to be very strict about backward compatibility, in order to provide a stable platform for developers. See https://go.dev/doc/go1compat. So we aren't going to introduce a syntax and then revise it. We don't have to please everyone the first time, but any change we make does have to be good enough the first time.
Comment From: jonathansharman
@ianlancetaylor Totally - whatever design is decided here, Go will be stuck with it probably forever. I would like to echo @skasti's sentiment though. Even though this feature is "just" syntactic sugar, I think it's almost a prerequisite for the Go community to begin fully experimenting with Go's functional features, in particular with iterators. For instance, it's difficult for https://github.com/golang/go/issues/61898 to advance because it's hard to get practical experience to help guide the design since passing closures today is so painful.
As skasti's mentioned, this issue is over seven years old now, with hundreds of comments. Respectfully, I hope that relatively minor disagreements over alternatives that are ~99% as good as each other won't delay acceptance of some solution for an extra release cycle or two. 🤞
Comment From: Merovius
@jonathansharman
I think it's almost a prerequisite for the Go community to begin fully experimenting with Go's functional features, in particular with iterators.
This might be nitpicking, but I object to the characterization of iterators as a "functional feature". They are, as the name implies, a way to standardize iteration. In particular, to enable user-defined container types. That you can also use them to write higher-level functions is true, but I would argue that because we don't have lightweight function literals or generic methods, that is not the primary use case for iterators. In particular, I'll note that #61898 could have been implemented in the past as well and the actual "functional code" using those APIs would have looked exactly the same, whether or not you can then range
over the result or not.
And - in my opinion - that is a feature, not a bug. To me, that kind of functional pipeline code is harder to read and write and I don't want Go to become the kind of language where its idiomatic to do so. I acknowledge, that this is a personal opinion, though. And that, over the long term, that's likely something I will just have to get used to accepting.
Just wanted to make clear that iterators are very useful on their own. And that, while I agree with your premise (that writing higher-level functions in the language as-is is not very useful) I disagree that that's a bad thing.
Comment From: skasti
@skasti In Go we aim to be very strict about backward compatibility, in order to provide a stable platform for developers. See https://go.dev/doc/go1compat. So we aren't going to introduce a syntax and then revise it. We don't have to please everyone the first time, but any change we make does have to be good enough the first time.
This could "easily" have been introduced in backwards compatible manner. the "normal" syntax
func (arg ArgType) ReturnType { return arg.SomeType }
could be expanded to not require the types when they can be inferred;
func (arg) { return arg.Something }
This is still backwards compatible. And to be honst, I would not even have bothered being frustrated if this was possible. This is "good enough" to reduce 99% of my frustrations
then making func optional if can be done (what, if any, character is used between params and body is not important. people are able to learn)
(arg) -> { return arg.SomeThing }
if, for some reason, there is a huge revolt against using ->
, support for =>
can be added. still backwards compatible.
All of this can be done in small steps, and adding more options does not break backwards compatibility? 🤔
Comment From: aarzilli
@skasti What you are proposing was discussed before and is not backwards compatible func (arg) { ... }
is already valid Go where arg
is interpreted as a type.
@jonathansharman
For instance, it's difficult for https://github.com/golang/go/issues/61898 to advance because it's hard to get practical experience to help guide the design since passing closures today is so painful.
For #61898 to advance you would need this, then solve the problem with return type inference with Map
(which no one is even discussing) and then a way to chain function calls.
Comment From: jonathansharman
@Merovius Yes, iterators are somewhat useful on their own, even if they aren't composable. However, based on my experience with other modern programming languages, much of their power comes from composition; hence my choice of words: we aren't yet "fully experimenting" with what's possible. I also acknowledge that short lambda syntax is not the only missing piece to make iterator composition as useful as it could be (generic methods are arguably even more important), but one proposal at a time. 🙂
the actual "functional code" using those APIs would have looked exactly the same
Purely functional code, yes, but it's pretty common to mix and match iterator composition with imperative iteration, e.g. iterating over a filtered set of values. And certainly the existence of a standard iterator interface that collection types would be implementing anyway makes the library more compelling.
We'll have to agree to disagree on the ease of writing and reading iterator chains, as it's a matter of taste. There is clearly some appetite for it in the ecosystem though.
That said, putting iterators aside, there are many other use cases for functional parameters (they've long been used in sort
's API, for instance), and I think the point stands that it would be a shame for this proposal to languish too much longer from bikeshedding, especially when a lot of the disagreements seem to be going in circles. Let's not let the perfect be the enemy of the already very good.
Comment From: aarzilli
The fact that most people that want lightweight anonymous functions want it for higher lever iteration actually speaks against the feature, because of the several obstacles to higher level iteration that have no clear path forward.
Comment From: mibk
I still feel it might be a good idea to make it consistent with the short variable declaration syntax. In both cases, the keyword (var/func) and the types would be omitted.
The syntax could look like:
func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}
var _ = compute((a, b) := { return a + b })
// …
slices.SortFunc(s, (a, b) := { return strings.Compare(a.Name, b.Name) })
The :=
is already part of Go. By requiring braces and the return keyword, we can eliminate any visual ambiguity. Also, I strongly prefer using parentheses for the argument list. That’s why I never liked the func a, b, c { … }
syntax; it feels more like a hack.
Comment From: skasti
@skasti What you are proposing was discussed before and is not backwards compatible
func (arg) { ... }
is already valid Go wherearg
is interpreted as a type.
Sorry, I was not aware 🙈 😞 I have never seen this in use; how do you use an argument when it is just a type without an identifier?
Edit; oh no... is this why it is not possible to use _
to ignore an argument I don't need in my implementation of an interface? That the correct way is to just write the type without an identifier, unlike when ignoring a return-value I don't care about? 😅 🙈
Comment From: aarzilli
I have never seen this in use;
It's typically used in type definitions: https://github.com/golang/go/blob/28f4e14ebe281d8e46cba430bfd123ce21fcd0cc/src/iter/iter.go#L203
how do you use an argument when it is just a type without an identifier?
You don't.
Comment From: tmaxmax
@aarzilli
The fact that most people that want lightweight anonymous functions want it for higher lever iteration actually speaks against the feature
Some higher level iteration is actually useful: sorting, searching, sometimes mapping/filtering, partitioning, checking that a predicate applies to everything etc. I think introducing the possibility to make higher level iteration chains is problematic and goes against Go.
Let's take a look at the following:
mapped := make([]T, 0, len(values))
for _, v := range values {
m := someExpr(v)
mapped = append(mapped, m)
}
mapped := xslices.Map(values, (v) -> someExpr(v))
for i := range values {
for j := i; j > 0 && !lessExpr(values[j-1], values[j]); j-- {
values[j], values[j-1] = values[j-1], values[j]
}
}
slices.SortFunc(values, (a, b) -> lessExpr(a, b))
var filtered []T
for _, v := range values {
if condExpr(v) {
filtered = append(filtered, v)
}
}
filtered := xslices.Filter(values, (v) -> condExpr(v))
When you only need to do one of these and exactly one of these, it's way better to have the option to use some higher level iteration utility. In the experiment I've brought some data to prove that in at least some codebases this happens often enough that a lightweight function syntax would be a significant improvement.
I do agree that iteration chains should not find their way into Go. They honestly feel like a trend, the same way OOP was at some point, and retrofitting it in a language not built with such paradigms in mind is bound to be a disaster.
On a very personal note: having lightweight functions wouldn't encourage me to make iteration chains and wouldn't change my coding style in any way – I'm already using functions literals everywhere I find them necessary, I've never avoided them because of the syntax.
For this reason I'd rarely ever see myself using the proposed xiter
package, as most of my code with it would look like:
mapped := slices.Collect(xiter.Map((v) -> expr(v), slices.Values(values))
which sucks. I already have a slicesx.Map
helper in the codebase for that. It would be useful for other sort of iterators – lines in CSV documents is an example from my work codebase (in this light I'll voice my agreement for what @Merovius stated above). But for that I'd also not make chains, as the line processing is usually much more involved than what some higher order iteration helpers could comfortably describe. I'd most certainly use for
and a Map
or a Filter
here and there.
then solve the problem with return type inference
We've discussed this ourselves at some point above so I'll leave the links to the relevant comments here for reference: 1. https://github.com/golang/go/issues/21498#issuecomment-2453571667 1. https://github.com/golang/go/issues/21498#issuecomment-2453626361 1. https://github.com/golang/go/issues/21498#issuecomment-2453926037 1. https://github.com/golang/go/issues/21498#issuecomment-2454274943 1. https://github.com/golang/go/issues/21498#issuecomment-2454442564 1. https://github.com/golang/go/issues/21498#issuecomment-2454506617 1. https://github.com/golang/go/issues/21498#issuecomment-2454641468
@mibk I don't think further syntax bikeshedding will bring us much value. It seems that the least controversial form is (args) -> expr
and (args) -> { stmts }
– I think at this point we'd drive the discussion further towards a decision if we'd tackle semantics and implementation details (like type inference).
Plus, every sensible syntax has been discussed. If anyone wants to propose a new syntax I think they should make their due dilligence to read the thread and see whether anything has really been missed.
Comment From: skasti
@aarzilli Thanks for the clarification :D
I hope that people realize that I am just frustrated and want the functionality, and that I do not have very strong preferences regarding the syntax. I was wrong about my examples, as I do not have deep enough knowledge about the language, but I hope that the intent was conveyed at least 😅
Comment From: Roccoriu
@aarzilli Thanks for the clarification :D
I hope that people realize that I am just frustrated and want the functionality, and that I do not have very strong preferences regarding the syntax. I was wrong about my examples, as I do not have deep enough knowledge about the language, but I hope that the intent was conveyed at least 😅
Yeah I agree. I think the syntax does not have to even be anything new. I think it's enough if we can maintain the same overall syntax and just omit the type declarations.
func compute(fn func(float64, float64) float64) float64 {
return fn(3, 4)
}
var _ = compute(func(a, b) { return a + b })
// …
slices.SortFunc(s, func(a, b) { return strings.Compare(a.Name, b.Name) })
Comment From: griesemer
@Roccoriu As has been pointed out before - more than once, I think - just leaving away the types doesn't work because it is ambiguous - otherwise we'd have done it long ago. The compiler cannot know if the names are parameter names or type names (with anonymous parameters). There needs to be another symbol.
Comment From: mpx
There needs to be another symbol.
What if the change was as simple as different leading symbol + no types? I don't recall (and can't find) this above. Eg:
slices.SortFunc(items, lambda (a, b) { return a < b })
slices.SortFunc(items, fn (a, b) { return a < b }) // "fn" is a short "func" :)
slices.SortFunc(items, #(a,b) { return a < b }) // New keywords are hard, maybe a symbol? Also shorter
These are fairly close to a regular function definition, so less to learn and similar to read in shape.
At this point I'd be very happy with some form of simple/concise syntax, mostly to omit types when they don't add value and detract from readability - making the logic more obvious.
Separately, I've found the earlier examples based around the arrow syntax less readable -- jarring since they aren't an actual comparison. My eyes give ->
/=>
the same "weight" as :=
which hurts readability for me. Perhaps this would improve with familiarity over time.
@griesemer 's func { a, b | return a + b }
syntax has also grown on me too. I still prefer the more familiar shape and different leading keyword of the examples above tho'.
Comment From: entonio
@Roccoriu As has been pointed out before - more than once, I think - just leaving away the types doesn't work because it is ambiguous - otherwise we'd have done it long ago. The compiler cannot know if the names are parameter names or type names (with anonymous parameters). There needs to be another symbol.
I don't think there's ambiguity, as that case is a type literal and what we're discussing here is a function literal, and those don't occur in the same contexts.
Generally speaking, in this discussion we've had a number of objections to this or that syntax on the basis of 'the compiler can't handle it', when a quick glance at other languages will show that their compilers don't have such problems. That's not to say that there aren't potential issues, or that go's existing syntax doesn't have its specificities, but I'd wait for the core go team to weigh in on that rather than taking it at face value.
Regarding any new proposals, or whether this discussion is going anywhere or not, I do feel there is already a number of solutions each of which 'works'. I also don't think it's a matter of polling, because the people eventually paying attention to polls in this thread aren't representative. I do feel it's up to the go core team to vet the options, but my impression is that most of them are very wary of having a simplified function syntax in the first place, and it's their prerogative and informed opinion.
Comment From: Merovius
@entonio func(a, b) {}
is valid syntax in value-context (assuming a
and b
are defined).
Comment From: Merovius
Generally speaking, in this discussion we've had a number of objections to this or that syntax on the basis of 'the compiler can't handle it', when a quick glance at other languages will show that their compilers don't have such problems.
Other languages have other grammars and are implemented using other design restrictions. The first means, that there can be different semantic ambiguities. The second means, that other languages can implement some things, that Go doesn't. For example, Go made the conscious decision not to require back-tracking, which is why optional semicolons are implemented as a lexer-rule, instead of a grammar feature (as it is, for example, in Javascript).
Comment From: avamsi
I don't think further syntax bikeshedding will bring us much value. It seems that the least controversial form is
(args) -> expr
and(args) -> { stmts }
– I think at this point we'd drive the discussion further towards a decision if we'd tackle semantics and implementation details (like type inference).
@griesemer @ianlancetaylor (and any other stakeholders), could I ask for your thoughts on @tmaxmax’s https://github.com/golang/go/issues/21498#issuecomment-2491556457? I was hoping it would gain some traction, but oh well. From reading past comments, @griesemer seems to be leaning positive, while @ianlancetaylor appears neutral to slightly negative, citing concerns about it not being Go-like. While this issue has been open since 2017, I think it could really help now with iterators and #71203.
Comment From: ianlancetaylor
My recollection is that the experiment that @griesemer did in https://github.com/golang/go/issues/21498#issuecomment-1132271548 shows that the approach suggested at the end of https://github.com/golang/go/issues/21498#issuecomment-2491556457 doesn't seem to work all that well in practice. There are certainly cases where it is fine, but there are others where it seems too obscure.
I don't think we have any clear consensus here.
Comment From: tmaxmax
@ianlancetaylor What do you refer to when you point to the "approach" suggested in my comment? Do you mean the syntax choice, i.e. the arrow syntax? If that's the case, I might miss something but @griesemer states in that experiment that:
The arrow style notation feels surprisingly readable to me. It is also familiar to people coming from other languages.
He pushes back on the unparenthesized param list syntax, not on the arrow syntax. Nothing from the observations made in that particular experiment seem to indicate in my view that this syntax "doesn't seem to work all that well in practice".
If syntax is not what you're referring to or there is something I'm missing do feel free to clarify.
Comment From: ianlancetaylor
Yes, that is what I am referring to. I am disagreeing with what @griesemer wrote back then. I am not convinced that the arrow syntax is the least controversial form.
I did find your discussion starting at https://github.com/golang/go/issues/21498#issuecomment-2445580267 to be quite helpful.
Comment From: jimmyfrasche
What is the issue with the arrow syntax?
Comment From: ianlancetaylor
As far as I know, the arrow syntax works fine. I'm just not particularly happy with it.
For example, when I look at code like https://go-review.googlesource.com/c/go/+/406395/2/src/cmd/cgo/gcc.go I see a list followed by another list. It's not really clear what I am looking at until I get farther along to the =>
.
I think there are a number of other comments in this issue in which people express some discomfort with the arrow notation.
I don't have any clearly convincing arguments here. I don't think anybody does. That's why this issue remains open.
Comment From: jimmyfrasche
That is kind of a fair objection but it's a binary operator so that applies to other binary operators so it's kind of like saying + is bad since "4 + 5" doesn't tell you that you're adding until after the 4. And, on the line given, (f,
isn't valid in that context unless it's the arg list to an arrow func so the clue is a bit earlier and earlier still if you're familiar with the signature of the method being called and know that it takes a function.
I didn't especially care for arrow syntax the first time I saw it but any objections I had went away after using it a few times. You get used to it very quickly.
A lot of people are already used to it given it's use in many popular languages so I think at this point using anything other than arrow syntax would need a pretty strong argument for any language.
Comment From: tmaxmax
Indeed the arrow syntax seems to have some rough edges. On the other hand it's the only one that causes the least intense objections. We've proposed here quite a lot of forms, each trying to fix one issue or another. The arrow syntax is the least offensive.
People seem to desire a syntax which:
- doesn't use too "weird" new symbols or symbol combinations (so no stuff like ->(a, b) { ... }
, \a b -> ...
etc.)
- differentiates between expression form and block form (so not like I initially proposed, func { a, b -> ... }
)
- doesn't mix parameter list and body together (again, not like I proposed)
- is easily identifiable in code
The arrow syntax ticks the boxes almost fully. The only remaining problems that I see would be: - in some cases hard to distinguish in code (what @ianlancetaylor exemplifies above) - the close parenthesis chain in nested function calls (at least I don't like it – I don't think anyone else has mentioned this):
// example taken from stdlib
b.Run("same", (b) -> {
benchBytes(b, sizes, bmEqual((a, b) -> Equal(a, a))) // this chain here
})
b.Run("same", (b) -> benchBytes(b, sizes, bmEqual((a, b) -> Equal(a, a)))) // worse when written as one-liner
This second problem is a problem only with the expression form and it's also not really specific to the arrow syntax – it's just how nested function calls look. The syntaxes which keep the expression inside some tokens (for example, func { ... }
) somewhat solve this by breaking the )
chain but these syntaxes have received enough backlash.
The only further optimization I can see is maybe shamelessly stealing the Rust syntax:
f.walk(arg, ctxExpr, |f, arg, context| {
px, ok := arg.(*ast.Expr)
if !ok {
return
}
// ...
}
It should be more easily distinguishable, given that a list enclosed by |
can only be a lightweight function parameter list, so I believe it solves that issue (the way it looks above should be pretty clear, I believe, both for those that would have a problem with the normal parenthesized list and for those that wouldn't have). It doesn't solve the closing parens chain issue because the expression form doesn't enclose the expression in something:
b.Run("same", |b| benchBytes(b, sizes, bmEqual(|a, b| Equal(a, a))))
but, again, that's not an issue of the lightweight function syntax itself.
Trying to push for the Rust syntax over the arrow syntax honestly does feel like a micro-optimization, all while most probably having a greater refusal rate from the community. Unless I'm wrong about my assumptions or – by means of divine intervention – we manage to come up with a totally new syntax which solves everything and is liked by everyone, the arrow syntax remains the best candidate. It seems like no one's favourite but everyone's favourite.
I think we should not lose sight of the fact that this feature is not something purely syntactical. It has a motivation to exist and must come with some semantics. Maybe we should spend some time debating: - whether the motivation to have lightweight functions is right - what would be the right semantics of lightweight functions
Both were discussed, albeit nowhere close as exhaustively as we've discussed syntax. I believe that if we reach a consensus on the fact that lightweight functions are right for Go and we find some fitting semantics, the 80% in terms of syntax that we've achieved here would be more than acceptable. Regardless of syntax, motivation and semantics would have to be the same. Plus, both semantics and motivation have an effect on syntax – so discussing those might further clarify how we want lightweight functions to look like.
My proposal for this proposal would be to preemptively settle on the arrow syntax, temporarily close the syntax discussion and dive deep into the other subjects. So basically: "for the sake of the argument, we assume that () -> expr
, () -> { stmts }
is right. why do we want it and how does it work?" The philosophy behind this is that we are trying to make an argument for lightweight functions; we can neither prove nor disprove that some syntactic form is right, but we might be able to prove or disprove that either they are/aren't needed or they can/can't work inside of Go. If we prove they are not needed or that they can't work the syntax discussion is useless.
Based on previous discussions, syntax and motivation are pretty subjective. Semantics – mostly type inference – seem to have the highest degree of objectivity, given the clear technical constraints. So: - motivation: subjective - syntax: subjective - semantics: (probably) objective
I think we'd have the greatest chance to advance this proposal by discussing the latter. If we have no good semantics, we can close the proposal; if we have good semantics, then we can clear out the other topics.
Comment From: DmitriyMV
@ianlancetaylor if we combine this proposal with ~~#71203~~ #71460 and the change that suggested that ?
always diverge control flow it means that you can write code like this:
io.Copy(os.Stdout, rdr) ? (err) => { return fmt.Errorf("copy to stdout failed: %w", err) }
or if short form is allowed:
io.Copy(os.Stdout, rdr) ? (err) => fmt.Errorf("copy to stdout failed: %w", err)
Which removes all magic from the ~~#71203~~ #71460 and still look concise.
Comment From: thepudds
if we combine this proposal with https://github.com/golang/go/issues/71203
FWIW, see the suggestion from @jimmyfrasche in https://github.com/golang/go/issues/71203#issuecomment-2581308614, a sample recent reply from Ian in https://github.com/golang/go/issues/71203#issuecomment-2608252905, or expand the discussion there and Control-F for jimmyfrasche.
Comment From: DmitriyMV
@thepudds I know that, I just trying to show how the arrow syntax can interact with other features.
Comment From: thepudds
That makes sense.
Partly I was trying to give landing spots for people who might be following this issue here but not up to speed on the related discussion in #71203 (especially since both issues now have hundreds of comments), and #71203 was mentioned here a few days ago in https://github.com/golang/go/issues/21498#issuecomment-2613815119.
Comment From: doggedOwl
The arrow syntax while new in go it's really familiar from other languanges, java, kotlin, swift, javascript all use a variation of either thin arrow or fat arrow (-> / =>) . so even in the case reported in comment https://github.com/golang/go/issues/21498#issuecomment-2616935621 as unclear, to me it is better than the original but admitedly because I am used to scan for that type of anonymous function.
Comment From: gophun
Go already uses arrows for channel sends, and fat arrows closely resemble the less-than-or-equal sign. Please avoid introducing additional arrow symbols for an entirely different purpose. Just because other languages have copied each other doesn't necessarily make it a good choice. If a lightweight form is deemed necessary (which is not entirely clear), consider using syntax that aligns with or is related to the non-short form—ideally incorporating the func
keyword in some way.
Comment From: DeedleFake
@gophun
Having used multiple languages that have unrelated <-
and ->
operators and/or >=
and =>
operators, I have never once seen anyone get confused about them. I don't think that particular syntactic choice is a problem, especially if the syntax involves the func
keyword, which most people who are in favor of the proposal seem to want.
Comment From: TDMR87
Just voicing one opinion here: I'm currently learning Go (and loving it) but was a bit bummed by the lack of syntactic sugar regarding anonymous functions.
Instead of this:
var evenNumbers = Filter(numbers, func(num int) bool { return num % 2 == 0})
I'd like to write something like this:
var evenNumbers = Filter(numbers, (num) => num % 2 == 0)
or this:
var evenNumbers = Filter(numbers, num => num % 2 == 0)
Comment From: vennarshulytz
You can try Go Arrow Functions Plugin for IntelliJ IDEA & GoLand | JetBrains Marketplace
Comment From: griesemer
Below is an AI-generated summary of the discussions so far. I have very roughly skimmed the entire discussion and believe the summary is decent, but it lacks (not surprisingly) a conclusion that hints at a particular direction to take. I have not verified the list of syntax ideas at the end but it looks plausible.
[Start of AI summary]
Based on the extensive discussion, a clear and overwhelming consensus has not yet arisen regarding a single preferred syntax for lightweight anonymous functions in Go. The discussion is ongoing and has been for several years.
However, some trends and preferences can be observed:
Arguments Against a New Syntax / For Status Quo:
- Many participants express a strong desire to "keep Go simple" and avoid introducing multiple ways to achieve the same thing, emphasizing Go's current clarity and readability. Some argue that the savings are minimal and not worth the added complexity or potential for reduced readability, especially when types are not explicitly visible.
Key Debate Points:
- Presence of func keyword: Some prefer to keep func for clarity and consistency with existing Go syntax.
Open Questions and Challenges:
- Backward Compatibility: A significant concern is ensuring any new syntax does not break existing Go code.
- Implicit Return: There's a strong divide on whether the return keyword should be implicitly omitted for single-expression bodies. Some find it cleaner. Others believe it complicates Go's explicit error handling and can be confusing.
- Parameter Parentheses: There's a strong preference for keeping parentheses around parameters to clearly delineate them and avoid ambiguity, especially when passing the anonymous function as an argument to another function.
Current Status: The Go team acknowledges the utility of type inference in function literals, particularly with the advent of generics.
- Ambiguity: Several proposed syntaxes face challenges with parsing ambiguity, especially when trying to reuse existing Go syntax elements or when types are omitted.
In conclusion, while there's a recognized desire for a more lightweight anonymous function syntax in Go, especially with the introduction of generics, there is no single syntax that has gained widespread, unambiguous support. The Go team's approach is to find a "good enough" solution that maintains Go's core principles of clarity, readability, and backward compatibility, rather than rushing to adopt a syntax that may prove problematic in the long run. The discussion seems to be circling around finding a syntax that is both concise and clearly Go-like.
The proposal has generated numerous syntax ideas, which can be grouped based on their structural similarities:
1. Arrow Function Style: These syntaxes typically use an arrow-like operator to separate parameters from the function body and often allow for implicit returns for single expressions.
- (x, y) => { x + y }
- (x,y) => x+y
- x, y { x + y } (Rust-like with types elided)
- |x:f64,y:f64| -> f64{x+y} (Rust-like)
- (a,b) => a+b
- (a,b) => { ... } (with a block)
- item -> item+1
- (item) -> item+3
- item -> item%2 == 0
- func(x int) => x * 2
- a => a.x
- (num) => num % 2 == 0
- num => num % 2 == 0
- fn (a, b) { return a < b }
-
(a,b)
2. func Keyword with Variations: These options retain the func keyword but modify the parameter list or introduce new separators to enable type inference or a more concise form.
- func a, b { a+b } (parameters without parentheses)
- func(a,b): a+b (colon as separator)
- func(x, y): { return x + y } (colon as separator with block)
- func(x, y): x + y (colon as separator with expression)
- func a, b { return a+b }
- func (p) error { return p.SetData([] byte ("Hello, ")) } (elided type for parameter p)
- func(p_) { return p.SetData([] byte("Hello, ")) } (underscore for inferred type)
- func a, b { a+b }
- func x, y { ... } (without parentheses, experimented by griesemer)
- func x,y,z { ... } (func style)
- func a, b { return a+b }
- func |a,b| { return a==b } (parameters in vertical bars)
- func { x, y -> x + y }
- func { x, y -> ... } (multi-line)
- func { -> 1 + 2 } (no parameters, returns expression)
- func { _ -> } (single inferred parameter, no return)
- func { x, y | return x < y } (vertical bar as separator)
- func { x, y -> return x < y } (arrow as separator)
- func { |a, b| -> a + b } (parameters in vertical bars, arrow as separator)
- func { |a, b| return a + b } (parameters in vertical bars, return keyword)
- func(a, b _) _ { return a + b } (underscore for type inference)
3. Implicit Parameter/Return Type Inference: This category focuses on the idea of the compiler inferring types from context, often without significant changes to the existing func syntax.
Allowing the function literals to elide unambiguous types.
- func(a,b): a+b (colon indicating type inference)
- func(a,b) { a+b } (with parameter types and implicit return)
- func a, b { a+b } (parameter names only, types inferred)
- func(a, b) { return a+b } (types omitted, inferred from context)
- func(t expr.Expression) result.Result[value.Value] { return e.eval(ctx, t) } (where t's type is inferred)
- func(r result.Result[*value.Value]) bool { return r.IsOk() } (where r's type is inferred)
- func(yield func(int) bool) { ... } (where yield's type is inferred)
- func(a, b _) { return a.Size - b.Size } (underscore as a placeholder for inferred types)
4. Alternative Keywords/Symbols: Suggestions that propose replacing or augmenting the func keyword with a new symbol or a shorter keyword.
- lambda(p) { return p.SetData([] byte("Hello, ")) } (using lambda keyword)
- _ + _ (Scala-like shorthand for binary operators)
- fun (a, b) { return a + b } (shorter keyword fun)
- lambda (a, b) { return a < b }
Comment From: adonovan
I ran a quick experiment on the kubernetes code base (see https://go.dev/cl/676798). The data suggest that a narrow proposal, addressing only FuncLits whose body is a single return statement, might be viable, as they account for a quarter of all FuncLits.
I agree with @griesemer's opinion that the (a, b) => a + b
syntax is quite readable.
Comment From: gopherbot
Change https://go.dev/cl/676798 mentions this issue: go/analysis/passes/lambda: measure complexity of FuncLits
Comment From: jimmyfrasche
@adonovan Two use cases presented in this thread for complex lambda are:
mux.HandleFunc("/", (w, r) => {
// etc.
})
and
func Iter() iter.Seq[int] {
return yield => {
// etc.
}
}
(which generalize to any case where you pass or return a function).
I have no problem with initially limiting to expressions as long as expression-or-block remains on the table. I'd much prefer to just have both at the same time, of course.
Comment From: griesemer
Almost all of the suggested notations allow omitting the incoming parameter and result types, as that is likely to provide the largest savings - this seems largely uncontroversial. As has been pointed out early on, simply leaving the types away from a func literal as in
func (x, y) { ... }
is ambiguous because the parameter names might denote named types; i.e., this could be an ordinary function literal with two unnamed parameters of type x
and y
. Even with context information it's not always clear what the right interpretation is.
If we want to keep the func
keyword, there must be an additional indication of sorts (an arrow, a colon, etc.) to distinguish this from an ordinary function literal.
However, we can opt to leave the func
keyword away as well, as in
(x, y) { ... }
If the AI summary is correct, this particular notation has not been proposed yet. Specifically, the syntax would be:
ShortFuncLit = "(" [ IdentifierList [ "," ] ] ")" FunctionBody .
In short, it is exactly like an ordinary function literal, but without the func
keyword, and without any parameter and result types. There is a similarity with short variable declarations where we leave away the var
keyword and the variable type of an ordinary variable declaration. A ShortFuncLit can only be used in assignment context: it must be assigned to a variable, passed to a parameter, or returned as a result, all with a fully specified function type (no unresolved type parameters). The parameter and result types of the ShortFuncLit are then inferred (in fact, just copied) from the target function. The FunctionBody is unchanged, specifically return
statements need to be written as usual.
The single parameter case deserves some attention.
(x) { ... }
First, because the spec does not allow a parenthesized type name for composite literals, this notation cannot be confused as a composite literal - there is no syntactic ambiguity.
Second, because a short function literal would only be permissible in assignment context, in cases like
if (x) { ... }
the parser can recognize this as a proper if
statement with a parenthesized condition, and not as an if
followed by a function literal (we are not in assignment context).
(There may be other ambiguities, but I haven't found one yet.)
Here are some examples:
() { println(".") }
(x) { list = append(list, x) }
(x, y) { return x < y }
or, in context, using the examples from here:
mux.HandleFunc("/", (w, r) {
...
})
and
func Iter() iter.Seq[int] {
return (yield) {
...
}
}
Comment From: DeedleFake
That syntax has precedent in Dart, actually: https://dart.dev/language/functions#anonymous-functions
Dart uses two syntaxes, (args) { statements... }
and (args) => expression
. I'm not a fan of the large difference between the two, though. I would be fine with just a statement version to start with and we could see how it goes needing an explicit return
as it would be easy to add an alternative later.
Edit: Your post doesn't mention conversions. Would http.HandlerFunc((rw, req) { ... })
be allowed? Does that fall under "passed to a parameter" despite it not really being a function call?
Comment From: Merovius
I think I would find (x) { … }
hard to read, in practice. To me, it lacks a clear visual indication that this is a function (which =>
or func
would provide).
I know this is the bikesheddiest of issues, but I think this is the first proposed syntax I actually have a (negative) reaction to.
Comment From: jba
@Merovius we could add a backslash:
\(x) { ... }
That has some precedent in Haskell, is reminiscent of the letter 𝛌, and doesn't require a new keyword.
Comment From: griesemer
@DeedleFake I didn't think of conversions but they look like a function call so there's no new syntactic issue, and they also allow direct inference of the implied signature. Thus I would think short function literals should be allowed with a conversion. Which is actually pretty nice because it ties together the explicit function type with the parameter names as used in the function body.
Comment From: griesemer
@jba The \
would also eliminate all syntactic ambiguities because it's a token that is currently not used in Go. It would have the advantage that its meaning would be easy to look up if one were unfamiliar with it.
Some examples, function literals vs respective short function literal:
func () { println(".") } \() { println(".") }
func (x int) { list = append(list, x) } \(x) { list = append(list, x) }
func (x, y float64) bool { return x < y } \(x, y) { return x < y }
Using #
in place of \
has been proposed before, but \
seems a bit nicer (to my eyes) and is easier to type. But of course these things are highly subjective.
Comment From: apparentlymart
I don't personally care very much about exactly what punctuation delimits the arguments and the body; the details of how the effective function type of the result are decided seem more important if we have consensus that the primary goal here is to avoid writing those out explicitly.
With that said, I feel a little unsure about how exactly that would work given the most recent comments above. I'm going to use the syntax from https://github.com/golang/go/issues/21498#issuecomment-2917671187 in this comment just because I need some syntax to use, but this comment is purely about the type-selection behavior and not about the syntax.
One possible interpretation is that a shorthand anonymous function expression has a type somewhat analogous to an untyped constant. For example, (x, y) { return x + y, x - y }
could have a new kind of type that tracks only the parameter and result counts, which I'll notate for this example as (2) { return 2 }
representing "two parameters, two results". (Note: I am not suggesting this notation as syntax we would use in the language. It's only for exposition in this comment)
A function literal of type (2) { return 2 }
could therefore convert to any concrete function type that takes two parameters and returns two values as long as the function body is consistent with the conversion target.
Converting (x, y) { return x + y, x - y }
to func (x, y int) (int, int)
is acceptable because the arity matches and substituting int
as the type for all of the arguments and results produces a valid concrete function.
However, converting it to func (x, y bool) (int, int)
would not work because the +
operator is not defined for bool
.
The other characteristic of the existing untyped constants is that they each have a default type, which is used in situations where there is no explicit target type to convert to. That's trickier for these incompletely-typed anonymous functions because we don't any information to directly imply the intended types of x
and y
in the example I'm using. There are many different types for which the +
operator is valid, and each type that allows the +
operator produces a different type of result, so the function body is not in general sufficient to decide a default type. Therefore it seems to me that (unlike the other untyped constants) it must be forbidden to use these in any context that doesn't imply a conversion to a concrete function type.
There is a narrow case where we could decide the parameter types by context and then decide the result type by threading the chosen parameter types through the body:
func Map[T, R](seq iter.Seq[T], f func (T) R) iter.Seq[R]
If this function were called with seq
as an iter.Seq[string]
, then we can assume that f
's parameter is also of type string
, and so we could in principle infer R
as long as the given function's body is acceptable for an argument of type string
and, when substituted as such, all of the return
statements produce a value of a consistent type.
The last part of that seems the most tricky though, since I expect it would be hard to always return clear error messages if the function body doesn't have consistent types in all return
statements for a given concrete substitution of T
. For example, if given an untyped anonymous function (x) { if x.Foo() { return x.Bar() } else { return x.Baz() } }
then this is valid for any x
whose Foo
method returns bool
and whose Bar
and Baz
methods return the same type, but would fail for a different x
whose Bar
and Baz
methods return different types. That failure case seems quite hard to clearly diagnose in an error messaage.
That particular detail seems simpler if we stick to the variation where an untyped anonymous function allows only results and no statements, like (x, y) => (x + y, x - y)
or (s) => ("Hello, " + s)
(again, I'm not proposing this specific notation), since then there is only one (implied) return
statement to consider when deciding the effective return type. As long as all of the result expressions have concrete types or are constants that have a default type then there is exactly one inferred result type.
Nonetheless, this does seem to interact in a somewhat-complex way with the existing type inference for type parameters. 🤔
Comment From: Merovius
@apparentlymart @griesemer suggested only allowing them in assignment context specifically because we know the type in those contexts (presumably, short variable declarations are disallowed as well, as they are with nil
, for example). He also mentioned that the types must be fully specified, i.e. there must be no unresolved type parameters, which addresses your Map
example - y := Map(seq, (x) { return x })
would not be allowed (the return type is not inferable), you would have to write y := Map[T, T](seq, (x) { return x })
. Perhaps we can come up with rules for type inference at a later point, but for now, it makes sense to restrict to something we know that works.
At least that's how I would understand it.
Comment From: DeedleFake
@apparentlymart
To quote @griesemer's post above:
A ShortFuncLit can only be used in assignment context: it must be assigned to a variable, passed to a parameter, or returned as a result, all with a fully specified function type (no unresolved type parameters). The parameter and result types of the ShortFuncLit are then inferred (in fact, just copied) from the target function.
Your example would simply not work because R
is unknown. This is disappointing, but could be solved separately later. What you could do is something like func Map[R, T](sew iter.Seq[T], m func(T) R) iter.Seq[R]
and that would allow Map[float](ints, (v) { return float64(v) })
with an explicit type.
Generally speaking, this new syntax does not really do type inference. It just copies the argument and return types directly from whatever it is bring assigned to.
Comment From: aarzilli
@apparentlymart @Merovius that is also my understanding however I want to point out that almost everyone asking for this feature is requesting it for use with Map/Filter/etc, so is this actually implementing something nobody asked for?
Comment From: apparentlymart
Thanks for the followups to my comment. On my first read I think I misunderstood what was meant by "the target function". On re-read, I guess it was intended to mean "from the target function type", and is therefore describing something mostly similar to what I was describing in the first half of my comment.
FWIW, I was not intending to suggest that what I wrote in the second half must be allowed. I was just thinking aloud about whether and how there is something analogous to a "default type" for an untyped anonymous function, like how anonymous integer constants default to int
. My conclusion is that there isn't any such default type in general, but that in some cases we could use partial type information to infer one. But indeed, that doesn't necessarily mean we should; it would be okay to say that there simply is no default type, just as nil
has no default type.
Comment From: infogulch
That's a pretty good AI summary, it matches my memory from following this thread since 2017.
I understand that basically everyone here is a self-selected lambda enthusiast and brings their own sugar flavor preference from a previously used language, so this will be an unpopular opinion among the active thread participants, but Go doesn't need a super compact lambda syntax sugar. The existing syntax is fine. Just like Go doesn't need a try
statement and if err != nil
is fine. Go doesn't need two completely different syntaxes for writing lambdas.
Completely new syntax aside, the original motivation of inferring types would still be nice to have, but this problem can be addressed with a much smaller syntax change than most proposals. In particular the "underscore as a placeholder for inferred types" solution is better than the attention it's received would indicate:
- It resolves the param name / type name ambiguity problem with an absolute minimum change to syntax
- Underscore in the type position has not been implemented before, but it still reads very much like Go.
- Only a handful of characters longer than other proposed syntaxes (3+, depending on the number of arguments). Go's syntax choices generally do not minimize typing.
- This leaves open the ability to specify types of some parameters in the case where a simple type inference algorithm doesn't resolve.
Examples from recent posts:
func() { println(".") } func() { println(".") }
func(x int) { list = append(list, x) } func(x _) { list = append(list, x) }
func(x, y float64) bool { return x < y } func(x, y _) { return x < y }
Map[float](ints, func(v int) float64 { return float64(v) }) Map[float](ints, func(v _) { return float64(v) })
Comment From: griesemer
@aarzilli When we Map/Filter/etc. there's usually a know data structure over which to Map/Filter. So I think this will work as expected in most cases.
Comment From: Merovius
@griesemer I do think it's a significant limitation. For a simple example, this wouldn't be allowed, as I understand it:
func Map[A, B any](seq iter.Seq[A], f func(A) B) iter.Seq[B] { /* … */ }
func Primes() iter.Seq[int]
func main() {
for v := range Map(Primes(), (x) { return x*x }) {
fmt.Println(v)
if v > 100 { return }
}
}
On the other hand, in contexts where the generic function (Map
in this case) is itself passed to another higher-level function, we could infer its result type (in theory).
But I honestly do not believe we would ever end up with a way to allow this example which omits the types in the short function literal syntax. It would require doing actual type inference on the function body. I can't see that being added to Go for a relatively minor convenience feature like this.
Comment From: griesemer
@infogulch In your examples, if you want to be consistent, I believe you'd also have to use _
for the result type, otherwise you're mixing ideas (leaving away the types in some cases but not in others). But if you do that, you'd have to name the result parameters, I think. What about func(_ _) _ { ... }
? It get's odd pretty quickly.
Another problem with _
is that it has already a very specific meaning: it is an identifier that is never declared. Using _
in type position is already permitted syntactically, but an error semantically. With your interpretation it means "infer the type". We could decide to do that, of course, but it opens Pandora's box. What about func (x int, y _) { ... }
? Should that mean the _
type is inferred? Or should this be an error? Can we use _
in other situations, say: var x _ = 42
?
It seems much cleaner to have a syntactic notation which is very clearly indicating that we have something different from an ordinary function literal, very much like x := 42
is clearly different from var x int = 42
.
Comment From: infogulch
I believe you'd also have to use _ for the result type
Not necessarily, using _ for the type of a parameter could indicate that the return type should be inferred as well, which will be no more complex than any other lambda syntax. But I don't mind if we choose to require _ in the result type position. If a parameter is not used you can just omit its name like today so it would just be func(_) { ... }
or func(_) _ { ... }
, which doesn't seem that odd to me.
Using _ in type position is already permitted syntactically, but an error semantically.
Exactly, so this doesn't cause any conflict with current uses. The pandoras box is type inference, I don't see how this syntax is any more or less pandoras box than any other syntax.
func(x int, y _) { ... }
This is what I meant by 4. If type inference can't guess x is int for some reason, then it's quite handy to be able to fall back to specifying the type for the one parameter with type inference problems without having to switch to an entirely different syntax. What would you propose if type inference suddenly couldn't guess one parameter? Switch back to the completely different "old" syntax? This seems like unnecessary code churn. I think it's cleaner to allow type inference in any lambda parameter at the author's discretion.
Can we use _ in other situations, say:
var x _ = 42
?
This proposal should be scoped just to type inference for lambdas specifically. Other uses of the syntax can be considered separately. I would say "no" but whatever happens happens.
Comment From: griesemer
@Merovius Good point, of course.
That example has another problem, actually: let's say we knew the type of B
in your Map
function, and it's different from A
. Then (x) { return x*x }
would actually not be correct because the type of x*x
is A
, not B
. To make it work, a conversion would be needed which requires having a way to denote the inferred result type of (x) { return x*x }
.
Or put another way, (x) { return x*x }
as used in your example really is a shortcut for something like
func [A, B](x A) B { return B(x*x) }
where we don't specify the type constraints. Hmm.
Comment From: Merovius
Yupp. I think that's basically why I don't think the problem I mention will be solved. We want the types to be known to be able to just normally type-check the body. There just fundamentally is not enough information in my example to infer the return type, without inspecting the body and do type-inference on that. It would have to appear in the signature.
I don't think the conversion really adds an issue. I think in practice, if a conversion would be needed, you'd probably know the actual result type you want (so you'd directly write (x) { return uint64(x*x) }
or whatever). The only thing where you don't is if you really have a "properly" generic function (i.e. one that legitimately doesn't know enough about the type arguments you want to use) and those don't work as literals anyways and you have to pass them by name. That is, I think it is in the nature of function literals that you really do know the actual types and can write them out explicitly, where needed.
Comment From: jimmyfrasche
My only problem with the (x) { return x }
syntax is that it would need an additional token if a single-expression form is added later.
That seems like a reasonable ask, given that 25% of literals are single expr. If a single expression form is added, the language would end up like Dart where you have both (x) { return x }
and (x) => x
(or whatever token gets selected).
I don't see much value over just using the much more common args => block-or-expression
syntax, unless we're confident that there will never be a single-expression form.
Comment From: entonio
I was going to say that unless there is a clear path to a simple expression syntax like (x, y) -> (x, y)
(this is returning 2 values), maybe it's not worth bothering with this feature request at all, because a more complex syntax seems to be, as pointed out before, little gain for what appears to be a lot of work.
The issue of type inference has been assumed by most as something that just has to be doable - after all, go doesn't have func overloads, so when passing a lambda as an argument there should be enough constraints on what the types can be, but maybe the compiler doesn't work that way, or maybe generics complicate it - but from the latest interactions I'm not so sure anymore that it's always doable. Of course, restricting contexts where the syntax can be used can help, but the difficulties seem to be precisely with the most profitable uses.
It's one thing if this feature simply goes against the ethos of the core go team, which I feel it does, but can still be used thoroughly by the community at large. However, if it's (also, at that) a matter of difficulty and undesirable complexifying of the compiler, then your efforts should be directed elsewhere.
Comment From: griesemer
As this discussion has gone for so long, it's hard to not go in circles. But from the many comments it's fair to say that more people want something done here than leaving things alone.
Here's an attempt at making some more progress.
It seems to me that we have a fairly wide agreement (perhaps even consensus?) on the following points:
- a primary (and desired) benefit of short function literals is that one can omit parameter types
- per the summary, there's a strong preference for enclosing the incoming parameters in parentheses (it's also the most Go-like notation)
- in cases where the signature types cannot be easily derived from the context, one simply cannot use a short function literal
If we can agree on this, then the notation starts with something that looks like (a, b, c)
, followed by the function body (in various forms), and perhaps with an additional disambiguating token (\
before the parameters, or =>
after the parameters). There's some sympathy for =>
(or ->
). The=>
use seems perhaps more acceptable; it is also widely known. Reading through more of the comments, many people like the =>
notation (and so do programming languages).
From this I'd conclude that any of the following notations
(x, y) => x + y
(x, y) => { return x + y }
(x, y) { return x + y }
\(x, y) { return x + y }
could achieve reasonably wide support.
Function literals have become more common, but they are still not extremely common, and likely won't ever surpass the frequency of assignment statements in typical Go code. I am venturing out on a limb here, but given this fact I think we should try to get to only one alternative way of writing a function literal.
Personally, I think =>
works well if the function body is a single expression producing a result(s). It cannot be used in cases like (x, y) => x+y, x-y
; i.e., where more than one result is produced with multiple expressions. Consider this short function literal as an argument to a function call: it's unclear what the ,
separates. We cannot parenthesize the result expressions either since Go doesn't have a notion of tuples.
Also, per the experiments, only a small number (12.8%) of functions had a function body consisting of just a return statement, so the =>
form with just an expression following it feels limited. If we want short function literals to be a bit more general, we probably need a block. If we have a block, I think we need to stick to the usual Go syntax for the block, specifically, return
statements should be spelled out, otherwise we have bigger issues with readability and special rules for blocks of short function literals. Let's keep it simple.
So we end up with something like:
(x, y) => { return x + y }
(x, y) { return x + y }
\(x, y) { return x + y }
Per my experiments in 2022, the (x, y) => { ... }
style notation shows up with a lot of testing code (t) => { ... }
where t
is of type testing.T
. @ChrisHines thought the notation is ok. I've argued for this notation again, later.
Therefore, if we like =>
, we probably should use (args) => { ... }
or perhaps func (args) => { ... }
if having the func
keyword is considered crucial for readability.
If we don't like =>
, something like (args) { ... }
, \(args) { ... }
would work.
For completeness, @tmaxmax reinvigorated the discussion in late 2024 with an alternative idea where the parameters are inside the block. This leads to compact notations such as (the variations are forms that appeared in subsequent comments, suggested by others):
func { x, y -> x + 1 }
func { x, y => println(x, y) }
func { x | return x*x }
func { t | ... }
and variations that use \
instead of func
. He also produced a detailed analysis of existing code.
From all this, my conclusions are the following:
1) If having the func
keyword is not important, given that a lot of people like =>
, the notation (args) => { ... }
seems the most flexible, yet reasonably compact. One could prefix with func
but that seems unnecessary.
2) If having the func
keyword is important, a notation such as func { args | ... }
is equally flexible and compact.
Variant 1) would also keep the (args) => expr
option open, but I think we should not have multiple short forms. And only permitting (args) => expr
seems too limiting.
Variant 2) could use =>
instead of |
but there's no need for introducing a new token with this form; personally I think |
reads nicely and borrows from Smalltalk blocks with arguments.
Finally, the form (args) { ... }
is perhaps a bit too finessed and "naked" and hard to decipher in context. \(args) { ... }
might be better, but perhaps not better than using =>
given the arrow's popularity. The \
is also less visible than =>
. Personally, I have a soft spot for 2) as it keeps the func
keyword, is very compact, and much closer visually to existing function literals. It also doesn't require a new token. But 1) seems workable, too, if readability concerns are satisfied.
Comment From: jimmyfrasche
:+1: for (args) => { ... }
in basic contexts where the signature is entirely determined. That leaves room for improvements if more need/data comes in.
I will note that as far as I am aware every language that uses it only requires parens for argument lists of length 0 or greater than 1 and they can be left off if there's a single parameter like x => {
. gofmt can add the () if that's deemed too much of a special case.
Comment From: DeedleFake
Was it ever really clear which of the two arrow types was more popular? I much prefer ->
, though I'm one of the ones who would rather not have an arrow unless it's the func { a, b -> ... }
syntax. I think that syntax, either with ->
or |
, is good because it's an obvious func
and the very next token, either (
or {
, shows what kind very easily. It also makes it easy to avoid the JavaScript problem of the difference between single-expression and statement list variants being annoying to convert between. That problem comes from the lack of {}
around only the expression variant in JavaScript, (a, b) => expression
vs. (a, b) => { statements... }
. It's more annoying to deal with than you might think.
Comment From: griesemer
Per @jimmyfrasche's analysis the distribution seems about the same. ->
appears to be used in more languages, but =>
is used in more popular languages (and vice versa, if the number of languages is increased). In this discussion, people seem to use =>
more often (but that's an impression that I haven't verified by counting). I think ->
is fine, too, though in some fonts =>
looks better than ->
(the -
is not centered with the >
). If we're down to which arrow to use, we're in a good spot.
Personally, I prefer func { args | ... }
because it's very close visually to our existing function literal syntax, and very compact. It also doesn't require a new token. I think it reads really well, and degrades nicely into func { ... }
if there are no parameters.
Given our small "representative" group (ahem...) we're down to a few alternatives, really.
Here's a table with a general pattern and some special cases. Not sure about the cases marked with /* ? */
.
The arrow ->
could also be =>
, of course.
1) using arrow 2) using func and vbar 3) using func and arrow
(args) -> { ... } func { args | ... } func { args -> ... }
(x, y) -> { f(x+y) } func { x, y | f(x+y) } func { x, y -> f(x+y) }
(x) -> { return x*x } func { x | return x*x } func { x -> return x*x }
(x) -> x*x /* ? */ func { x | return x*x } func { x -> return x*x }
x -> { return x*x} /* ? */ func { x | return x*x } func { x -> return x*x }
x -> x*x /* ? */ func { x | return x*x } func { x -> return x*x }
() -> { count++ } func { count++ } func { count++ }
-> { ... } /* ? */ func { ... } func { ... }
The case 1) allows for more simplifications in special cases (one parameter, no parameter, single expression as a result); I'd say they almost ask for those simplifications to "look good". The cases 2) and 3) degrade more evenly, irrespective of the number of parameters. I think we would want that because it's more consistent and it makes it easier to change the code (say add/remove a parameter).
What matters is how these look in context. Examples of 1) can be seen in CL 406395.
In go/types/builtins.go (CL above) there is a (rare) case of a function literal assignment.
arg
is declared variable of function type in this case.
arg = (x, i) => { *x = *xlist[i] }
vs
arg = func { x, i | *x = *xlist[i] }
vs
arg = func { x, i => *x = *xlist[i] }
Maybe esoteric, but having func
here seems clearer. No need to look twice.
Comment From: avamsi
I think I'm in the arrow camp if we're ever going to allow (args) => expr
, but otherwise func { args | ... }
is growing on me. Very readable.
Comment From: doggedOwl
again another vote for the arrow syntax (form 1 in https://github.com/golang/go/issues/21498#issuecomment-2921171577 ) not very imprtant whether that is fat arrow or thin arrow. Not only it is more familiar from other languages but also gives a clear visual distinction between the lambda form and normal form of a function.
Comment From: zhuah
@griesemer with the func { x, i | ... }
syntax, how will the code be formatted if function body has two or more lines ?
Comment From: jba
func { args | ... }
feels very strange to me, but you make a good case for it. I especially like how it looks with no args. It's got my vote. And definitely vertical bar. Much easier to type than either arrow.
Comment From: jba
@zhuah I would write it as
func { x, i |
...
...
}
by analogy with
func (x, i int) {
...
...
}
**Comment From: zhuah**
@jba Thanks, that seems clear enough. Though I have another question: As far as I know, in formatted Go code, {} denotes a code block where the left brace { is typically placed at the end of a line, and the right brace } occupies its own line to close the block. But this rule is violated in the `func { x, i | ... }` syntax.
**Comment From: DeedleFake**
@zhuah, yes it is, technically. I don't think that's a problem at all, though.
**Comment From: infogulch**
> [@jba](https://github.com/jba) ...the left brace { is typically placed at the end of a line...
This is not a problem with the lambda syntax that is supported today. https://go.dev/play/p/JWNk7GOxFJP
```go
fmt.Println(func() string { return "Hello, 世界" }())
Comment From: Merovius
I think the valid question is how this formatting interacts with automatic semicolon injection. The answer is that |
is not in the set of tokens after which a semicolon is injected by the lexer, so it should work fine: https://go.dev/play/p/V2ny6m189Ea
Comment From: jimmyfrasche
I still don't like how frankenstein func { args | ... }
looks. The part without func
only exists in languages that encourage "DSL" style and let you include a terminal closure by juxtaposition so you can do things like
ordinaryFunctionThatYouCanPretendIsSyntax { x, y |
// closure that you can pretend is a block
}
It's so strange to see it being used for purely aesthetic reasons.
If nothing else, I think it may be a stumbling block for people coming from languages that use it for its designed purpose.
Comment From: griesemer
Whether func { args | ... }
looks "frankenstein" or not is of course in the eye of the beholder.
But the rest of your argument doesn't quite hold up, I think. The "part without func
" that exists in other languages is a closure - and in Go closures traditionally start with the keyword func
. Your example is written like so in Go (with T1
, T2
being suitable types), and this kind of code pattern is pretty common:
ordinaryFunctionThatYouCanPretendIsSyntax(func(x, y T1) T2 {
// closure that you can pretend is a block
})
With short function literals we have now narrowed the choice to
ordinaryFunctionThatYouCanPretendIsSyntax((x, y) -> {
// closure that you can pretend is a block
})
and
ordinaryFunctionThatYouCanPretendIsSyntax(func{ x, y |
// closure that you can pretend is a block
})
Again, which one looks "better", is in the eye of beholder: grammar ambiguities aside, syntax is a lot about "purely aesthetic reasons", after all!
To me, the ->
looks like an operator from another syntactic production that happens to use a block on the RHS. That is, the ->
separates the part before and after the arrow, yet the part immediately before and immediately after should be read as a single unit.
Multi-line closures of this form are much more common then one-liners.
Comment From: jimmyfrasche
My point is that languages that use the Smalltalk-derived syntax do so to allow you to write what syntactically appears to be control structures. You can write loop { ... }
instead of loop({ ... })
where loop is an ordinary function and {}
is an ordinary closure. Chopping that ability off and sticking func
on the front is the frankensteining I referred to.
Adopting the arrow syntax uses the same syntax in the same way so you can expect things to be essentially the same.
The { args | ... }
portion of the other choice comes with expectations that will not be met.
Comment From: earthboundkid
I think \() {}
looks good. It has the downside that the slash will be eaten by Markdown though, sadly.
ordinaryFunctionThatYouCanPretendIsSyntax(\(x, y) {
// closure that you can pretend is a block
})
Comment From: griesemer
@jimmyfrasche I hear what you're saying. There is a technical argument against func { args | ... }
but yours is not it.
And on the other hand, the problem with ->
is that visually it reads like an operator (because that's what it is) where we wouldn't expect one in Go. After all the notation ->
is inspired by the mathematical view of a function describing a mapping from a to b. It works very well for expressions (x -> x*x
) but it doesn't work well for blocks as it separates the LHS from the RHS. I think that is what makes it hard to read in context, with multi-line closures.
The technical reason why func { args | ... }
might be problematic is that the |
will always be required, even if there are no parameters. Otherwise, if a short function literal block without parameters starts with a multi-assignment, we can't tell until very late if we have an assignment or possibly a parameter declaration. This can be finessed to some extent, but the RHS of an assignment syntactically can contain arbitrary expressions, including "or" ( "|") expressions. Those will likely be invalid at type-checking time, but at the moment I don't see an easy way to handle this purely syntactically. And the hard way may be very hard and probably not worth it.
In practice, this is not a problem: if there are no parameters ~one can always use func () { ... }
. Or~ one could write func () {| ... }
. Or use another symbol instead of |
(maybe \
).
[edit: @DeedleFake points out that func () { ... }
would not be equivalent to func { | ... }
.]
Anyway, not a showstopper, but not quite perfect, either.
Comment From: griesemer
I implemented CL 677655 which enables gofmt -s
to rewrite eligible function literals into the short form { args | ... }
. The analysis is purely syntactical, so there may be false positives. I've applied it to the std library, so we can actually look at the different variants for real:
1) CL 406395: uses (args) => { ... }
notation (CL and std lib from 2022, uses =>
instead of ->
)
2) CL 677577: uses func { args | ... }
notation
Enjoy!
Comment From: jonathansharman
@griesemer That's so helpful to see! :rocket:
I'm going to throw in my two cents on the side of some form of arrow notation. We'll quickly get used to whatever is chosen, but the "vertical bar" syntax with an empty parameter list in particular...
func {|
// ...
}
func {|}
...looks quite strange in my opinion.
Comment From: jcsahnwaldt
@griesemer You (quite correctly) pointed out that the perception of a certain syntax is in the eye of the beholder, but that also applies to whether something reads like an operator, or is hard to read in general. People who have used ->
or =>
for functions in other languages probably won't read these tokens as operators and will find them easy to read.
Comment From: griesemer
@jonathansharman func {|}
looks strange, but this case would be exceedingly rare, and this case actually would be trivial to handle as func {}
. The func {| ... }
may be more common, but one could also write func () { ... }
in this case. But I do agree these are somewhat unfortunate as they sully the otherwise very compact notation.
One could use something else for |
. In the past, :
has been suggested but that conflicts with label declarations. @tmaxmax suggested ->
as in func { args -> ... }
but then one might as well move the ->
outside. He also suggested ;
which could work, but would be incredibly subtle. That discussion led to |
, eventually. func { args \ ... }
may be too far out.
All in all, this probably shifts us back to the options discussed earlier:
(args) { ... }
\(args) { ... }
(args) -> { ... } // or =>
Comment From: griesemer
It seems appropriate to also throw in the AI suggestion. Below is what Google Gemini 2.5 Pro ("thinking model") suggest. I am amazed.
Proposed Syntax
The syntax simply drops the func keyword and the types from the parameter list. The parameter names are enclosed in parentheses, followed immediately by the function body in curly braces.
For example, consider sorting a slice of strings.
Standard Syntax:
sort.Slice(lines, func(i, j int) bool {
return lines[i] < lines[j]
})
Proposed Short Syntax:
sort.Slice(lines, (i, j) bool {
return lines[i] < lines[j]
})
In this context, the type of the second argument to sort.Slice is known to be func(i, j int) bool. The compiler can therefore infer that i and j are of type int and that a bool must be returned. The return type is kept for clarity, but could also be inferred in many contexts.
A version where the return type is also inferred would be:
sort.Slice(lines, (i, j) { // return type also inferred
return lines[i] < lines[j]
})
EBNF Definition
To formally define this, we can amend the Go language specification's EBNF. We introduce a ShortFuncLit as an alternative form of a FunctionLit.
A FunctionLit would be redefined to allow for this new, shorter form.
EBNF
FunctionLit = ( "func" Signature | ShortFuncHeader ) Block .
ShortFuncHeader = "(" [ IdentifierList ] ")" [ Result ] .
IdentifierList = identifier { "," identifier } .
// Existing unchanged definitions for context
Block = "{" StatementList "}" .
StatementList = { Statement ";" } .
Signature = Parameters [ Result ] .
Parameters = "(" [ ParameterList [ "," ] ] ")" .
Result = Type | Parameters .
Comment From: ianlancetaylor
One concern I've developed after looking at the various suggested options is that if we are optimizing for the reader, then the current syntax is pretty good. It's very clear at the moment you see it that it is a function literal, which I think is less true of the =>
syntax variants. It doesn't really on the heavily overloaded backslash. It tells you the types involved, which for the reader is sometimes redundant but also sometimes helpful.
There are various reasons why Go doesn't use anything like Hindley–Milner type inference, but one of them is that Go forces you to write down the types at function boundaries. When reading Go code you don't always know the type of a variable in Go, because of :=
and var v =
, but you always know the type of function parameters and results.
To me the strongest argument for a shorter function literal syntax would be for code like iter.Map(s, func(a int) int { return a*2 })
. In that kind of code the two occurrences of int
really are redundant. But iter.Map
doesn't currently exist so few people write code like that in Go today.
As soon as the function literals get more complicated, for me the advantages of the shorter syntax seem less useful. Also I find the examples in the original proposal, over 700 comments ago, less compelling.
Comment From: jimmyfrasche
@griesemer My argument wasn't intended to be technical. It's about expectations of people coming from other languages having problems if the syntax leads them to believe things that are not true.
There is a mathematical notation for anonymous functions and it is ↦: https://en.wikipedia.org/wiki/Function_(mathematics)#Arrow_notation The asciiification of that is |->
but when it was taken up by the mathier functional languages that clearly felt it sufficiently obvious to simplify it to ->
. I'm not entirely sure why that drifted to =>
. I think it may come from coffeescript that iirc had ->
and =>
with different notions of this
but everyone used =>
because its notion was easier to deal with (don't quote me on this!). Regardless, notation is more like natural language than computer languages and "wrong" things become the right thing when that's just what everyone uses.
Comment From: DeedleFake
func {| ... }
(or func {}
) and func() {}
are not equivalent. The short form can still have a return type, while func() {}
is void.
Comment From: tmaxmax
my two cents on syntax:
- if we plan on an expression form: (args) -> expr
, (args) -> { stmts }
- if we don't: (args) { stmts }
(i dig this actually)
my proposal of func { args -> exprOrStmts }
made sense to me only if the expression form and statement form are unified. this idea didn't get support, so i don't see the point of this syntax or any of its derivatives anymore.
syntax aside, having the possibility to specify the return type, as in the AI proposal, would also be useful for the Map
case and similar:
iter.Map(seq, (v) int { return v * v })
is still way better than:
iter.Map[int, int](seq, func(v int) int { return v * v })
though some form of inference would be cool. i'd love to be able to do:
v, err := () {
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
defer cancel()
return longRunningAction(ctx, ...)
}()
i.e. use short function literals to create scopes for defer
statements.
i'll relink my experiment and observations here, mainly as a reminder that the trends observed in the standard library need not correspond to trends in other real-life codebases.
i'll relink some correspondence on return type inference which I've found very insightful: - https://github.com/golang/go/issues/21498#issuecomment-2453571667 - https://github.com/golang/go/issues/21498#issuecomment-2453626361 - https://github.com/golang/go/issues/21498#issuecomment-2453926037 - https://github.com/golang/go/issues/21498#issuecomment-2454274943 - https://github.com/golang/go/issues/21498#issuecomment-2454442564 - https://github.com/golang/go/issues/21498#issuecomment-2454506617 - https://github.com/golang/go/issues/21498#issuecomment-2454641468
The gist of all that is that interfaces (namely nil interfaces vs interfaces holding nil pointers) and untyped nils sabotage any code depending on return type inference.
Comment From: griesemer
Any form that uses (args)
(with or without arrow) could allow the naming of result parameters as well. Not suggesting it should, just pointing out the possibilty. For instance
(x, y) (z) -> { z = x + y; ... return }
Comment From: zhuah
The syntax (x, y) (z)
appears slightly weird to me with consecutive parentheses. I'd prefer alternatives like |x, y| z
or |x, y| (z, a)
or |x, y -> z, a |
– this approach reduces confusion and enhances readability.
Comment From: Merovius
@ianlancetaylor To me, there is an advantage to the reader in that the code becomes less wide - the same reason why I prefer short variable names. It makes it easier to scan the structure of the code. Obviously, there are limits to how much that should be optimized for. But it is something I notice semi-regularly, when using fs.WalkDir, for example.
Comment From: mitar
So what are really arguments against (args) { ... }
? From my understanding, technically, it is as easy to parse as other options. It is succinct and after looking at examples here I think I quickly got an eye to detect/read it well in the code.
If those are just question of aesthetics at this point, I would suggest that or a) a decision is made by stewards of the language, or b) various forms are put to the community vote.
Comment From: entonio
I don't think there are weighty arguments against (args) { block }
and its companion (args) -> expression
, other than a notion that it's irritating to convert between the two. I felt the discussion had moved on to how difficult type inference (a prerequisite for all this) would be, or if it would be possible at all, then there was talk of it working if there were restrictions on context, then I said that if that made (args) -> expression
non viable it was better to spend the team's efforts doing something else, and we seem to have gotten back to syntax bikeshedding.
I apologise in case this isn't a fair assessment. If there isn't yet a syntax that works, regardless of other requirements, please say so and I'll try to think of one. I did think we had several doable options and the concern now was type inference.
Comment From: mitar
Yea, I am also not clear on if type inference is possible or not, but I think this is fine as that can be expanded through time.
And why (args) { expression }
is not OK?
Comment From: jba
@mitar The main objection is that it doesn't look like a function, just another bit of code made out of parens and braces. See https://github.com/golang/go/issues/21498#issuecomment-2918270125. I suggested starting it with \
, but the main problem with that (as I see it) is that markdown processors won't like it. Although GitHub's markdown seems fine if you use backticks: both \(x) {}
and
\(x) {}
render correctly, even though (x) {} doesn't (there is a backslash before the (x) in that last one).
Comment From: jba
Reading over @griesemer's experiment CLs, I'm struck by how much I miss the return information. We probably all know that test functions don't return anything, and perhaps that slices.SortFunc
takes a function that returns int
. But here is the first thing I came across that I'm less familiar with, from the bytes package:
func Title(s []byte) []byte {
// Use a closure here to remember state.
// Hackish but effective. Depends on Map scanning in order and calling
// the closure once per rune.
prev := ' '
return Map(
(r) => {
if isSeparator(prev) {
prev = r
return unicode.ToTitle(r)
}
prev = r
return r
},
s)
}
I don't know the signature of Map
offhand, so I have to look that up or read the function literal carefully to know if it even returns anything at all. (Here the comment helps; there won't always be a comment.) I see from return r
that it returns one thng. As far as its type, well, it's the same type as the argument's, but I don't know that either. Luckily, in this case I know that unicode.ToTitle
is from rune
to rune
, so I can do the type inference in my head. If that were also unfamiliar, I'd be in trouble. If I were in my editor, I could use its help, but what if I'm just browsing code?
And we've only saved a few characters: from
func(r rune) rune
to
(r) =>
Of course, you could always write it the old way. But will you? Will the person or LLM whose code you're reading?
Having looked through a lot of these, most of them were easy to figure out, even when I didn't know the signature. The functions are short so the return
or lack thereof is apparent. But some required real effort. Here are a few, as exercises for the reader:
- https://go-review.googlesource.com/c/go/+/406395/2/src/cmd/compile/internal/importer/ureader.go#380
- https://go-review.googlesource.com/c/go/+/406395/2/src/cmd/compile/internal/ir/mknode.go#145
- https://go-review.googlesource.com/c/go/+/406395/2/src/cmd/go/internal/modload/buildlist.go#156
Admittedly, these were the exception. I only found a handful in the first 200 files of the 946 in the CL.
Comment From: griesemer
To answer @mitar 's question, I do think we're really down to just the three options I have mentioned earlier:
1) (params) { statements }
2) \(params) { statements }
3) (params) -> { statements } // or =>
No 1) has the advantage that no special tokens are needed, and it allows a graceful degradation from an ordinary function literal to its short form (remove func
and types); or to go from the short form to the long form (add func
and types). 2) and 3) are close. Personal preferences and unforeseen obstacles aside, we would probably be fine with any of these. Also, once we have a block, let's keep the insides of the block as usual, so no { expr }
without return
statements. We've discussed this before, and it adds needless complexity while also reducing the readability. Let's not go in circles.
Regarding @jba's concern about the missing return information: I think it's a valid concern, and I think in cases where the functions are sufficiently long or complex, the answer is to not use the short form. Alternatively, one could allow the naming of the result values, but it's not an obvious win (to me) over the full (non-short) form.
Comment From: jimmyfrasche
I don't really get the missing information arguments. In any case where you could have a short function you could also just have a variable. Then you don't even know it's a function. It does mean you'd have to go somewhere else for specifics but that's already always possible in any context where this is applicable.
1 would require an additional token for introducing a single-expression form leading to either three separate syntaxes for functions or never ever ever introducing a single-expression form even if it some later changes would make the idea a pragmatic necessity or there are demographic shifts that make it overwhelmingly popular. It's already quite popular and we have evidence that this is applicable to around 25% of short functions so I don't think it's wise to decide against it prematurely.
2 is not much better than 3 so it would take a good technical argument for choosing that over the essentially standard 3 but it's already been demonstrated that 2 works in the grammar.
Comment From: ianlancetaylor
Go 1) (params) { statements } 2) \(params) { statements } 3) (params) -> { statements } // or =>
Given those options, I personally would object to option 1, because (as others have said), it takes parentheses and braces, widely used in Go, and simply by dint of careful ordering turns into a function literal. I also would object to option 2, because the backslash in Go and other languages generally means quoting of some sort; using the backslash as a cheap version of λ is too clever.
I don't like option 3 very much because I don't like the code I've seen that uses it. But I don't have a strong objection to it.
Comment From: bjorndm
As has been noted before => or -> are used by several programming languages so option 3 seems the least bad. One possibility not mentioned would be to put the arrow in front of the parenthesis if that can be parsed.
Like: => (args) {body}
Comment From: b97tsk
What about func {|params| statements }
? It hasn't been objected, right?
If there are no parameters, it degrades into func { statements }
without a problem.
Comment From: apparentlymart
This discussion is going round in circles. 😞
Comment From: tmaxmax
I'll state this: the only syntax that will receive the least disapproval is (args) -> expr
/(args) -> { stmts }
. Any other suggestion has died and will die. We'd do ourselves a service if we'd decide on it, stop the syntax talk and proceed forward with the other matters.
Is there any compelling reason to continue thinking about other syntax variants I'm missing, beyond the next commenter's subjective objection?
Comment From: jba
@tmaxmax Please don't strongarm. You are free to proceed forward with whatever matters you wish, while the rest of us have a civil discussion.
@apparentlymart That might be your impression. But this is a major language change and it's well worth getting it right. As long as posters perform due diligence by checking that their ideas haven't been proposed yet, or haven't been adequately considered, we should keep talking.
Speaking of which:
I still think a better syntax would begin with some sort of marker, instead of putting it in the middle as the arrow syntax does. All declarations in Go begin with a word, except for short variable declarations. Composite literals also begin with a word or type, unless they are nested. Short function literals should too. If backslash doesn't work, let's try something else.
I've grepped the entire issue for fn (args) {body}
. It does not appear except in the AI summary. (The AI got two things wrong: it made up this syntax, and put it in the wrong category.) The word fun
was suggested over a year go. It got two downvotes with no explanation, and no further discussion. I think it's worth talking about.
fn
bears to func
exactly the relationship we want: short to long. It would be a reserved word, not a keyword, so it wouldn't interfere with the same name used elsewhere. It would only have its special meaning at the start of an expression. It could be shadowed, as in
func g(a, b, ...) {
fn := func(...) {...}
h(fn (a, b))
}
The argument to h
is a function call, not a literal, but the parser would have to read the entire arglist to know that, which is presumably infeasible. However, that shadowing behavior wouldn't be unique to fn
: all reserved words exhibit it.
It's also much easier to type than either arrow.
Comment From: Merovius
It would be a reserved word, not a keyword, so it wouldn't interfere with the same name used elsewhere. It would only have its special meaning at the start of an expression.
An identifier can start an expression and fn
and fun
are both valid identifiers. So finding one of them at the start of an expression is not enough to assign a special meaning. Currently fn (a)
parses as a function call. With the suggested syntax it could also start a function literal.
So the parser needs to look ahead, to decide (or it needs to know which identifier are in scope, making parsing context-dependent). That seems very unfortunate. I don't think this syntax passes what we would expect from a syntax extension.
Comment From: jimmyfrasche
@jba We could make fn
a keyword (complicates deployment) but if a prefix is the main complaint there was also func (args) -> exprOrBody
(from https://github.com/golang/go/issues/21498#issuecomment-2920926263) which is redundant but only requires a new token. I don't see much benefit over just (args) -> exprOrBody
but it's not entirely unacceptable if a prefix is a hard requirement.
Comment From: aarzilli
A new keyword (especially fun or fn which I imagine are pretty common identifiers) would make so much code retroactively invalid that I have my doubts the convenience of this feature warrants it.
Comment From: jimmyfrasche
Each .go file is versioned (from go.mod or a build tag) so it would only be a keyword in files that need a newer go version and well before that Go version is released tools could be released for mass finding renaming to avoid transitional problems. It's not impossible if there's enough to be gained but it does seem to be more effort than is justified here especially given the better alternatives.
Comment From: jimmyfrasche
Poll: Let's say that hypothetically it's down to two options (modulo which arrow, see next post)
- :rocket: func (args) => exprOrBody
- :eyes: (args) => exprOrBody
- :tada: Neither will ever be acceptable we must do something else
- :-1: do nothing
Comment From: jimmyfrasche
Poll: Iff we use an arrow:
- :tada: =>
- :heart: ->
Comment From: jimmyfrasche
Survey:
I've worked with code that uses arrow functions (in any language or languages) and I: - :heart: liked it - :tada: did not like it - :eyes: do not have a positive or negative reaction toward it
Or - :rocket: I have not worked with code that uses arrow functions
Comment From: mitar
Taking a step back: I think the main motivation for lightweight anonymous function syntax is to be able omit types - that makes it shorter by default. I am not sure if I understand technically how easy it is to infer types for such functions, but ... wouldn't maybe the solution here be simply that Go compiler tries to infer types for all anonymous functions, without requiring special syntax for it? So you could simply omit types in simple cases where it is clear what types are which is exactly those cases where you want to pass some anonymous function in some callback or during functional programming.
My understanding is that one argument against this is it becomes then unclear if func () {}
means a function which returns nothing or a function which should be inferred what it returns. But is this really a problem? First, inferring and even reading if a function is returning something is pretty easy by searching for return
statement. Second, all existing valid programs would stay valid: type for all those functions would be inferred that they do not return anything, which is also the expected type.
Second argument is that it is unclear if func (a, b) {}
refers to types arguments a, b
without types or two two arguments without names with types a, b
. I think this is tricky, but is it really so tricky that it requires a separate syntax? @griesemer says:
Even with context information it's not always clear what the right interpretation is.
But couldn't we devise a set of rules to follow here? Something like:
- If and only if you have a list of identifiers on anonymous functions, types can be omitted. (So if function is not anonymous or if it contains any named parameter, then no type inference is done.)
- If all identifiers resolve to types, then we interpret them as a list of unnamed types. This makes all current valid programs still valid.
- Otherwise we interpret the list as names of parameters for which types have to be inferred. This is the new behavior.
I think Go already has a pretty short symbol for defining functions, func
, are we really gaining much by going for =>
or ->
? Two characters?
In recent pools and options I am missing the previously proposed func x, y { ... }
syntax. @griesemer made some experiments with that syntax some time ago. It looks like syntax could work, but it seems it is less readable? But is this just something we are used to? I was also surprised initially that in Go for
does not have parentheses, but then I got used to that. It looks to me like func x, y { ... }
syntax is similar, it clearly uses func
which makes it clear that some function definition is happening.
Comment From: DeedleFake
There is also the option of going all in on the Rust/Ruby style and doing something like |a, b| { return a + b }
. That makes it very obvious very quickly what it is, has precedent, and doesn't include extraneous things like func
and arrows.
Comment From: Merovius
@mitar
I am not sure if I understand technically how easy it is to infer types for such functions, but ... wouldn't maybe the solution here be simply that Go compiler tries to infer types for all anonymous functions, without requiring special syntax for it?
"Simply […] infer types" is a classic "draw the rest of the owl" step.
To be clear, I think the restriction that @griesemer outlined (short function literals can only be used in assignment context and their signature is derived from the type expected in that assignment) is easy to implement, covers the overwhelming majority of use cases and in the few cases that don't work, you can fix that by explicitly instantiating a generic function or just use the long function literal. i.e. even my example above would work fine under this rule, if you specify the type arguments for Map
. While I do think it is a restriction, I think people have taken my objection as more catastrophic than it really is.
The issue with doing actual type inference (to solve that tail of use cases - in particular, not require explicitly instantiating Map
in my example) is that you need to account for the fact that different return statements can use different types - as long as all of them are assignable to a common return type. You want the actually inferred return type to be the most general of those, while still not being overly specific (e.g. you can't just infer any
, which would always work, but is useless inference). And you also have a bound from the other direction, in that the type parameters of your outer function (Map
in my example) put constraints on what the actually inferred return type can be.
I actually do think you could probably write down an algorithm to do type inference for Go (it would probably look like 1. if all return
s use identical types, use that, 2. if all return
s use untyped constants, use the default type of those, 3. otherwise, use an interface that has the intersection of the method sets of all types in all return
statements and 4. add a bunch of special prose for channels and directions. And 5, something-something type parameters?). It's just not something we've been willing to do in the past and it seems a good chunk of complexity budget in the spec, for a relatively minor benefit.
Really, I think the restriction lined out by @griesemer is the right tradeoff. It is a limitation, but I think it is likely less of one than it may sound, in practice.
[edit] also, given that I commented now anyways: My impression was that @griesemer did a really good job keeping the discussion on rails and guiding it to an actual result. I don't really know why it seems to have devolved a bit again. If me being not clear enough above is a reason for that, I apologize - my objection wasn't intended to be a wrench in the works [/edit]
Comment From: mitar
Really, I think the restriction lined out by @griesemer is the right tradeoff. It is a limitation, but I think it is likely less of one than it may sound, in practice.
Sorry if I was unclear. What I meant to say is that I would prefer that we find ways that we do not need any new syntax and simply allow omitting types under restrictions lined out by @griesemer (which seem reasonable to me in practice, as you say), for func () { ... }
syntax (or func x, y { ...}
syntax). Those restrictions is what I meant by "how easy is to infer types" - so how easy is to implement support for omitting types under those restrictions.
Comment From: avamsi
The compiler might be able to figure out whether a and b are types or variables in func(a, b)
, but it could quickly become difficult for humans to reason about. Also, the func x, y { ... }
syntax becomes a bit ambiguous in function arguments -- for example, run(ctx, func x, y { ... })
could look like three arguments rather than two and might lead to confusion.
Comment From: sammy-hughes
Really, I think the restriction lined out by @griesemer is the right tradeoff. It is a limitation, but I think it is likely less of one than it may sound, in practice
Totally agree!
If this is ":= but for functions args and return", this is much more flexible. This syntax needs to resolve to a function with a non-template signature, or else it cannot support accessing fields and methods. It can only have a concrete function signature if type-inference can happen at compile time. The most direct way to do that is by assignment inference, E.G. assigning to an existing variable or a function with concrete/statically-resolved signature.
If type-inference is based on how it's called, not how it's assigned, this gets much more complex. Struct-fields and struct-/interface-methods can't be used in generic functions with 'T any'. If this is just "sugared, generic functions", we don't get the ability to use methods and fields; it would be primitives and routing only.
I can't find the comment, but it was stated that Go intentionally constrains type inference at function boundaries. Assignment inference retains that principle. If the approach was instead "sugared, generic functions", type inference would be based on the body of the function instead of the boundary.
Comment From: griesemer
Here's another summary of the latest discussions:
- Some consider the notation
(params) {statements}
problematic because it essentially depends on clever use of parentheses and curly braces in the right places. However, there's no fundamental technical reason against it, and several commenters like it. - There seems to be some agreement that the notation
\(params) {statements}
is not great: the\
will cause trouble with markdown; also it's not really that easy to see in a larger amount of code. - There's a lot of enthusiasm for
=>
or->
, with=>
winning out per emoji voting, but an arrow notation would probably not be blocked on the choice of arrow. - I have mentioned that
func { params | statements }
would require a|
even if there are no parameters, otherwise the block is exceedingly hard to parse. However, the notationfunc {|params| statements }
(using two|
's) would work nicely and would gracefully degrade tofunc { statements }
if there are no parameters. - We mostly all agree that short function literal can only be used if in assignment (or conversion) context, i.e. where all parameter and result types of the target function are fully known. That is, the signature of the short function literal has exactly the same types, and the short function literal simply provides "local" parameter names.
With all this in mind, there's perhaps two major variants:
1) (params) => { statements }
// or -> instead of =>
2) func {|params| statements }
// or slight variations
Admittedly, there's been little discussion around 2, but I am adding/keeping it in the discussion as a representative of a non-arrow based notation. I think we need to very carefully consider how short function literals are used and look like in Go in context, as opposed to stand-alone. I venture to say that both 1) and 2) feel equally non-Go like at the moment. If an arrow-based version wins out, there's some fine-tuning that could be done (see below); if a keyword-based approach is preferred, there's some variations possible, too.
To make things a bit more concrete, I created 4 corresponding CLs which show how the std library would look like if we had one of these short function literals. Since 1) invites further "shortcuts" so I created multiple versions (a, b, and c). 2) is more "stable" as the number of parameters increased/decreases, or the body changes; only one syntax is needed, which is nice.
1a) CL 678617 // variant 1
1b) CL 678636 // like 1a but allows x => { statements }
and (params) => expr
(and combos)
1c) CL 678618 // like 1b but uses ->
instead of =>
and also allows -> { statements}
and -> expr
2) CL 678635 // variant 2
I invite readers to really look at the CLs before commenting, and citing examples that are good/bad.
Finally, the most recent discussion is really dominated by a dozen people or so, thus is far from representative. But at the very least we have whittled down many notations to a small number of viable ones.
Comment From: apparentlymart
If the version with |
characters delimiting the parameters is chosen, is there any advantage to putting that segment inside the braces rather than outside it?
That is, I see the following three variations of that style:
func {|params| statements }
/func { statements }
func |params| { statements }
/func { statements }
1func |params| expression
/func || expression
Some advantages I see for variant 2 are:
- The
{ statements }
part is just a normal Block, rather than a block-like thing with an extra optional element at the start of it. -
It's more similar to some other peer languages that use
|
to mark anonymous function parameters, such as Rust.The only difference is the leading
func
keyword, which was justified in earlier discussion by making it clear to a reader that this is an anonymous function. - It could be extended to also support variant 3 later if desired, though I think that form would not support eliding the||
because otherwise it would be ambiguous.(Leaving open the possibility of doing this seems important to some folks earlier in the discussion.) - It could potentially be extended to allow optional explicit return type(s) later if desired:
func |params| returntypes { statements }
, if that turns out to help with type inference to allow use in generic functions like "map".(I think this is mutually-exclusive with the previous point though: the params must be followed by either a naked Result or a naked expression, to avoid ambiguity. This form also cannot elide
||
when there are no parameters, for the same reason.)
The first two of those points are the ones I'd give most weight to, since the first is about being more "Go-like" (reusing an existing production rather than introducing another very similar one) and the second is about familiarity for those coming from other languages, which seems to have also been a common argument also for the two arrow styles.
(I don't particularly favor this ||
style over the arrow style, but there was a lot more discussion about the arrow style already so I find myself having more questions about the ||
style.)
If the exact punctuation used is the primary remaining question here it seems like the remaining discussion would be heavily influenced by personal preferences, but I wonder if we can put that aside and try to agree on a way to prioritize the following somewhat-competing goals, and then evaluate each option based on how well it meets each one:
- Familiarity for folks coming from other languages.
- Including the
func
keyword so it's clearer that this is defining a function, vs. relying only on punctuation. - Room for future extension for a single-expression variant later, if we learn of a good reason to do that.
- Room for future extension for explicit return types later, if we learn of a good reason to do that.
- any others...?
I'm sorry for expression some frustration yesterday; with a more level head today I think the root of my frustration is that it doesn't seem like there's consensus on what characteristics to prioritize in making a decision, and so the discussion keeps oscillating rather than converging. I hope that trying to agree on what the most important characteristics are would be a little easier than trying to agree on a single specific design while everyone seems to be prioritizing different characteristics.
Comment From: bjorndm
Looking at the PRs I feel I like variant 2 the best because it really abbreviates the function literal without being less readable.
It is also the notation used in Ruby, so there is a good precedent to it.
Comment From: DmitriyMV
func (s *_TypeSet) IsComparable(seen map[Type]bool) bool {
if s.terms.isAll() {
return s.comparable
}
return s.is(t => t != nil && comparableType(t.typ, false, seen) == nil,
)
}
The last line is very hard to read IMHO.
func testBadInstParser(t *testing.T, goarch string, tests []badInstTest) {
for i, test := range tests {
....
err := tryParse(t, -> {
parser.Parse()
})
....
}
}
Same. It also opens up to something like p -> p*p
or even p -> p
which looks very similar to channel operations.
All in all, I find (...) => {...}
syntax quite easy to spot, read and reason about. I'm not convinced about arg => expr
but that's subjective. -> { ... }
looks like something from syntactic error scope to be honest.
I have no strong feelings about func { ... }
or func { |arg1, arg2| }
aside from the fact this this looks a bit like Ruby (which is not the language everyone likes, including me) and func { ... }
doesn't look like a clear advantage over func() { ... }
.
To me, one of the bonuses of (arg, arg2) => {
over func { |arg1, arg2|
is that it's easier to grep
(Just two symbols instead of at least 6 (actually 7, because {
needs to be escaped). I also immediately know that I'm looking at lightweight function just by spotting =>
unlike func
where I need to look and parse the surrounding symbols in my head.
Comment From: jimmyfrasche
(There are many unrelated simplifications being performed and sometimes files have nothing related to short funcs. For example src/cmd/compile/internal/types2/errsupport.go simplifies a for i, _ := range
and nothing else.)
Agreed that changing func {|params| statements }
to func |params| {statements }
seems to remove most of the noted issues albeit while raising additional questions. Regardless, I still vote for the arrow notation based on its wide usage. A novel syntax means everyone has to learn it; a widely used syntax at least spares some.
For the arrows:
One thing I noticed skimming CLs back to back is that =>
sticks out more than ->
when scanning lines due to the extra pixels. That's obvious but the effect was more pronounced than I would have expected.
1b: I see the logic but don't know about shortening the no args case to -> {}
. Can you still use ()
? It looks abrupt to me to have an empty LHS to an operator but then again I am very used to reading and writing () => ...
.
If I pretend the ||
are before the block, they all look fine though. (The arrows look better and 1b is my favorite, of course). I clicked a bunch of random files in all the options and didn't see any cases where it was bad and the majority of cases were improved.
Usage like src/cmd/compile/internal/inline/inlheur/score_callresult_uses.go would ultimately be better served by #73502 but a lambda syntax simplifies what's there now.
Comment From: jakebailey
One thing I noticed skimming CLs back to back is that
=>
sticks out more than->
when scanning lines due to the extra pixels. That's obvious but the effect was more pronounced than I would have expected.
I don't know if you mean this is a negative or a positive, but I personally find it a definite positive as it "sticks out" and you see it and go "yeah, that's a function".
Comment From: jcsahnwaldt
@DmitriyMV
return s.is(t => t != nil && comparableType(t.typ, false, seen) == nil,
)
The last line is very hard to read IMHO.
I agree, but to some extent it's an artifact of the transformation. If we clean up the trailing comma and the line break, we get
return s.is(t => t != nil && comparableType(t.typ, false, seen) == nil)
which is much better. And if we add parentheses around the parameter (maybe they should be required even with a single parameter), we get
return s.is((t) => t != nil && comparableType(t.typ, false, seen) == nil)
I think that's fine.
Next problem:
err := tryParse(t, -> {
parser.Parse()
})
As @jimmyfrasche pointed out, this should be written as
err := tryParse(t, () -> {
parser.Parse()
})
I think the empty parentheses should be required for anonymous functions without parameters, as in Java and C# (and probably others).
Java and C# (and probably others) slightly relax the requirements of the () -> expression
form and also allow statements, which is quite useful in many cases. One might think of them as "expressions returning void". If Go allows this, we could shorten the example to
err := tryParse(t, () -> parser.Parse())
Nice!
Comment From: DmitriyMV
@jcsahnwaldt
I think that's OK.
Now imagine that new argument appears:
return s.is((t) => t != nil && comparableType(t.typ, false, seen) == nil, t)
Can you immediately spot what is part of the callback, and what is the part of the argument list?
I think the empty parentheses should be required for anonymous functions without parameters, as in Java and C# (and probably others).
On that I agree.
One might think of them as "expressions returning void".
I'm not sold on this, because if tryParse
signature changes from func tryParse(t *testing.T, fn func())
to func tryParse(t *testing.T, fn func() ParseResult)
this will be a silent breaking change.
Comment From: jimmyfrasche
It's entirely possible for x =>
to be legal but have gofmt format it as (x) =>
. I would not choose that personally but it's an acceptable choice
@DmitriyMV :+1: to the majority of your last comment except that for
return s.is((t) => t != nil && comparableType(t.typ, false, seen) == nil, t)
This hypothetical new is
should take the t
first so that it reads return s.is(t, (t) => t != nil && comparableType(t.typ, false, seen) == nil)
or could take a multiline literal. Although even as it's written originally, it is getting up there and I think it's fair to consider using t => { return etc }
though it's not strictly necessary in that case, even if it's just so you can put the function body on its own line like
return s.is(t, (t) => {
return t != nil && comparableType(t.typ, false, seen) == nil
})
Comment From: Merovius
One (hopefully obvious) thing to point out is that the example CLs aren't really what Go would look like, as it will still be entirely possible to choose more verbose syntax, if it helps readability. That is, even if x => expr
is legal, you could still always decide to write
return s.is((t) => { return t != nil && comparableType(t.typ, false, seen) == nil }, t)
Which IMO is pretty decent, while still being shorter than the current original, in allowing to omit the types.
The CLs are useful to give an actual representative set of how example could look. But let's not forget that humans will behave differently from an automated tool. And it's already possible to write unreadable code.
(Personally: I just don't like how 2 looks. I am more and more a fan of 1b. I like the prospect that sorting by a field could be made as easy as slices.SortBy(people, p => p.Age)
. 1c goes too far, IMO)
Comment From: rogpeppe
1a) CL 678617 // variant 1 1b) CL 678636 // like 1a but allows
x => { statements }
and(params) => expr
(and combos) 1c) CL 678618 // like 1c but uses->
instead of=>
and also allows-> { statements}
and-> expr
2) CL 678635 // variant 2
(aside: I presume that "like 1c" is a typo and that it was intended to write "like 1b").
I'd vote for 1b there. I like the way it looks; the =>
is nicely distinctive and IMHO intuitive, and allowing a bare single argument and expression seems natural to me.
One thing that gave me pause was shorthand func notation with varargs: somehow the "vargargness" of a parameter seemed a bit different from the parameter type, even though it's inferrable in just the same way:
ctxt.DiagFunc = (format, args) => {
failed = true
t.Errorf(format, args...)
}
In particular, it's not too obvious that the args...
is possible. It's probably fine though.
But I guess another possibility might be to require the ...
to be specified in the params anyway:
ctxt.DiagFunc = (format, args...) => {
failed = true
t.Errorf(format, args...)
}
Although that breaks the nice current syntactic symmetry between ...T
(any number of T
values) and v...
(spread out v
), so maybe not a good idea.
One other minor style thing, I'd say that func() {....}
is still mildly preferable to () => {....}
even though it's one character more. But maybe that's just my historic bias speaking.
Comment From: ct1n
I'm not a fan of variant 2, it looks very alien. It also seems to make the language more irregular: before they had a very simple meaning (albeit they're also used for struct literals) and now they become a grab bag of syntactic sugar.
I've gotten pretty fond of the ->
notation, though maybe an argument against this version is that in expression form a->expr
looks like a C pointer dereference.
It seems to me the most value would be from allowing the expression forms. For multiple statements it doesn't seem to save that much over regular function notation.
Would the single expression form also allow a single statement?
Some examples that might make an interesting argument regarding allowing expression forms:
batch.Process((b) -> { return (yield) -> { b.Foreach((e) -> { return yield(e) }) } })
batch.Process((b) -> (yield) -> { b.Foreach((e) -> yield(e)) })
batch.Process((b) -> (yield) -> b.Foreach((e) -> yield(e)))
Elegant weapons, for a more... civilized age.
Comment From: jcsahnwaldt
One little issue with the arrow syntax options: Humans might be confused by a mix of ->
or =>
with <-
, <=
and >=
. (It's no problem for the parser, since these are different tokens.) I think this has been mentioned somewhere in the 1000 comments above, but it may be useful to look at concrete examples. Here are a few cases I can think of that might look strange.
With ->
:
doSomething(() -> ch <- v)
doWithValue((v) -> ch <- v)
doWithValue(v -> ch <- v) // in case parentheses are optional for unary functions
getValue(() -> <-ch) // this one's funny
getValue( -> <-ch) // this one's bad, but see previous comments about whether () should be required
getValueFrom((x) -> <-x) // this one's funny
getValueFrom(x -> <-x) // in case parentheses are optional for unary functions
filter(foo, (v) -> v >= 0)
filter(foo, v -> v >= 0) // in case parentheses are optional for unary functions
filter(foo, (v) -> v <= 0)
filter(foo, v -> v <= 0) // in case parentheses are optional for unary functions
With =>
:
doSomething(() => ch <- v)
doWithValue((v) => ch <- v)
doWithValue(v => ch <- v) // in case parentheses are optional for unary functions
getValue(() => <-ch)
getValue( => <-ch) // see previous comments about whether () should be required
getValueFrom((x) => <-x)
getValueFrom(x => <-x) // in case parentheses are optional for unary functions
filter(foo, (v) => v >= 0)
filter(foo, v => v >= 0) // in case parentheses are optional for unary functions
filter(foo, (v) => v <= 0)
filter(foo, v => v <= 0) // in case parentheses are optional for unary functions
I don't think this is a major issue, people will probably get used to either syntax quickly. A few lessons I take from these examples:
* Parentheses should be required for nullary functions.
* Maybe parentheses should be required (or inserted by gofmt) for unary functions.
* One might argue that =>
would be preferrable to ->
, because mixing ->
with <-
can lead to weird situations.
* On the other hand, one might argue that ->
is preferrable to =>
, because filter operations using >=
or <=
are more common that using <-
in an anonymous function.
Comment From: earthboundkid
The func || {}
version of variant 2 is interesting. Moving the pipes to before the curly braces solves the major objection to variant 2 and it seems easy enough to teach func parenthesis requires types for the variables, func pipe is the same but omits the types. I suppose func [] {}
is impossible because square brackets mean "generic", but they would look good too.
Comment From: tmaxmax
Re: @jcsahnwaldt
If the expression form for single statements is disallowed, then the following:
(v) -> ch <- v
becomes illegal and must be written as:
(v) -> { ch <- v }
The only possibly funky form that remains would be:
() -> <-ch
And I believe this is more than fine: how often do you have a callback which exclusively pulls a value from a channel and without checking whether the channel is closed or not? It's an edge case that will virtually never happen.
I'm personally heavily leaning towards simple arrow, ->
, as it looks nicer (IMO) than =>
and, as you've pointed out, doesn't interact weirdly with comparison operators.
Re: @ct1n
Regarding your examples:
batch.Process((b) -> (yield) -> { b.Foreach((e) -> { yield(e) }) })
batch.Process((b) -> (yield) -> b.Foreach((e) -> yield(e)))
if b.Foreach
expects a func(T)
and yield
is the iterator yield function, so func(T) bool
, the second example is a type error; if b.Foreach
expects a func(T) bool
then the first example is a type error. Unless yield
has another type something doesn't make sense here.
Re: all
Generally I believe that parenthesis should not be optional and expression form for void statements should be disallowed:
- the less options, the better
- we're already losing type information – keeping a distinction between void and non-void at least preserves something
- despite the braces one can write short functions with single statements on a single line
- refactoring is more convenient: if process
in (v) -> process(v, otherParam)
starts returning something, you'll get a compiler error without braces but nothing will happen if the function was (v) -> { process(v, otherParam) }
- could this argue in favour of expression form for single statements because it makes refactoring safer? if you do add a return value here one can argue that you probably don't want it ignored.
I believe that, in the same spirit in which Go is strict about type conversions and in distinguishing expressions from statements (void functions don't return at all in Go, there is no unit type), short function literals should have a strict, singular form and respect that distinction.
Comment From: mitar
func [arg1, arg2] {}
is maybe the best. :-) Generics extend static types and []
extends explicitly typed functions. :-)
Comment From: ct1n
@tmaxmax Thanks, fixed the examples.
Your argument regarding changing the function signature to add a return type not triggering a compiler error seems like a very strong one to allow statements if expressions are allowed. Also, functions have a number of returned types that includes zero. In order to be consistent zero should be handled the same way.
Comment From: tmaxmax
@ct1n but now your all your examples can effectively be written as:
batch.Process((b) -> b.Foreach)
or
batch.Process(batch.Batch.Foreach)
😃
If we accept the safer refactoring argument, I'd go as far as to not allow the braces form of short function literals with a single ExpressionStmt
at all, allowing only one of these two variants:
(v) -> process(v, otherParam)
(v) -> { _ = process(v, otherParam) }
This way if the number of return values of process
changes a compiler error will occur regardless of how you write the function. Though this will be a peculiar syntax edge case which will both slightly complicate implementation (you can't just parse a Block
anymore) and puzzle programmers ("I can write this block everywhere else"). We could let it be legal in the compiler but have go fmt
simplify that – but then the formatted code doesn't have the same behavior as the unformatted one.
Comment From: ct1n
@tmaxmax good point. It was a contrived example. I happened to choose a ForEach with a signature that works as iter.Seq.
Comment From: Merovius
could this argue in favour of expression form for single statements because it makes refactoring safer? if you do add a return value here one can argue that you probably don't want it ignored.
(x) -> process(x)
is not necessarily the only context process
is called in. So going from zero to more than zero returns already requires you to manually check every call site of process
. I don't think there is any meaningful safety here.
I strongly object to allow statements that are not expressions on their own. Passing a function literal that does not return anything should be rare enough anyways. Having the braces in that case adds significant clarity.
Comment From: DeedleFake
If the differentiation between returning an expression and a standard function body is going to be the {}
, then the ability to convert back and forth between the two needs to be added to gopls. Dealing with that manually in JavaScript before LSPs became common was an absolute pain. I'm still in favor of the func { a, b | return a + b }
and func { a, b -> a + b }
syntaxes, though, which would just simply not have that problem, though I, like many, am not thrilled with the func {| ... }
oddity. It has a similarity with Kotlin, too, which uses { a, b -> ... }
as their syntax.
@Merovius
I agree, but I think that should be amended to "a function literal consisting of a single statement that doesn't return anything should be rare". http.HandlerFunc((rw, req) -> { ... })
, for example, will probably be quite common, but a handler that is only a single statement would be less so. Even a simple (rw, req) -> { log(); next() }
wrapper requires two statements.
@bjorndm
|a, b| { ... }
is the Rust syntax, not Ruby, though it is similar. Ruby has two syntaxes, not counting the { ... }
vs. do ... end
difference. The block syntax is { |a, b| ... }
, but there is also a lambda syntax that is ->(a, b) { ... }
. Blocks are a weird Ruby thing that doesn't really map to anything in Go and are part of the syntax of a method call, not expressions in and of themselves, whereas a lambda is actually an expression that returns a function as a value similar, conceptually, to a Go function literal. Technically the lambda syntax is actually just shorthand for lambda { |a, b| ... }
, which is just a block being passed to the lambda
method which then returns a Proc
that, when called, calls the block.
Comment From: jimmyfrasche
It's probably because most of the code I've written in arrow languages used =>
but x => x <= c
doesn't look weird to me while c <- c -> v
does look weird to me. Though that's likely an argument that I'd get used to the latter eventually.
Requiring {}
on single statements is necessary to disambiguate between h => h.Close()
and h => { h.Close() }
. It allows the first to obviously be returning the error and the second to obviously be discarding the error. There's still value in being able to see that at a glance even with an implicit function signature.
Comment From: eihigh
(I apologize if this opinion has been raised before. Feel free to mark as outdated or duplicated.)
I support the view that 'anonymous functions are effective when they are functions that return a value with a single expression.' For complex functions, the benefits of omitting function arguments and return values become relatively small. I also think it would be acceptable to compromise by making argument types explicit. While this would be incomplete compared to what everyone desires, it wouldn't require strengthening type inference and I believe it would be an appropriate middle ground.
Specifically, I propose the following 'expression-body function':
add := func(a, b int) = (a + b)
slices.SortFunc(users, func(a, b *User) = (a.Name < b.Name))
// Could also be applied to top-level functions?
func seq(n int) = (
func(yield (int) bool) {
for i := range n { if !yield(i) { return } }
}
)
With this syntax, it's clear that return value types and the return keyword can be omitted while being usable only for single expressions.
Comment From: griesemer
@apparentlymart Regarding your questions:
1) Writing the |params|
outside the block would make it easier to deal with the block, but, judging from earlier discussions, most (but not all) people seem not fond of using ||
in place of ()
. Putting them inside the block makes them look less like parentheses, I think. That said, only a few people like this or any variation of it.
2) With respect to priorities, I think it's relatively simple: users primarily want to not repeat the parameter types. This would provide most bang for the buck. Everything else is gravy.
@DmitriyMV points out that (params) => {statememts}
is easy to spot as the =>
sticks out. With respect to reading something like x => x <= c
vs x -> x <= c
, one could always write x => (x <= c)
. That said, something like x <= c
is not an unlikely expression, which would argue in favor of using ->
.
Most commenters don't seem to like -> {statements}
; i.e., the ()
should be required: () -> {statements}
.
@rogpeppe asked about varargs: Given a literal of the form func(someparams, x ...T) {}
the short version would be (someparams, x) => {}
; i.e., the ...
would not be written because it's (currently) considered part of the type.
With respect to the stylistic question () => {statements}
vs func () {statements}
, perhaps this is something that gofmt could decide and thus enforce a preferred style.
To summarize:
-
If we want to keep the
func
keyword plus a block, we need a way to specify the parameters. Using(params)
is clearly the most preferred approach, but that requires an extra token of sorts to distinguish the notation from an ordinary function literal, or different parentheses. Most people do not like different parentheses, and we also haven't seen any convincing notation using an extra token (say a:
or what have you) in addition tofunc
. I think, realistically this means this approach is just not working well for most people. -
We've really whittled down the parameter section to
(params)
no matter what, it is the most "natural" notation for Go. This still leaves two possibilities:X (params) {statements}
and(params) X {statements}
(or(params) X expr
) whereX
stands for a suitable token. For the latter (infix) notation, the natural choice is an arrow. For the former (prefix) notation, we know thatfunc
doesn't work, and we would also have to come up with a new token. A new keyword is difficult to impossible, so it would have to be an operator of sorts. We have discussed\
which has problems, and others e.g.#
(more visible). -
Barring any breakthrough agreement replacing
func
with another prefix token, what remains is the infix arrow notation. The infix notation also more naturally allows an expression or a block on the RHS. Whether it's=>
or->
doesn't matter a whole lot it seems (we'd get used to either), but->
works better if the RHS contains a comparison<=
, and=>
works better if the RHS contains a channel operation. See also this comment.
Which leaves the following notations (using ->
this time, just for a change, and not expressing a preference over =>
):
(params) -> expr
(params) -> { statements }
We probably may want to require the parentheses on the LHS always (so no x -> x*x
for now), just to keep it simple. Still this matches version 1b closely; this also happens to be most liked by this audience so far.
Looking at the code samples in CL 678636, (params) -> { statements }
sometimes seems odd, especially if the block is large, and/or one would like to see the parameter types. One could disallow this form and thus "mandate" that one needs to write the parameter types for blocks, or leave this as a style decision that a programmer should make. My interpretation from the discussion so far is that we want to allow both, blocks and expressions.
Thus we're down to the choice of arrow (bikeshed). Shoot away!
(To be clear, as I mentioned before, the people in this discussion are not a representative group. But if we can come up with a conclusion, it should help drive this forward.)
Comment From: DeedleFake
One comment before bikeshedding on arrows: Is func { (a, b) statements }
not possible? Moving the parentheses inside the {}
should help disambiguate it, especially if they are always required.
In terms of arrows, I know that I've said several times that I prefer ->
, but I seem to be in the minority. Maybe it's because Java was one of the first places that I used a syntax like this, but =>
has just always felt kind of weirdly heavy to me despite my having done quite a bit of JavaScript over the years. I know some people think that =>
, >=
, and <=
could be confusing, but despite my dislike for =>
overall I actually don't think that would be a problem. I also don't think that ->
and <-
would be a problem, and that one also doesn't have a >-
so it's only potential confusion between two instead of three.
I also don't much care for {}
being used to differentiate single-expression and full function body variants for reasons I've gone over a couple of times in here, but it's not a deal breaker for me, especially if gopls gets support promptly for converting back and forth. Honestly, at this point, I don't really care what the syntax winds up being as long as it gets added at all. I run into places that I want this almost daily.
In either case, I very much agree that the ()
should always be required, even in the case of zero or one arguments. It helps a lot with visual scanning for extremely little downside.
Comment From: griesemer
@DeedleFake func { (a, b) statements }
is possible I believe, but perhaps a bit problematic. Note that (f())
is a valid expression statement. (a)
is not, as a
must be "used". But syntactically, writing the parameters inside the block using parentheses overlaps perhaps a bit too much with regular statement syntax (even if semantically invalid) for comfort. Also, func { () statements }
(if there are no parameters) looks a bit funny to me.
Comment From: DeedleFake
If only the ambiguity could be resolved later by the type checker as that would know about the number of arguments, but I'm guessing that that's not feasible given the timing of when it runs relative to parsing without significantly more extensive changes.
Comment From: griesemer
@DeedleFake There's no hard ambiguity here for the parser: for one, it knows that we're in a short function literal because of func {
, and then it could handle the case { () ... }
and also the others, such as { (x) = ... }
. It would take a bit more effort than dealing with parameters outside, but it wouldn't need type information.
Comment From: jimmyfrasche
The issues with the func { something
syntax weren't really what that particular something was.
Comment From: DeedleFake
No, I mean a case like func { (a) -1 }
if ()
aren't required for zero-argument functions. Is that a function with one argument, a
, returning -1
where -
is the unary minus operator, or is it a function that takes no arguments and is returning a - 1
with some weird parentheses usage? The only way to know for sure is to check the function type. This can be mitigated by requiring return
, but I suspect that there would still be some weird edge cases, such as func { (a) }
being either a function with one argument or a single invalid unused expression. It's a somewhat less problematic variant of requiring |
for zero-argument functions with the func { a, b | ... }
syntax.
Comment From: tmaxmax
@jimmyfrasche if braces are required for statements, (c) -> c <- v
is illegal. it would have to be (c) -> { c <- v }
.
@DeedleFake personally I'm not a fan of func { (params) stmts }
. I dislike the double wrapping in brackets and that syntax won't have an expression form. To me the whole idea of wrapping everything in braces made sense only if the expression and statement form were unified. That didn't get any support. I feel like opting for that sort of syntax gives something cumbersome to write, longer and handicapped to statements obly.
Anyway, my arrow of choice is ->
.
Comment From: bjorndm
Ok if we are down to the arrow then how about this arrow:
~>
This has the advantage that it can't be confused with a channel send nor with a comparison operator.
( a, b) ~> { a + b }
Comment From: jimmyfrasche
@tmaxmax oops, I had the arrows swapped. Updated. Thought of a worse case anyway: c <- c -> <-c
Comment From: griesemer
@bjorndm I believe ~>
has been proposed in the past. Depending on font/font size/a person's eye strength it can be hard to see the difference between ->
and ~>
. Also, in math notation, ~>
sometimes means "approximately greater than" which is not what we want to express.
Comment From: tmaxmax
@jimmyfrasche Assuming mandatory parenthesis, you'd have to have c <- (c) -> <-c
. Assuming basic decency towards yourself and others, you'd have to have c <- (ch) -> <-ch
(or any name other than c
, lol). And all this matters assuming you'd have a situation where you want to send on a channel a function which solely blocks until a value is received on the input channel.
Sure, there's worse:
c <- (c) -> <-(<-c)((c) -> (<-c)(c))
but will I ever write this? Is this fight worth having?
Comment From: ChrisHines
I've been following along with the discussion here recently and just spent some time reviewing some of the earlier discussion from about 3 years ago. The discussion has explored several variations since that time. But it seems we've come back to essentially (one of) the same forms that we were discussing back then.
Perhaps that's a sign that it is the best form. Or maybe that's a sign that we can't do any better and if we didn't like it enough three years ago to move forward it's just not good enough.
Back then, after looking at @griesemer's demo CLs, rewriting some of my own code and trying to take all of the discussion up to then into account I wrote:
So far my opinion has been essentially 50-50 on this proposal. I am a fan of function literals and use them regularly, but after further consideration of all the arguments and code examples and thinking about my own code that could use this feature, I've come down on the no side, 👎 . The strongest arguments against this proposal for me are:
- Adding a second function literal syntax is too big a cost relative to the benefits I am seeing
- The lack of information about return parameters in the signature significantly damages readability in my opinion. I think knowing whether a function returns anything prior to reading the body is important.
- Documentation, books, and training material for the language will either become stale or need to be updated everywhere. When updated they have to explain more styles and when they are allowed or should/should not be used.
- It pushes more work on tool maintainers to make updates
Three years on I see more benefits from short function literals thanks to generics and I am a bit less bothered by the lack of return parameters in the signature. But my last two concerns are still valid and perhaps a bit bigger since more books and tools have been written in the intervening time.
I do think the form @griesemer identifies we're converging on in https://github.com/golang/go/issues/21498#issuecomment-2941432763 is the best form if we proceed. These days I think I am 60-40 on the 👍 side now. But I could live without short function literals without much regret.
Comment From: jimmyfrasche
@tmaxmax what fight? I think c <- c -> <-c
is the best worst example because it has all the possible juxtapositions in one line so you can see whether that's acceptable. Similarly, the best worst examples for =>
are x => x <= 0
or x => x >= 0
. I don't think either is really better or worse. My preference is for =>
but there's no real reason for that other than familiarity related to its popularity.
Comment From: atdiar
Personally I find => a more legible symbol than merely -> as was stated by others above.
I'm still concerned by the lack of information about the return types. It won't be apparent from the function since we only see variable names (return a, not return bool a).
Perhaps that (a, b) => bool { // }
can be considered?
That could be useful in a codebase where there are functions with similar callback heavy code with different signatures.
The second form with the args within the curly braces didn't look very readable to me.
Is it possible to try with the following form?
func(|a,b|) retTyp { // }
instead?
After all, we simply want a form that allows us to get rid of the type info in the argument position. And if no argument, it might degrade nicely to a normal function? Unless I am missing something or it has been discussed before in which case just ignore.
Comment From: jakebailey
I don't find "optional parameter types, but required return types" to be a good balance; both are roughly as important as each other, and yet both can be inferred from types potentially "far away" or equally annoying to write out.
It seems to me like if you want to have the types written, you can use the old form.
(I'll note that editors with inlay hints can also restore the inferred types to provide additional readability, though that is of course not available anywhere else.)
Comment From: atdiar
There is a difference though. Usually, we have function argument names appearing close to the type while the return variable does not appear, only its type gets mentioned.
e.g. func(a Typ1, b Typ2) (bool, int)
We have more information about inputs than outputs in general.
It is still true with the short form currently proposed since we keep the arity visible for the inputs only. So there is an asymmetry. That we may remove inferable info for the inputs does not mean that doing similarly for the outputs (whose information is often already fewer) would have the same impact.
Making all information about return values completely disappear is decreasing legibility if one has many such callbacks which only vary by this factor.
For instance, I have event based code that is such that I have two types of callbacks with different signatures. Getting rid of the return type info would be confusing. (e.g. one returns nothing, while the other returns a boolean). (could be even worse, one could return an error while the other, a boolean)
Comment From: doggedOwl
since we down to which arrow to use, for me both are almost equally suitable but in relation to the potential confusion brought up (in regard to comparison or channel operations) i would say that at least in my cases having comparisons near the lambda is way more common than having channel operations near it. so in that case the "->" notation is slightly better.
Comment From: jimmyfrasche
Potential confusion can be reading the arrow wrong or writing the arrow wrong.
Is there an example for either arrow type where a reading error can make one bit of code look like it does something else?
Are there cases where a swapped arrow could not have a good error message? For example if x => y {
should be pretty easy to detect so the error message can say hey you meant >=
.
Comment From: griesemer
Thanks @ChrisHines for your perspective.
If we care about expressing return parameters, we could write the parameter list section as follows:
(x, y) -> {...} // no result
(x, y) _ -> {...} // one result, we don't care about the name
(x, y) (u, v) -> {...} // two results called u and v
or perhaps:
(x, y) -> {...} // no result
(x, y) -> _ {...} // one result, we don't care about the name
(x, y) -> (u, v) {...} // two results called u and v
In the case where the result is an expression (or perhaps multiple expressions), one would then leave away the block, so:
(x, y) -> x // two parameters, mapped to expression x
(x, y) -> _ {...} // two parameters, mapped to one result which is computed via the following block
(x, y) -> (y, x) // two parameters, mapped to two result expressions (swap of x, y in this case)
(x, y) -> (a, b) {...} // two parameters, mapped to a, b which are computed via the following block
That is, if there is no block, what follows after ->
is an expression (or possibly a list of expressions); if there is a block, what follows after ->
is a single result parameter, or possibly a list of result parameters. Maybe a bit to cute?
Anyway, I agree with the caveat if we proceed.
Comment From: apparentlymart
To me it seems more important to be able to express the result type (optionally) than to give the result a name.
Named return values are mainly used for documentation purposes, which only really applies to named declarations. They are sometimes used for awkward situations like panic/recover, but those situations can be written using the traditional anonymous function syntax.
However, optionally expressing a type would in theory allow using this new syntax in situations where the result type would otherwise be ambiguous, such as the generic function situations we discussed earlier in the thread. I was broadly in agreement that we should probably not worry about that for the first pass, but I think it would be nice to make sure there's room to grow to support that in future, since there are already other proposals that would benefit from it.
With all of that said, I'd expect that identifiers in the result position, if present at all, would be type names rather than symbol names:
(x) -> int { return x + 1 }
(s) -> (string, string, bool) { return strings.Cut(s, ":") }
(I personally think it's fine to not even support this at all for a first round, and just leave that part of the syntax reserved for future use once we have more experience with how this new syntax might be used in practice.)
Comment From: griesemer
@apparentlymart I think things get much more complicated once we allow some types and not others. Let's stick to the simple use case for short function literals.
We always have the long form.
Comment From: Merovius
@apparentlymart
However, optionally expressing a type would in theory allow using this new syntax in situations where the result type would otherwise be ambiguous, such as the generic function situations we discussed earlier in the thread.
It is approximately the same amount of typing to provide that type when instantiating the generic function (at which point type inference takes over). Moreover "if type inference fails, fully instantiate the function" is a pretty simple general rule to keep to.
Comment From: apparentlymart
I agree! My argument was that if there is any mention of results at all then it would be more useful for that to be types rather than symbols. But I'd rather just start with it not being allowed at all.
Since it has got buried in the hidden part of this discussion I will just restate the example that potentially benefits from being able to write a result type:
func Map[T, R any](in iter.Seq[T], f func (T) R) iter.Seq[R]
Without the ability to infer the result type of f
the caller also loses inference for T
, making them write both out explicitly.
If f
has a known return type then both T
and R
can be inferred automatically, in principle.
However, as I've said repeatedly: I don't think we should try to make this work as part of this proposal. I'm only arguing that if there ever is any syntax for specifying results in the shorthand syntax then this application of it would be far more impactful than supporting named return values. So I'm not in favor of any version of this proposal that supports named return values.
The version that doesn't support saying anything about results at all seems like the sweet spot we should focus on first.
Comment From: sammy-hughes
Usually, we have function argument names appearing close to the type while the return variable does not appear, only its type gets mentioned.
This is what editor inlay type hints are for! Hasn't everyone already agreed that this can only be used for assignments, with variables or parameters which are already statically resolved? The return type might not appear in text at that text location, but the concrete type, variable declaration or function signature, will be present in nearby text.
Comment From: atdiar
Usually, we have function argument names appearing close to the type while the return variable does not appear, only its type gets mentioned.
This is what editor inlay type hints are for! Hasn't everyone already agreed that this can only be used for assignments, with variables or parameters which are already statically resolved? The return type might not appear in text at that text location, but the concrete type, variable declaration or function signature, will be present in nearby text.
I don't see why the function signature should be in nearby text. It could be defined a couple packages away.
Also, not very convinced by the inlay hints argument. Not all code is consumed using an IDE. Take textual documentation or the go playground even... Or even the code on this very page...
With no return type info, we get some kind of dangling return statements where the return types may not even be visible from within the function body and require to fetch the function definition located in another file/package. Maybe it makes things easy to write as a oneshot but doesn't seem to make things easier to read.
[edit]:
We can note that even back in 2017, @bcmills intuition was also to leave the error return as is and just remove the type info for the input parameters. https://github.com/golang/go/issues/21498#issuecomment-324725266
Which is the same intuition I had when looking at some of my code below (evt or event is fine enough a name, the type is a bit redundant)
AppSection.WatchEvent("mounted", MainFooter, ui.OnMutation(func(evt ui.MutationEvent) bool {
tlistElement := TodoListFromRef(TodosList)
todos := tlistElement.GetList()
if todos.Length()) == 0 {
SetInlineCSS(MainFooter, "display : none")
} else {
SetInlineCSS(MainFooter, "display : block")
}
return false
}).RunASAP())
// vs
AppSection.WatchEvent("mounted", MainFooter, ui.OnMutation(func(evt) bool {
tlistElement := TodoListFromRef(TodosList)
todos := tlistElement.GetList()
if todos.Length()) == 0 {
SetInlineCSS(MainFooter, "display : none")
} else {
SetInlineCSS(MainFooter, "display : block")
}
return false
}).RunASAP())
// Noting that here I am returning true or false
// and not the result of some function f() returning a boolean which would be even less legible with return types removed.
Just anecdotal, of course.
(also, I really would rather have func(|evt|) instead of (evt) => .. Why? Because there are already too many parens. The pipe operators act as a nice visual separator in this case.)
-
...or perhaps
func || { statements}
, but I don't think that empty||
is needed to disambiguate here? ↩