-
Notifications
You must be signed in to change notification settings - Fork 17.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: spec: lightweight anonymous function syntax #21498
Comments
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.) |
Please no, clear is better than clever. I find these shortcut syntaxes
impossibly obtuse.
…On Fri, 18 Aug 2017, 04:43 Robert Griesemer ***@***.***> 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
<#21498 (comment)>, or mute
the thread
<https://github.com/notifications/unsubscribe-auth/AAAcAxlgwt-iPryyY-d5w8GJho0bY9bkks5sZInfgaJpZM4O6pBB>
.
|
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 Your examples then become
The syntax is something like
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
as in
|
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.) |
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. |
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. |
It's not the 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, ")) }) |
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. |
I do not like the stutter of _. Maybe func could be replaced by a keyword that infers the type parameters: |
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. |
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. |
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 |
I would just like to chime in here and offer a use case where I have come to appreciate the consider:
Now imagine we are trying to curry a value into a 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 If there was an arrow syntax you would simply change |
@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 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 var _ = compute(func(a, b float64) float64 { })
^ That would certainly cover the use case I had in mind; does it cover yours? |
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. |
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. |
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) |
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. |
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:
For example:
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:
where the function type is given as a name. Expanding a bit, a BinaryOp closure could then perhaps be written as
which might go a long way to shorter closure notation. For instance:
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 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. |
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 ( 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 What I'm saying is: this discussion boils down to:
The best thing we can do is provide more use-cases. |
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. |
@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
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 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. |
Note that for parameter lists and
The 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
The expression after the Note that the difference between a 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 For various Rust-like:
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:
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:
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:
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:
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.) |
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. |
@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 This has been discussed repeatedly in this thread. |
I've implemented a parser for the Here's a PR with the implementation: tmaxmax#1. In the PR I've detailed some additional relevant discoveries. Here are PRs with results and statistics:
The standard library has a size of 2231483 lines of Go code (including comments and blanks) as counted with:
Here's a sample of the statistics I've collected for the standard library code:
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:
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:
Here are the significant ways in which the results from my work codebase diverge from those from the standard library:
Furthermore, out of all these single-statement function literals:
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:
We could also run it on some other relevant open source codebases and see the results. |
You could require a
You could use |
Some other thoughts I've had:
@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) -> x + y
(x, y) -> (x + y, x - y) Of course, unless we choose not to support expression lists and force people to use 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 { 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. |
I believe that a number of us are considering |
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.
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're not doing inference the regular |
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. |
That would require two tokens and remembering which is which. |
I feel like we're retreading ground that was covered, at least in part, before the |
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.
|
Reasons how I see
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())) |
@txmaxmax
A good property indeed, and we shouldn't have anything that's hard to parse, but it's not top of the list.
Some people said that but I believe more people voted against it in #21498 (comment) I don't have a problem with involving
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 Most of the time expression lambdas are something like 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:
When you count 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. |
The 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 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 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 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 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:
They both have their strengths and weaknesses. What makes me lean towards the About the 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 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:
|
I actually proposed this before: #21498 (comment). 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 |
My reasons to favour the func x, y -> expression syntax:
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. |
That is not redundant information. It may not always matter or be important but when it does it is.
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 |
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.
The other forms don't have this flaw, so why should we accept it?
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:
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()))
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. |
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 😅 |
@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. |
@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 #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. 🤞 |
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 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. |
This could "easily" have been introduced in backwards compatible manner. the "normal" syntax
could be expanded to not require the types when they can be inferred;
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)
if, for some reason, there is a huge revolt against using All of this can be done in small steps, and adding more options does not break backwards compatibility? 🤔 |
@skasti What you are proposing was discussed before and is not backwards compatible
For #61898 to advance you would need this, then solve the problem with return type inference with |
@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. 🙂
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 |
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. |
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:
The |
Sorry, I was not aware 🙈 😞 Edit; oh no... is this why it is not possible to use |
It's typically used in type definitions: Line 203 in 28f4e14
You don't. |
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, )
}
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 mapped := slices.Collect(xiter.Map((v) -> expr(v), slices.Values(values)) which sucks. I already have a
We've discussed this ourselves at some point above so I'll leave the links to the relevant comments here for reference:
@mibk I don't think further syntax bikeshedding will bring us much value. It seems that the least controversial form is 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. |
@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 😅 |
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):
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:
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:
Using the Rust syntax (just as an example):
Uses 2: errgroup
The errgroup package (http://godoc.org/golang.org/x/sync/errgroup) manages a group of goroutines:
Using the Scala syntax:
(Since the function signature is quite small in this case, this might arguably be a case where the lightweight syntax is less clear.)
The text was updated successfully, but these errors were encountered: