Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

proposal: spec: lightweight anonymous function syntax #21498

Open
neild opened this issue Aug 17, 2017 · 673 comments
Open

proposal: spec: lightweight anonymous function syntax #21498

neild opened this issue Aug 17, 2017 · 673 comments
Labels
LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal
Milestone

Comments

@neild
Copy link
Contributor

neild commented Aug 17, 2017

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.)

@neild neild added v2 An incompatible library change Proposal labels Aug 17, 2017
@griesemer
Copy link
Contributor

griesemer commented Aug 17, 2017

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.)

@davecheney
Copy link
Contributor

davecheney commented Aug 17, 2017 via email

@ianlancetaylor ianlancetaylor changed the title Go 2: Lightweight anonymous function syntax proposal: Go 2: Lightweight anonymous function syntax Aug 17, 2017
@gopherbot gopherbot added this to the Proposal milestone Aug 17, 2017
@ianlancetaylor
Copy link
Member

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, "))

@neild
Copy link
Contributor Author

neild commented Aug 17, 2017

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.)

@faiface
Copy link

faiface commented Aug 18, 2017

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.

@jimmyfrasche
Copy link
Member

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.

@bcmills
Copy link
Contributor

bcmills commented Aug 24, 2017

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, ")) })

@neild
Copy link
Contributor Author

neild commented Aug 24, 2017

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.

@martisch
Copy link
Contributor

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, ")) })

@davecheney
Copy link
Contributor

davecheney commented Aug 25, 2017

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.

@cespare
Copy link
Contributor

cespare commented Aug 25, 2017

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.

@bcmills
Copy link
Contributor

bcmills commented Aug 25, 2017

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.

@dimitropoulos
Copy link

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

@myitcv
Copy link
Member

myitcv commented Nov 6, 2017

@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?

@neild
Copy link
Contributor Author

neild commented Nov 6, 2017

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.

@mrkaspa
Copy link

mrkaspa commented Nov 20, 2017

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.

@hooluupog
Copy link

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)

@firelizzard18
Copy link
Contributor

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.

@griesemer
Copy link
Contributor

griesemer commented Jan 3, 2018

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.

@dimitropoulos
Copy link

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.

@firelizzard18
Copy link
Contributor

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.

@griesemer
Copy link
Contributor

@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.

@bcmills
Copy link
Contributor

bcmills commented Jan 3, 2018

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 IdentifierLists 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.)

@ianlancetaylor
Copy link
Member

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.

@skasti
Copy link

skasti commented Nov 21, 2024

@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.

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? 😅 🙈

@aarzilli
Copy link
Contributor

I have never seen this in use;

It's typically used in type definitions:

type Seq[V any] func(yield func(V) bool)

how do you use an argument when it is just a type without an identifier?

You don't.

@tmaxmax
Copy link

tmaxmax commented Nov 21, 2024

@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. proposal: spec: lightweight anonymous function syntax #21498 (comment)
  2. proposal: spec: lightweight anonymous function syntax #21498 (comment)
  3. proposal: spec: lightweight anonymous function syntax #21498 (comment)
  4. proposal: spec: lightweight anonymous function syntax #21498 (comment)
  5. proposal: spec: lightweight anonymous function syntax #21498 (comment)
  6. proposal: spec: lightweight anonymous function syntax #21498 (comment)
  7. proposal: spec: lightweight anonymous function syntax #21498 (comment)

@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.

@skasti
Copy link

skasti commented Nov 22, 2024

@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 😅

@Roccoriu
Copy link

@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) })

@griesemer
Copy link
Contributor

griesemer commented Jan 14, 2025

@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.

@mpx
Copy link
Contributor

mpx commented Jan 16, 2025

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'.

@entonio
Copy link

entonio commented Jan 16, 2025

@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.

@Merovius
Copy link
Contributor

@entonio func(a, b) {} is valid syntax in value-context (assuming a and b are defined).

@Merovius
Copy link
Contributor

Merovius commented Jan 16, 2025

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).

@avamsi
Copy link

avamsi commented Jan 25, 2025

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 #21498 (comment)? 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.

@ianlancetaylor
Copy link
Member

My recollection is that the experiment that @griesemer did in #21498 (comment) shows that the approach suggested at the end of #21498 (comment) 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.

@tmaxmax
Copy link

tmaxmax commented Jan 27, 2025

@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.

@ianlancetaylor
Copy link
Member

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 #21498 (comment) to be quite helpful.

@jimmyfrasche
Copy link
Member

What is the issue with the arrow syntax?

@ianlancetaylor
Copy link
Member

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.

@jimmyfrasche
Copy link
Member

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.

@tmaxmax
Copy link

tmaxmax commented Jan 27, 2025

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.

@DmitriyMV
Copy link
Contributor

DmitriyMV commented Jan 27, 2025

@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.

@thepudds
Copy link
Contributor

if we combine this proposal with #71203

FWIW, see the suggestion from @jimmyfrasche in #71203 (comment), a sample recent reply from Ian in #71203 (comment), or expand the discussion there and Control-F for jimmyfrasche.

@DmitriyMV
Copy link
Contributor

@thepudds I know that, I just trying to show how the arrow syntax can interact with other features.

@thepudds
Copy link
Contributor

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 #21498 (comment).

@doggedOwl
Copy link

doggedOwl commented Jan 29, 2025

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 #21498 (comment) as unclear, to me it is better than the original but admitedly because I am used to scan for that type of anonymous function.

@gophun
Copy link

gophun commented Jan 29, 2025

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.

@DeedleFake
Copy link

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
LanguageChange Suggested changes to the Go language LanguageChangeReview Discussed by language change review committee Proposal
Projects
None yet
Development

No branches or pull requests