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 · 443 comments
Open

proposal: Go 2: Lightweight anonymous function syntax #21498

neild opened this issue Aug 17, 2017 · 443 comments
Labels
Go2 LanguageChange Proposal
Milestone

Comments

@neild
Copy link
Contributor

@neild 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 Go2 Proposal labels Aug 17, 2017
@griesemer
Copy link
Contributor

@griesemer 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 davecheney commented Aug 17, 2017

@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

@ianlancetaylor ianlancetaylor commented Aug 17, 2017

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

@jimmyfrasche jimmyfrasche commented Aug 18, 2017

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
Member

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

@martisch martisch commented Aug 25, 2017

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 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 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
Member

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

@dimitropoulos dimitropoulos commented Oct 31, 2017

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

@hooluupog hooluupog commented Nov 20, 2017

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

@firelizzard18 firelizzard18 commented Dec 25, 2017

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

@dimitropoulos dimitropoulos commented Jan 3, 2018

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

@firelizzard18 firelizzard18 commented Jan 3, 2018

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

@griesemer griesemer commented Jan 3, 2018

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

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

@ianlancetaylor ianlancetaylor commented Jan 3, 2018

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.

@beoran
Copy link

@beoran beoran commented Jun 13, 2022

@deanveloper Great idea! Rosettacode seems like a good place to collect syntaxes: http://www.rosettacode.org/wiki/First-class_functions or http://www.rosettacode.org/wiki/Higher-order_functions

@DeedleFake
Copy link

@DeedleFake DeedleFake commented Jun 14, 2022

@beoran @deanveloper

Technically, Ruby uses the { |a, b| a + b } syntax for blocks, which are a weird Ruby-specific thing, and ->(a, b) { a + b } for anonymous functions.

Here are a few more syntaxes:

Language names are bold if they're statically typed.

Language Syntax Notes
Python lambda a, b: a + b
Kotlin { a, b -> a + b } Niladic functions have no arrow, such as { 3 }.
Dart (a, b) => a + b or (a, b) { return a + b }
JavaScript (a, b) => a + b or (a, b) => { return a + b }
Java (a, b) -> a + b or (a, b) -> { return a + b }
C# (a, b) => a + b or (a, b) => { return a + b } or delegate(int a, int b) { return a + b } The delegate syntax was the only available syntax before C# 3.0. All variants also allow explicit typing, but delegate requires it.
Rust |a, b| { a + b }
Haskell \a b -> a + b The backslash is apparently supposed to look like a λ.
Elixir fun (a, b) -> a + b or &(&1 + &2)
D int delegate (int a, int b) { return a + b } or delegate (a, b) { return a + b } or (a, b) => a + b The shorthand version allows typing, too.
Swift { (a: Int, b: Int) -> Int in return a + b } or { a, b in a + b } or { $0 + $1 } I'm not 100% sure about this one. The syntax is bizarre. Anyone in here who knows Swift, feel free to correct me.

Also: https://en.wikipedia.org/wiki/Anonymous_function

@griesemer
Copy link
Contributor

@griesemer griesemer commented Jun 14, 2022

A couple of observations:

  1. The JavaScript notation is closest (or the same) to what we've experimented with (=> notation). Thus, this notation is widely known among programmers, with JavaScript probably the most widely use PL in the world.
  2. If we want to keep the func keyword one could borrow from Elixir and write, e.g.: func (a, b) => a + b or func (a, b) => { return a + b }. Basically the same as JavaScript with the additional func keyword. The => is needed to distinguish the function signature from an ordinary signature with types.

As an aside, the "weird" Ruby notation comes of course from Smalltalk where one would write [ :a :b | a + b ]

@beoran
Copy link

@beoran beoran commented Jun 14, 2022

@griesemer func (a, b) => { return a + b } seems like a good compromise between the func notation and the JavaScript notation.

However, seeing that the keyword "delegate" is used in several languages, another Go-like approach would be to add a built in function named delegate which can then be used like this: delegate(a, b, a+b). The last argument is the returned expression, the other ones are the arguments.

@c3y28
Copy link

@c3y28 c3y28 commented Jun 14, 2022

It might be difficult to realize that the Col is returning in this case:

As somebody who uses arrow functions in other languages (not JS), it's perfectly obvious what it is happening in all your examples. Even the true one. There's nothing mysterious about arrow functions.

All the arguments about readability eventually boil down to the fact that this syntax would be new to Go, and some programmers don't think the extra learning effort is worth the benefits.

Agree. All the concerns about readability will fade away after a short-term learning. Those who don't like this Syntactic sugar may eventually prefer this styles when they want to lazy evaluation on the function or they use a famous programe pattern map-filter-reduce. short anonymous function definition is helpful to write more readable code especially the business logic is very very complex(ie it requres multiple map functions and multiple filters before reducing)

@fzipp
Copy link
Contributor

@fzipp fzipp commented Jun 14, 2022

  1. If we want to keep the func keyword one could borrow from Elixir and write, e.g.: func (a, b) => a + b or func (a, b) => { return a + b }. Basically the same as JavaScript with the additional func keyword. The => is needed to distinguish the function signature from an ordinary signature with types.

Great, if func(a, b) => a + b is possible from a parsing standpoint, then func(a, b): a + b is possible, too.

@DmitriyMV
Copy link

@DmitriyMV DmitriyMV commented Jun 14, 2022

func(a, b): a + b

It looks a lot like map initialization where left part is key and right part is a value.

Compare

func(a,b) => {
    c := a + b
    return c, nil
}

to

func(a,b): {
    c := a + b
    return c, nil
}

To my tastes its too similar to typeless map inialization, that is:

"foo": {
	Foo: "bar",
	Bar: 1,
},
"bar": {
	Foo: "baz",
	Bar: 2,
},
"baz": {
	Foo: "qux",
	Bar: 3,
},

@fzipp
Copy link
Contributor

@fzipp fzipp commented Jun 14, 2022

It looks a lot like map initialization where left part is key and right part is a value.

While I don't hate the extra => I don't love it either. I find it a bit too ASCII-arty. In my opinion the func keyword makes it clear enough. Keywords are known to the programmer, they are unambiguous and unchangeable. Some people even highlight them in their editors. And I haven't heard of Python programmers mistaking lambda functions for dictionary keys.

@deanveloper
Copy link

@deanveloper deanveloper commented Jun 14, 2022

Not that Go needs to support this, but figured it'd be a fun addition: Kotlin and Swift both have the idea of "trailing lambdas", which are lambdas that are the last argument to the function. They are very powerful and allow for the creation of DSL-like APIs. Essentially, the concept allows you to put the lambda outside of the parentheses for the function call, if the lambda is the final argument to the function. For instance (using Kotlin syntax):

// (note: "(Int) -> Unit" is the type which represents a function that takes an Int and returns the Unit ("void") type)

// This function
fun foo(a: Int, b: (Int) -> Unit);

// "Normally" called like this
foo(5, { someInt -> 
    println("called with $someInt")
})

// Can equivalently be called like
foo(5) { someInt ->
    println("called with $someInt")
}
// Or this way, "it" is the default name for the first parameter of a lambda
foo(5) {
    println("called with $it")
}

I'm not really advocating for this feature in Go per se, but figured that it would be a useful thing to bring up so that we can possibly learn from it.

@DeedleFake
Copy link

@DeedleFake DeedleFake commented Jun 14, 2022

Kotlin also allows you to omit the parentheses entirely if the closure is the only argument:

doStuff { a ->
  stuff(a)
}

I'm not so sure this kind of cutesy syntactic trick makes sense in Go, though.

@carlmjohnson
Copy link
Contributor

@carlmjohnson carlmjohnson commented Jun 14, 2022

Ruby also works that way.

For my part, I was in favor of this feature before I listed out the pros and cons, about fifty “load more comments” ago. ;-) Having really considered it, I think the burden of explaining to users that there’s a new function syntax but it only works when the types can be inferred is too hard. For example, the Go Time Podcast discussed this issue recently and from the conversation, it wasn’t clear to me as a listener if they were aware that the new syntax would only work when types could be inferred and I don’t think a listener who didn’t know that would have gotten that impression. It’s too confusing and subtle as a distinction. So I think it would be better to find ways of adapting the current syntax to infer types. For example, besides using underscore, there could be a go.mod rule that if the version is 1.20 or higher than func(a, b) has inferred types instead of types a and b.

@griesemer
Copy link
Contributor

@griesemer griesemer commented Jun 14, 2022

For example, besides using underscore, there could be a go.mod rule that if the version is 1.20 or higher than func(a, b) has inferred types instead of types a and b.

That means that existing (pre-1.20) might not work if it's simply copied, w/o suitable adjustments or go.mod adjustments. That seems too brittle. We've been very careful to maintain backward-compatibility under almost all circumstances; we've gone through great lengths to ensure it even for such fundamental changes as generics. We shouldn't give that up for syntactic sugar.

The issue at stake here is really a suitable notation that we find palatable after some getting used to (every new notation needs some getting used to).

I'm not concerned about users not being able to understand that the light-weight function syntax "only works when the type types can be inferred". At a first approximation this is always true when a function is passed as an argument to another function (which is the vast majority of use cases). So the rule of thumb is trivial: the light-weight function notation can be used when passing a function to another function. If that extra decision is one decision too many, then simply always use the lightweight function literal notation when passing a function to another function.

As an aside, "type inference" here implies some heavyweight compiler mechanism: the reality is much simpler. The function type is simply copied from the parameter list of the callee (or the LHS expression of an assignment, as the case may be), and then the argument names are suitable replaced by the argument names provided in the lightweight function literal. There's no magic here. The primary benefit of lightweight function literals is removing boilerplate, exactly because the types are simply repeated.

@aarzilli
Copy link
Contributor

@aarzilli aarzilli commented Jun 14, 2022

@griesemer

As an aside, "type inference" here implies some heavyweight compiler mechanism: the reality is much simpler. The function type is simply copied from the parameter list of the callee

I think it's interesting to note how many examples posted in this thread wouldn't work with this. A short list of examples selected from older posts:

#21498 (comment)
#21498 (comment)
#21498 (comment)

Leaving aside my general dislike for the idea, I think the type inference question might have been handwaved away too much.

@DeedleFake
Copy link

@DeedleFake DeedleFake commented Jun 14, 2022

@aarzilli

I'm not sure what you mean. Unless I'm horribly misunderstanding something here, that sure seems like it works with all of those.

@aarzilli
Copy link
Contributor

@aarzilli aarzilli commented Jun 14, 2022

Say you have

func Map[Tin, Tout any](in []Tin, f func(Tin) Tout) []Tout

Map([]int{1, 2, 3}, (x) => { /* body of the function omitted from this example */ })

where does the return type come from? If the type is just copied from the formal arguments it's func(int) Tout which is incomplete.

@DeedleFake
Copy link

@DeedleFake DeedleFake commented Jun 14, 2022

In that case, I would expect the compiler to complain about an inability to infer Tout. But the error is in the call to Map(), not the usage of the anonymous function. If it does the copy before the generic inference, if that's possible, there should be no problem, I think. In other words, it would determine the function signature to be func(Tin) Tout via the copy, and then fill those in via the generic type inference, which would then fail.

@carlmjohnson
Copy link
Contributor

@carlmjohnson carlmjohnson commented Jun 14, 2022

That means that existing (pre-1.20) might not work if it's simply copied, w/o suitable adjustments or go.mod adjustments. That seems too brittle. We've been very careful to maintain backward-compatibility under almost all circumstances; we've gone through great lengths to ensure it even for such fundamental changes as generics. We shouldn't give that up for syntactic sugar.

I would be surprised to learn that very much code actually uses the func (atype, btype) format. Perhaps someone can quantify it with a corpus analysis. If the change were made, it would need to come with a rewriter triggered by go mod edit -version=1.whatever. That might still catch out anyone who manually changed their go.mod version, but at that point, it’s an easy fix for what I think is a small number of users.

@aarzilli
Copy link
Contributor

@aarzilli aarzilli commented Jun 14, 2022

In that case, I would expect the compiler to complain about an inability to infer Tout

Right, which was my point, that people are writing examples of code that won't work.

@DeedleFake
Copy link

@DeedleFake DeedleFake commented Jun 14, 2022

@aarzilli

At least the first example that you linked to doesn't use generics. It should work fine, no type inference required.

@aarzilli

This comment was marked as outdated.

@beoran
Copy link

@beoran beoran commented Jun 14, 2022

@carlmjohnson In Magefiles, the func(type) syntax is used often, so it should stay usable in the future. It is a very handy syntax to simulate namespaces within a single package. That's why we have to find a different syntax for this.

@tv42
Copy link

@tv42 tv42 commented Jun 14, 2022

simulate namespaces within a single package

As far I understand, the Mage trick is more about methods added to dummy types, with the receiver value not being captured. I don't think that has to hinder this.

type MyGrouping mg.Namespace

func (MyGrouping) MyTaskName() error {
        ...
}

@carlmjohnson
Copy link
Contributor

@carlmjohnson carlmjohnson commented Jun 15, 2022

Yes, the change would only need to apply to closures/callbacks where the types are already known from contexts and the other interpretation would be a type error.

@go101
Copy link

@go101 go101 commented Jun 15, 2022

If this proposal is for type inference purpose, I hope it also applies to the following alike cases:

func foo() {
	var x func(int, int) (int, error)
	x = func(a,b) => {
	    c := a + b
	    return c, nil
	}
	...
}

@griesemer
Copy link
Contributor

@griesemer griesemer commented Jun 15, 2022

@go101 Yes it is, as has been said a few times. This is also "assignment context".

@gazerro
Copy link
Contributor

@gazerro gazerro commented Jun 15, 2022

In the discussion so far it has been assumed that since func(x) already has a meaning in Go then this syntax cannot be used, however it does not seem to me that there is ambiguity for the compiler. The type checker knows if x in func(x) is a type or not.

In this example

var f func(a int)
f = func(x) { }

According to the current specification, x must be a type and must be the type int, otherwise the compiler fails with an error.

We can change this spec in this way:

If x is a type, the type must be int. If x is not a type, func(x) { } is a function literal with inferred types and it is validated accordingly.

Examples:

var f func(a int)
f = func(int) { }    // int is a type so the compiler checks that it is the same type as the 'a' parameter
f = func(v) { }      // v is not a type so the compiler infers the type from the type of the 'a' parameter
f = func(v) int { }  // an error occurs: "v is not a type" or "inferred function literals cannot have return types"

Note that we already have cases when a name can be a type or a value. In the x.m expression, x can be a type or a value with diffent meanings.

If we accept this spec, we may also accept the following

sort.Slice(s, func(i, j) s[i] < s[j])

// can be used instead of:

sort.Slice(s, func(i, j int) bool { return s[i] < s[j] })

@hherman1
Copy link

@hherman1 hherman1 commented Jun 15, 2022

@gazerro it was a goal of the generics proposal to keep code parsable without evaluating types. I suspect that constraint applies here too.

@DeedleFake
Copy link

@DeedleFake DeedleFake commented Jun 15, 2022

Actually, he might be onto something. Since this is only in an assignment context, the compiler might be able to copy the types without evaluating them. In other words, given the fact that you're assigning to func(int) int, anything other than func(int) int would be illegal in current Go, so if there are only types given and they're not the right ones, assume it to be new identifiers instead. Then do normal type checking later.

In other words, given var f func(int) int, if I try to do f = func(x), automatically expand that to f = func(x int) int by copying the old types, then do type checking normally. All existing code of that kind would be illegal, unless it exactly matched with possible aliasing, in which case it'll get expanded to f = func(int int) (int int). In that case, you can add a flag that says that it's been expanded, and then unexpand it during the type checking phase if the identifiers match like that.

I don't know how feasible any of this is, but it actually sounds vaguely plausible to me.

Edit: To clarify, what I'm saying is that an assignment involving a function literal with at least its argument or return list involving only types is put into an ambiguous state that can then be tracked and cleared up during type checking, Unlike the generics problem with x, y = a < b, c > (d), its not actually resolving a syntactic ambiguity with completely separate grammars, Rather, it's just resolving an ambiguity in terms of what type of identifier is being declared, if any.

That being said, this could result in more confusion, though I doubt it would be too bad. For example, something like doSomething(a, func(x, y) { ... }) might not look obvious if x and y are actually types or are arguments, but with the body it would be pretty obvious, such as doSomething(a, func(x, y) { return x + y }, because you can't use a type like that.

@gazerro
Copy link
Contributor

@gazerro gazerro commented Jun 15, 2022

@hherman1 I think this goal remains. In what cases, with the new spec, does the parser need to know the types to parse the code?

@gazerro
Copy link
Contributor

@gazerro gazerro commented Jun 15, 2022

@DeedleFake

That being said, this could result in more confusion, though I doubt it would be too bad. For example, something like doSomething(a, func(x, y) { ... }) might not look obvious if x and y are actually types or are arguments, but with the body it would be pretty obvious, such as doSomething(a, func(x, y) { return x + y }, because you can't use a type like that.

I think the cases where you read doSomething(a, func(x, y) { ... }) in code and x and y are types are very rare. And in case it is not evident when reading the type names and the function body that they are types, the code can be written as doSomething(a, func(_, _) { ... }) with the new spec.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Go2 LanguageChange Proposal
Projects
None yet
Development

No branches or pull requests