New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proposal: Go 2: 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. |
|
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. :) |
|
@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 |
|
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. |
|
I've argued before that in cases where the existing type restrictions make |
True, however Go already has the concept of type omission (mostly due to inference) in various places. And here is another form of type omission. I do not need to provide the type argument for This is mainly an expansion of the point @carlmjohnson just made
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: Should Go support function literal parameter type deduction? If so, which syntax?
(Maybe I'm missing a few obvious ones but it really feels like viewing this as a standalone new feature narrows the options significantly) |
|
Given the same example function signature as above: Should Go support function literal return type deduction? If so, which syntax?
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? |
|
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. The earlier point is more geared toward having two syntax for function calls rather than mere type elision. |
|
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: Of course, if I'm not sure whether 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? |
|
It is called type deduction in Go usually but one could argue that it's a flavor of type inference. :) |
|
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!! |
|
This was suggested in #58559 as a way to quickly turn an operator like But in my usecase of I would like to be able to call it like |
|
I would love to see a lightweight anonymous function syntax in go, but:
If we get it though? very nice... :) |
how about this example: #59122 |
|
I like lambda function syntax where it makes sense. Coming from a JavaScript background, a lot of people have incorrectly treated There's a danger of the same thing happening to Go. To prevent this:
|
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() { ... }
} |
|
We currently use
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 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 And really, the block part of a function is a lot like other blocks, except that the So, say we have a named function type; 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. |
|
@seebs Note that the |
|
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() } |
That is false though... |
|
if anyone is familiar with js, to support Promise-Chaining, we do need let caller determine the next output type. |
|
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. |
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 := nilis 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: