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: Go 2: Lightweight anonymous function syntax #21498

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

proposal: Go 2: Lightweight anonymous function syntax #21498

neild opened this issue Aug 17, 2017 · 510 comments
Labels
LanguageChange Proposal v2 A language change or incompatible library change
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 A language change or 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
Contributor

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
Contributor

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.

@earthboundkid
Copy link
Contributor

foo(func(int) { println(int) }) is not a compilable old form function, so it must be a new form function. foo(func(int) { /* body does not use 'int'*/ }) is valid under either interpretation.

Do we have an example of something that is valid under the old form but not the new form and would be broken by interpreting it as new form? I haven't been able to think of an example where this takes a working program and breaks it yet.

@zhuah
Copy link

zhuah commented Jan 19, 2024

@griesemer
Perhaps the simplest approach would be to replace the func keyword with lambda, resulting in something like foo(lambda(int) { println(int) }), in this case, int would be recognized as the parameter name rather than the parameter type.

I'm curious about the Go team's perspective on this proposal, since it was raised back in 2017, which feels like a long time ago, and we already have many comments here.

@sammy-hughes
Copy link

sammy-hughes commented Jan 19, 2024

I'm curious about the Go team's perspective on this proposal, since it was raised back in 2017, which feels like a long time ago, and we already have many comments here.

@zhuah, the guy you replied to has a REALLY good idea what the Go team thinks of things!

We really do need a syntactic difference (if ever so small) to distinguish between the existing function signature syntax and light-weight function signatures.

I'm skimming over the history of this thread, but was there ever a parse-related objection to the func a, b {a+b} syntax suggested by @bcmills back in this post?

@zhuah
Copy link

zhuah commented Jan 20, 2024

@sammy-hughes I know @griesemer is one of Go creators and i respect him so much for his great work on Go, i apologize if my previous comment offended people, i'm not so good at English, and that comment was translated by ChatGPT.

What my question is that whether the Go team tend to accept this proposal or not, or are considering what syntax to adopt?

But why would syntax be a problem, so many languages have lambda expression, Python, JS, Rust and even C++ and Java, their lambda syntax are relatively similar, i believe that's also suitable for Go, even invent new syntax should take only months rather than years on such a small language change.

Of course, it's totally acceptable if Go team decide to decline this proposal for code readability, i just think the day-by-day discussions on syntax seems a bit useless.

@timothy-king
Copy link
Contributor

@zhuah Suppose one had the valid Go function: func Apply(lambda func(A) B, a A) B { return lambda(a) }. Right now lambda is an valid identifier. If one makes lambda a keyword, this currently valid function would stop parsing (token LAMBDA instead an identifier token) and would no longer be valid Go. My understanding of your suggestion is that it is a backwards incompatible syntax change. It is not impossible for this change to happen, but the bar is extremely high. AFAIK Go has made 0 backwards incompatible syntax changes.

It is really important to get syntax decisions right. To maintain backwards compatibility, the language will be stuck with supporting programs using any new syntax for many years. New users will have to learn it. Programmers will have to read it. It imposes a cost on everyone using the language. This means that some discussions, where good suggestions haves both pros and cons, can take years to converge before progress is made. Progress can happen though, see generics. This process requests a lot of patience from all participants.

@griesemer
Copy link
Contributor

@carlmjohnson Even if it is always possible to decide whether code means the new form or the old form, since the decision would require checking whether the function body compiles dep. on the interpretation of the parameters, this seems like a pretty bad idea. Certainly not in the spirit of Go. Note that the function body may be long or complex and it may not compile in one or the other form because of other errors. It would be a very subtle and fragile mechanism. It seems safe to say that we'd not add such a mechanism to Go.

@zhuah What @timothy-king said. Also, yes, it's easy to invent new syntax and implement it (I have prototyped several versions of this a long time ago). But that is not the point. We try not to introduce new notation unless we have very strong technical reasons and a good amount of "buy-in" from the community. So far we have not had that. Also, it's not a huge priority item. It's a nice-to-have.

@sammy-hughes I believe the func a, b { a + b } notation works fine (no parsing issues). But if I recall correctly it was not particularly liked by many people.

@earthboundkid
Copy link
Contributor

@carlmjohnson Even if it is always possible to decide whether code means the new form or the old form, since the decision would require checking whether the function body compiles dep. on the interpretation of the parameters, this seems like a pretty bad idea. Certainly not in the spirit of Go. Note that the function body may be long or complex and it may not compile in one or the other form because of other errors. It would be a very subtle and fragile mechanism. It seems safe to say that we'd not add such a mechanism to Go.

I'm not sure I understand the objection yet, because in my proposal, it would always use the new form based whether the context of the callsite constrains the type of the argument, and never based the body of the function argument itself. It would be shift in the intensional meaning of existing code (from f(func(a, b, c){ }) meaning f(func(_ a, _ b, _ c){ }) to it always meaning f(func(a a, b b, c c){ })), but I'm still struggling to understand an example where it would take a valid program and give it observably different behavior or a breakage of working code (a change in extensional meaning, to use the philosophy jargon).

@earthboundkid
Copy link
Contributor

Here's a thing I don't like about my own proposal:

func f1(callback func(int)) { /* do something with callback */ }

func f2(callback any) { /* do something with callback using reflection */ }

f1(func(float64){ }) // would always be interpreted as f1(func(float64 int){ })

f2(func(float64){ }) // would continue to mean f2(func(_ float64){ }) 

Losing the compile time error for f1(func(float64){ }) would be a shame and lead to hard to debug issues. But it wouldn't break any existing code.

@griesemer
Copy link
Contributor

@carlmjohnson I see. Interesting. I suppose that could indeed work (I haven't been able to come up with code that could break so far, either). But it still seems rather fragile.

@avamsi
Copy link

avamsi commented Jan 22, 2024

Sorry, I think I still don't understand @carlmjohnson's proposal -- how would the following snippet be interpreted?

package main

func f(g func(int)) {
}

func main() {
	f(func(int) {
		_ = int(42)
	})
}

@earthboundkid
Copy link
Contributor

Okay, I think that’s an example of breaking existing code because you can’t use an integer as a callable.

@lnlife
Copy link

lnlife commented Jan 25, 2024

I created a function to find first matched element from a slice of objects. It works like Array.prototype.find() in Javascript:

func array_find[T any](a []T, fn func(u *T) bool) *T {
	for k, v := range a {
		if fn(&v) {
			return &a[k]
		}
	}
	var empty T
	return &empty
}

And use it:

tom := array_find(users, func(u *User) bool { 
    return u.name == "tom" 
})

What I expected is something like:

tom := array_find(users, (u *User) => u.name == "tom")

@sammy-hughes
Copy link

sammy-hughes commented Feb 12, 2024

What I expected is something like:

tom := array_find(users, (u *User) => u.name == "tom")

Yeah. I think everyone here is on the same page with you. What we can't seem to collectively figure out is what the specific expression should look like.

We all want to be able to throw a quick arithmetical statement or a map/slice-index operation or an attribute assignment as a function argument, or heck, just not have to write package-scope functions to be able to operate on generic parameters. What we can't settle on is whether it should be fat-arrow, skinny-arrow, vertical-bar, naked-func, or even whether it should include curly braces or not.

@hherman1
Copy link

What about:

func(u *User) u.Name == “Tom”

In other words, a func without {} permits a single expression which is automatically returned.

@sammy-hughes
Copy link

sammy-hughes commented Feb 12, 2024

At risk of being spammy, multiple-choice reaction poll?

  • 👍 like me, you just want this capability, whatever it looks like, within generous reason.
  • 👎 The specific syntax matters to you, and there is a definite right/wrong way to represent lambdas.
  • ❤️ require an explicit return as in |a, b| return a+b, (a, b) return a+b, (a, b) => return a+b, func(a, b) return a+b, etcetera.
  • 👀 omit the return as in |a, b| a+b, (a, b) => a+b, or func a, b { a+b }
  • 😀 use the keyword func. Examples would be func(a, b) { a+b }, func(a, b) return a+b, func(a, b) => a+b, func a, b { a+b }, func a, b => a+b, and so on.
  • 🙁 this should omit an initial keyword entirely. Examples would be (a, b) => a+b, (a, b) return a+b, (a, b) { a+b }, |a, b| a+b, etcetera.
  • 🚀 wrap the body in curly brackets, so it stays easy to parse, e.g. (a, b) => { a+b }, |a, b| { a+b }, |a, b| { return a+b }, or func a, b { a+b }. A weird example would be func{ a, b -> a+b }.
  • 🎉this style of expression should not use curly brackets, even if that restricts this functionality or makes it weirder to use. Examples would be the func(a, b) a+b, |a, b| a+b, (a, b) => a+b, (a, b) return a+b, etcetera.

EDIT 2024-02-13: forgot a counter-statement for "omit the return". Added heart statement as negation to eyeballs and boosted it by changing my eyeballs vote to heart.

@billinghamj
Copy link

The really key thing is being able to omit type info. At that point, virtually any syntax would be great.

I wonder if the Go team could take this away as a clear desire for the ability, and that whatever syntax they think is best basically works fine?

@ianlancetaylor
Copy link
Contributor

Thanks, I think we generally understand that. But we haven't found a perfect syntax yet.

@DeedleFake
Copy link

I like that func { a, b -> a + b } mentioned above. Reminds me of Kotlin's syntax, but with a bit more telegraphing thanks to func. It also kind of feels like a natural progression to me:

func F(a, b int) int { return a + b } // Named.
func(a, b int) int { return a + b } // Anonymous.
func { a, b -> a + b } // Typeless.

The -> is a little unusual, but, again, it's fairly similar to a fair number of other languages that way, including Kotlin and Elixir and, to some extent, Java.

Not sure about the need for an explicit return. I wonder how feasible it would be to say that the return is omitted unless there's a semicolon, including automatically inserted ones, after the ->. In other words,

func { a, b -> // Automatic semicolon here.
  return a + b // Return now necessary.
}

The single-line version would need to be only an expression to the right of the ->, not a statement. This would hopefully disssuade some ridiculous syntaxes and encourage people to write longer anonymous functions as multiple lines with returns.

On the other hand, it may be best to just just omit the implicit return for the initial version of this and add it later if it seems necessary. It would definitely help with things like slices.SortFunc(), though.

@fzipp
Copy link
Contributor

fzipp commented Feb 14, 2024

My 2 cents

func F(a, b int) int { return a + b }   // Named
func  (a, b int) int { return a + b }   // Anonymous
func  (a, b):        { return a + b }   // Types inferred; with body
func  (a, b):                 a + b     // Types inferred; with single expression

Example:

slices.ContainsFunc(s, func(x): x < 0)

@sammy-hughes
Copy link

I like that func { a, b -> a + b } mentioned above.

Yeah. I was just trying to cover the gamut in terms of examples. I personally declared it weird, but then it grew on me. Also, the skinny arrow is just a backwards channel-send/receive operator.

The result is pleasantly unambiguous without seeming alien.

Meanwhile, yeah. WHATEVER the syntax is, I just want it!

@jimmyfrasche
Copy link
Member

jimmyfrasche commented Feb 15, 2024

Ignoring the specifics of the syntax, there aren't really that many choices in general:

  • 👎 we shouldn't do this at all
  • 🎉 omit func
  • ❤️ omit types
  • omit return:
    • 🚀 always
    • 👀 sometimes

Always omitting return is like python's lambda: where you can only use it for a single expression with a single return.

Sometimes omitting the return is like javascript's fat arrow syntax where you can provide a single expression or a regular function body.

use the list above to vote for your must haves

@entonio
Copy link

entonio commented Feb 16, 2024

Brackets are probably ok in the US, but in other countries we usually have them tucked away behind key combinations. (There's a story about how 'programmers' always use the US layout because of this, but I doubt most do that, because we keep having to use other people's keyboards and vice-versa all too often). Now, that's already an issue when writing regular code blocks, but somehow when doing it inside an argument list, as these functions are mostly meant to be used, is even more of a chore. I bring this point up only because people in the US may not be aware of it. So the possibility of having plain -> a + b would be wonderful, not just a matter of trading any 2 characters for any other two.

@chad-bekmezian-snap
Copy link

Whatever we choose here, a restrictive syntax that does not allow for the use of return and allows for only one expression, which makes up the function's body, are ideal to me. Additionally, omitting func is a big win imo as well. The less syntax involved in utilizing this syntax, the clearer it will be to see what is happening in a simple expression.

@NatoBoram
Copy link

The syntax I'm preferring the most is exactly the same as the normal syntax except the types are inferred, but that syntax is not an option because of #21498 (comment).

The issue is that we need a slight difference, one character or so, so that existing code don't break.

One way would be to use fun instead of func for lambdas. This way, it looks almost identical and I think it respects Go's way of thinking.

@jcsahnwaldt
Copy link

jcsahnwaldt commented Feb 28, 2024

Maybe the func(arg _) _ syntax (inferred types, but with placeholders) would be an option that achieves somewhere between 50% and 80% of the benefits with 10% to 30% of the effort. Maybe not quite an 80-20 solution, but close.

I think this syntax was first suggested in #21498 (comment) in 2017.

Cases like #21498 (comment) by @DeedleFake and #21498 (comment) by @bradfitz would become a good deal less painful if type names could simply be replaced by _ placeholders.

And it doesn't shut the door for other solutions. If users gain some experience with it and still think it's too verbose for cases like func(a, b _) _ { return a+b }, support for syntax like (a, b) => a+b can still be added. No harm done.

@sammy-hughes
Copy link

I don't know why I didn't quite get the templating algebra necessary to allow arbitrary covariance between the different parameters and the return. Phew. Is it too late to just ask for generic, anonymous functions instead?

Just kidding....I think...

@eihigh
Copy link

eihigh commented Mar 23, 2024

Although I do not agree with it very positively, I think that abbreviated notation could be introduced only when the body of the function can be written in a single expression.

func (a, b int) int { return a + b }
func (a, b int) = a + b
func (a, b) = a + b          // can be inferred?
func (a, b int) = (a+b, a*b) // multiple returned value

In order not to add new tokens, I have chosen = for the symbol. It also looks like type aliases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
LanguageChange Proposal v2 A language change or incompatible library change
Projects
None yet
Development

No branches or pull requests