golang / go

The Go programming language
https://go.dev
BSD 3-Clause "New" or "Revised" License
121.61k stars 17.41k forks source link

proposal: Go 2: Lightweight anonymous function syntax #21498

Open neild opened 6 years ago

neild commented 6 years ago

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

jimmyfrasche commented 4 years ago

I removed my :-1:. I'm still not :+1: on it but I am reconsidering my position. Generics are going to cause an increase in short functions like genericSorter(slice, func(a, b T) bool { return a > b }). I also found https://github.com/golang/go/issues/37739#issuecomment-624338848 compelling.

There are two major ways being discussed for making function literals more concise:

  1. a short form for bodies that return an expression
  2. eliding the types in function literals.

I think both should be handled separately.

If FunctionBody is changed to something like

FunctionBody = Block | "->" ExpressionBody
ExpressionBody = Expression | "(" ExpressionList ")"

that would mostly help function literals with or without type elision and would also allow very simple function and method declarations to be lighter on the page:

func (*T) Close() error -> nil

func (e *myErr) Unwrap() error -> e.err

func Alias(x int) -> anotherPackage.OriginalFunc(x)

func Id(type T)(x T) T -> x

func Swap(type T)(x, y T) -> (y, x)

(godoc and friends could still hide the body)

I used @ianlancetaylor's syntax in that example, the major downside of which is that is that it requires the introduction of a new token (and one that would look odd in func(c chan T) -> <-c!) but it might be okay to reuse an existing token such as "=", if there's no ambiguity. I'll use "=" in the remainder of this post.

For type elision there are two cases

  1. something that always works
  2. something that only works in a context where the types can be deduced

Using named types like @griesemer suggested would always work. There seem to be some issues with the syntax. I'm sure that could be worked out. Even if they were, I'm not sure it would solve the problem. It would require a proliferation of named types. These would either be in the package defining the place where they're used or they would have to be defined in every package using them.

In the former you get something like

slices.Map(s, slices.MapFunc(x) = math.Abs(x-y))

and in the latter you get something like

type mf func(float64) float64
slices.Map(s, mf(x) = math.Abs(x-y))

Either way there's enough clutter that it doesn't really cut the boilerplate down much unless each name is used a lot.

A syntax like @neild's could only be used when the types could be deduced. A simple method would be like in #12854, just list every context where the type is known—parameter to a function, being assigned to a field, sent on a channel, and so on. The go/defer case @neild brought up seems useful to include, as well.

That approach specifically does not allow the following

zero := func = 0
var f interface{} = func x, y = g(y, x)

but those are cases where it would pay to be more explicit, even if it were possible to infer the type algorithmically by examine where and how those are used.

It does allow many useful cases, including the most useful/requested:

slices.Map(s, func x = math.Abs(x-y))
v := cond(useTls, FetchCertificate, func = nil)

being able to choose to use a block independent of the literal syntax also allows:

http.HandleFunc("/bar", func w, r {
  // many lines ...
})

which is a particular case increasingly pushing me toward a :+1:

One question that I haven't seen raised is how to deal with ... parameters. You could make an argument for either

f(func x, p = len(p))
f(func x, ...p = len(p))

I don't have an answer to that.

Splizard commented 4 years ago

@jimmyfrasche

  1. eliding the types in function literals.

I believe this should be handled with the addition of function-type literals. Where the type replaces 'func' and the argument types are emitted (as they are defined by the type). This maintains readabillity and is fairly consistent with the literals for other types.

http.Handle("/", http.HandlerFunc[w, r]{
    fmt.Fprinf(w, "Hello World")
})
  1. a short form for bodies that return an expression

Refactor the function as its own type and then things become much cleaner.

type ComputeFunc func(float64, float64) float64

func compute(fn ComputeFunc) float64 {
    return fn(3, 4)
}

compute(ComputeFunc[a,b]{return a + b})

If this is too verbose for you, then type alias the function type inside your code.

{
    type f = ComputeFunc

    compute(f[a,b]{return a + b})
}

In the special case of a function with no arguments, the brackets should be omitted.

type IntReturner func() int

fmt.Println(IntReturner{return 2}())

I pick square brackets because the contracts proposal is already using extra standard brackets for generic functions.

jimmyfrasche commented 4 years ago

@Splizard I stand by argument that that would just push the clutter out of the literal syntax into many extra type definitions. Each such definition would need to be used at least twice before it could be shorter than just writing the types in the literal.

I'm also not sure it would play too well with generics in all cases.

Consider the rather strange function

func X(type T)(v T, func() T)

You could name a generic type to be used with X:

type XFunc(type T) func() T

If only the definition of XFunc is used to derive the types of the parameters, when calling X you'd need to tell it which T to use even though that's determined by the type of v:

X(v, XFunc(T)[] { /* ... */ })

There could be a special case for scenarios like this to allow T to be inferred, but then you'd end up with much of the machinery as would be needed for type elision in func literals.

You could also just define a new type for every T you call X with but then there's not much savings unless you call X many times for each T.

billinghamj commented 3 years ago

I think this could play really nicely with Go generics too - making slightly more functional styles of programming easy to do.

eg Map(slice, x => x.Foo)

beoran commented 3 years ago

@jimmyfrasche

The short function body syntax you propose by itself would already be extremely useful, seeing that in well factored code, we often have a lot of one-line function bodies. This also seems a lot easier to implement than the type inference for function literal parameters, the proposal could be short, and if accepted, it could be implemented quickly. So perhaps we should split off the short function body syntax as a separate issue?

jimmyfrasche commented 3 years ago

I think they're related enough that they should be considered together, but I'm happy to file a separate proposal if someone from the review team thinks that would be helpful.

I doubt there will be any language changes until generics are in, regardless of how easy they are to implement.

egonelbre commented 3 years ago

One wild idea, maybe quite a few of the lightweight-syntax needs can be handled by adding an implicit accessors for fields.

type Person struct {
    Name string
}
// equivalent to `fn := func(p *Person) string { return p.Name }`
fn := (*Person).Name
p := &Person{Name: "Alice"}
fmt.Println(fn(p))
// prints "Alice"

This would mesh nicely with generics:

names := slices.Map(people, (*Person).Name)

This didn't seem large enough idea to make a separate proposal.

seh commented 3 years ago

I assume you meant to write:

// equivalent to `fn := func(p *Person) string { return p.Name }`

That is, dereference p to reach its "Name" field.

gonzojive commented 3 years ago

Is there a more detailed proposal for the algorithm that derives types? I see

I'd avoid trying to infer the types of paramaters based on the function body, instead add inference only for cases where it is obvious (like passing closures to functions).

I suspect some amount of inference based on the function body is helpful. e.g.

var friends []person
fmt.Printf("my friends' names: %v", functional.Map(friends, (f) => f.name()))

That seems like something programmers would expect to work. It seems the context and function body will both narrow the number and assignment of allowed types for ins/outs. If not, adding partial type information would make life easier:

var friends []person
fmt.Printf("my friends' names: %v", functional.Map(friends, (f): string => f.name()))

Which raises another question: Will types be allowed for some parameters and return values and not others?

lukechampine commented 3 years ago

Might as well mention here that I forked the Go compiler to support extremely concise lambda syntax: https://twitter.com/lukechampine/status/1367279449302007809?s=19

Both parameter and return types are inferred, so lambdas can only be used in contexts where those types can be determined at compile time. For example, this would not compile:

fn := \ x + x

but this would:

var fn func(int) int = \ x + x

The body assumes that parameters are named x,y,z,a,etc. This is pretty radical and I highly doubt it would be adopted, but it's fun to see how far such things can be pushed.

The fork also supports new builtins methods on slices, which solves the other big ergonomics issue with map/filter/reduce, but that's not relevant to this issue.

dolmen commented 3 years ago

I'm against lambda expressions that would not have the func keyword.

I like that the func keyword directly shows me there is a function definition here.

I don't want extremely concise lambda expressions that would hide function definition and reduce readability.

Note: I'm also in favor of adding syntax to reduce the use of short lived functions (anonymous function defined and immediately executed: a function definition followed by a list of arguments in the same expression) because that are cases where the func keyword is pollution.

DmitriyMV commented 3 years ago

@dolmen I think

func(x,y) => x*y

would fit this definition pretty well. Although I'm unsure about x*y part without return, since it would require compiler to infer anonymous function return type (or lack of thereof) from expression directly.

I also pretty sure that any significant syntax changes like this are delayed until generics work is completed. I also pretty sure that we should talk about anonymous function syntax we generics syntax in mind.

DeedleFake commented 3 years ago

Branching off of #47358, the more that I've been looking at the newly proposed generics-based packages for the standard library, the more that I'm convinced that some kind of short-form function syntax is a good idea. Code like

slices.EqualFunc(s1, s2, func(v1, v2 int) bool {
  return v1 == v2
})

is already adding a fair amount of bulk just for a single function call, along with coupling the function definition to explicit types that make maintenance more difficult later. More complicated calls can be much, much worse. For example, the discarded slices.Map() function:

names := slices.Map(make([]string, 0, len(users)), users, func(user User) string { return user.Name })

One thing that I've noticed though is that a good 99% of the situations in which short-form functions are useful are as arguments to function calls. What if there was an alternate syntax for short-form anonymous function declarations that was only available as a function call argument? In other words, function arguments would go from just being any expression to being Expression | ShortAnonymousFunction, or something, but the syntax wouldn't be legal anywhere else.

Also, while I think that the most important thing for a short-form function is that it have inferred argument types, some way of doing single-expression functions without requiring return would be nice, too.

And, while I'm at it, I may as well bikeshed a bit. I've become quite partial to the Kotlin syntax recently. I think a variant of it could work pretty well for Go, though I'm not exactly stuck on it if syntax disagreements are the primary delay in implementation:

// Single-expression body, so no explicit return is necessary.
slices.EqualFunc(s1, s2, { v1, v2 -> v1 == v2 })

// Body has multiple expressions, so an explicit return is necessary.
slices.EqualFunc(s1, s2, { v1, v2 ->
  d := v1 - v2
  return (d < 3) && (d > -3)
})

// Earlier map example:
names := slices.Map(make([]string, 0, len(users)), users, { user -> user.Name })

That last example still needs an explicit type at the call-site because of the make() call, but maybe that could be fixed with something like #34515.

ct1n commented 2 years ago

Maybe

func compute(func(float64, float64) float64)
compute(func(a, b float64) float64 { return a + b })

type G func(int) func(int) int
var g G = func(a int) func(int) int { return func(b int) int { return a + b } }

becomes

func compute(func(float64, float64) float64)
compute(func(a, b): a + b)

type G func(int) func(int) int
var g G = func(a): func(b): a + b

and perhaps also allowing

var h = func(a int): func(b int): a + b
zippoxer commented 2 years ago

@ct1n I really like this syntax for Go!

The func keyword is consistent with the language and a lot more glance-able than () =>.

With type inference:

func(a, b): a + b
DeedleFake commented 2 years ago

I like the syntax. It's not my favorite, but I could live with it. I'm particularly not fond of the colon. I'd prefer something like ->, as it's more visible at a glance.

In my opinion, the biggest reason for this is the type inference, not the expression body, though that's also nice. I'd definitely want a short syntax that supports full function bodies:

func LogHandler(h http.Handler) http.Handler {
  return http.HandlerFunc(func(rw, req) -> {
    log.Printf("%v %v", req.Method, req.URL)
    h.ServeHTTP(rw, req)
  })
}
ianlancetaylor commented 2 years ago

I think that if we do something in this area, there is one initial choice to make: whether to keep the func keyword or not.

If we keep the func keyword, then I think the most natural syntax is to permit omitting the types of the parameters, omitting the results, and permit omitting the return keyword if there is only a single expression (or sequence of expressions).

    func(a, b) { a + b }

This would be permitted in any assignment where the type of the left hand side is known (this includes function calls).

In this version, the block can contain multiple statements just like an ordinary function literal, but in that case the return keyword may not be omitted.

If we omit the func keyword, then I at least think that we need some specific operator. Many languages use the => operator for things like this. In that case, we have

    (a, b) => a + b

and perhaps

    (a int, b float64) => a + int(b)

In this version, the right hand side of => can only be an expression.

We could support all of these forms if we really want to.

I think that if we do something here we need to be careful to not stray too far syntactically from what Go permits today. I think that the options here do that.

neild commented 2 years ago

func(a, b) { a + b } is ambiguous, isn't it? a and b could be either types or parameter names. I proposed above that we could resolve that ambiguity by dropping the parentheses in the short form: func a, b { a + b }.

ianlancetaylor commented 2 years ago

Sigh, you're quite right. Missed that. Thanks.

I'm not too fond of func a, b { a + b } because of the syntactic difference. Hmmm.

jimmyfrasche commented 2 years ago

If the inferred-type and explicit type syntax are the same it's a bit confusing that (x, y int) means x is int instead of being inferred and someone under that confusion will try to write something like (x, y int, z).

Using => is fine by me but limiting the short syntax to only an expression is going to end up like Python's lambda: which is not a popular decision.

I stand by my previous comment https://github.com/golang/go/issues/21498#issuecomment-633140695 that the two things should be kept orthogonal: allow func a, b { return a + b } and func a, b => a + b and func(a, b int) int => a + b and func (Type) Close() error => nil

griesemer commented 2 years ago

If we consider syntactic sugar for simple (single-expression) function literals we should actually make them short and concise and leave away the func keyword; otherwise, why bother.

faiface commented 2 years ago

@griesemer I think (args...) => result would be perfect.

(x) => x + 1
(x, y) => x + y
(person) => person.Age >= 18
neild commented 2 years ago

If we consider syntactic sugar for simple (single-expression) function literals we should actually make them short and concise and leave away the func keyword; otherwise, why bother.

It's worth asking whether the goal is to make single-expression function literals extremely concise, or whether it is to permit eliding inferable types from longer literals. For example, the func passed to filepath.Walk or testing.T.Run will rarely be a single-expression function.

I worry that syntax that applies only to single-expression functions would interact poorly with Go's explicit error handling. I suspect that languages which uses exception-based error handling have more single-expression functions than Go, which often requires additional error handling states. Providing sugar for single-expression functions might encourage improperly discarding errors to avoid adding additional expressions to a function.

griesemer commented 2 years ago

For example, the func passed to filepath.Walk or testing.T.Run will rarely be a single-expression function.

Agreed. But then what's the point of saving a few keystrokes. The respective functions bodies will dominate the code and in those cases one probably wants to see all the types.

I worry that syntax that applies only to single-expression functions would interact poorly with Go's explicit error handling.

Can you provide a concrete example for this? If a single-expression function returns an error, it's (probably) a multi-valued expression, so it could only be used (if at all) in a suitable assignment. But I can only think of contrived examples.

jimmyfrasche commented 2 years ago

Agreed. But then what's the point of saving a few keystrokes. The respective functions bodies will dominate the code and in those cases one probably wants to see all the types.

The names of the parameters should be sufficient for readability given the context of the function it's being passed to. The majority of the time the types are just things your eye has to leap over to get to the next relevant bit and they're very easy to look up if it becomes relevant. If the types are important to understanding the code (a) you're probably doing something too tricky and (b) you can still write them out by using the less concise syntax.

neild commented 2 years ago

Can you provide a concrete example for this?

I don't have a specific example to mind, but my thought is that since any call chain in Go which can contain errors must include explicit error propagation there are probably fewer cases of passing around functions that return a single value than in languages where errors are passed up as exceptions.

But then what's the point of saving a few keystrokes.

That applies to any type inference, doesn't it?

I don't see much difference between eliding the types in these two ways of fetching a row; in both cases, you're just saving a few keystrokes:

var row *Row = iter.Next() // explicit
row := iter.Next()         // implicit

iter.Do(func(row *Row) { }) // explicit
iter.Do(func row { })       // implicit
lukechampine commented 2 years ago

Can you provide a concrete example for this? If a single-expression function returns an error, it's (probably) a multi-valued expression, so it could only be used (if at all) in a suitable assignment. But I can only think of contrived examples.

func unwrap[T any](v T, err error) T {
    if err != nil {
        panic(err)
    }
    return v
}

strs := []string{"1", "2", "3"}
ints := slices.Map(strs, (s) => unwrap(strconv.Atoi(s)))

IMO, this is part of a larger point: if lambdas look nicer than normal functions, people will try to contort their code so that it can use lambdas. Not really much we can do about this, except to anticipate the sort of contortions people are likely to reach for, and provide better alternatives (e.g. higher-order functions that play nice with error returns).

DeedleFake commented 2 years ago

I've never liked the discrepancy between => expr and { stmts } that a lot of languages have. It makes converting back and forth kind of awkward, for one thing, which I think is part of the cause of people contorting code to fit the single-expression syntax.

Some languages try and get around it by using the normal syntax and then implicitly returning if the last thing in the function is a single, otherwise unused expression, but that has its own issues, including pointless syntactic overlap in larger functions and an unclear return point depending on control flow.

How about a compromise? Keep the same syntax, but allow an implicit return if and only if the function body is a single expression. In other words:

// Legal.
func Example(a, b int) int { a + b }

// Not legal.
func Example(a, b int) int {
  c := a + b
  c
}

That would make converting back and forth easier, as you'd only need to add return then, but it would also remove some of the readability problems that stem from long functions with implicit returns. The same rule would then apply to anonymous functions and, presumably, any type-inferred anonymous functions that this discussion leads to.

Also, along with error returns, I think it's worth mentioning that the lack of any conditional expression in Go somewhat limits a lot of single-expression function usages, making easy conversion to a regular function body even more necessary, in my opinion.

gonzojive commented 2 years ago

To support either statements or expressions in the body, why not do what JavaScript and Typescript do? Allow both, and differentiate with curly brackets:

(a, b) => a + b (a, b) => { if b == 0 { return 0, fmt.Errorf("div %v by zero", a) } return a/b }

This was probably already mentioned.

(As a separate matter, I'm not sure how the operator version of this allows partial or complete specification of return types.)

On Wed, Jan 5, 2022, 5:20 PM DeedleFake @.***> wrote:

I've never liked the discrepancy between => expr and { stmts } that a lot of languages have. It makes converting back and forth kind of awkward, for one thing, which I think is part of the cause of people contorting code to fit the single-expression syntax.

Some languages try and get around it by using the normal syntax and then implicitly returning if the last thing in the function is a single, otherwise unused expression, but that has its own issues, including pointless overlap in larger functions and an unclear return point depending on control flow.

How about a compromise? Keep the same syntax, but allow an implicit return if and only if the function body is a single expression. In other words:

// Legal.func Example(a, b int) int { a + b } // Not legal.func Example(a, b int) int { c := a + b c }

That would make converting back and forth easier, as you'd only need to add return then, but it would also remove some of the readability problems that stem from long functions with implicit returns. The same rule would then apply to anonymous functions and, presumably, any type-inferred anonymous functions that this discussion leads to.

Also, along with error returns, I think it's worth mentioning that the lack of any conditional expression in Go somewhat limits a lot of single-expression function usages, making easy conversion to a regular function body even more necessary, in my opinion.

— Reply to this email directly, view it on GitHub https://github.com/golang/go/issues/21498#issuecomment-1006207988, or unsubscribe https://github.com/notifications/unsubscribe-auth/AAAUO553VYWWXS4Z4UJYCUDUUTU5FANCNFSM4DXKSBAQ . You are receiving this because you commented.Message ID: @.***>

sa- commented 2 years ago

Now that we have generics, it would make code so much more readable if we could have this with accompanying collection methods

What currently works with golang 1.18

func main() {
    original := []int{1, 2, 3, 4, 5}
    newArray := Map(original, func(item int) int { return item + 1 })
    newArray = Map(newArray, func(item int) int { return item * 3 })
    newArray = Filter(newArray, func(item int) bool { return item%2 == 0 })
    fmt.Println(newArray)
}

func Map[T, U any](s []T, f func(T) U) []U {
    r := make([]U, len(s))
    for i, v := range s {
        r[i] = f(v)
    }
    return r
}

func Filter[T any](s []T, f func(T) bool) []T {
    r := make([]T, len(s))
    counter := 0
    for i := 0; i < len(s); i++ {
        if f(s[i]) {
            r[counter] = s[i]
            counter++
        }
    }
    return r[:counter]
}

I would like to do something like this (or with some syntax that is equally concise):

func main() {
    original := []int{1, 2, 3, 4, 5}
    newArray := original.
        Map((item) -> item + 1).
        Map((item) -> item + 3).
        Filter((item) -> item%2 == 0)
    fmt.Println(newArray)
}

Note that the Map and Filter methods would be provided on slices instead of having to define them.

I hope that this example helps to convey that the conciseness doesn't come at the cost of readability, but it actually helps readability instead.

billinghamj commented 2 years ago

@sa- I think the chaining you've shown there realistically isn't possible without some kind of specific support for a pipelining operator. It's extremely unlikely that maps/slices/arrays will get methods added to them in such a way which allows [].Map etc.

e.g. original.[Map]((item int) int => item + 1) - syntax is obviously highly debatable, I think there was a JS proposal which looked more like original |> Map(%, item => item + 1)

I think for the arrow functions to really be useful, the typing should be inferred the same way it is typical variable assignment (:=) - you often have complex types in these situations, which would make the line unreasonably long. Changing func (...) { return ... } to (...) => ... is not much of a saving when the majority of the space is the typing anyway

sa- commented 2 years ago

I would like to clarify that my proposal shouldn't need a pipelining operator, nor am I particularly interested in such an operator.

If the Map and Filter methods are provided on slice and array types from the language, this shouldn't be necessary

beoran commented 2 years ago

@sa- looks like you just rediscovered Ruby stabby lambdas. Which is a good notation for one line function parameters.

f-feary commented 2 years ago

Discussing adding methods to slice and map types is a huge left turn from what this PR is supposed to be about, which is a shorthand for func literals.

Given the pattern long established for len, cap, delete, close, the entire std/strings package, and most recently the exp/slices and exp/maps packages, adding methods to primitive types would be a significant change deserving of it's own issue to track and discuss.

billinghamj commented 2 years ago

If the Map and Filter methods are provided on slice and array types from the language, this shouldn't be necessary

Discussing adding methods to slice and map types is a huge left turn from what this PR is supposed to be about

This has been discussed quite a bit, and has always been a pretty clear no. I think the chance of that ever happening in Go is pretty close to zero

ianlancetaylor commented 2 years ago

@bradfitz and @griesemer and I discussed for a while, and wound up reinventing @neild 's suggestion from https://github.com/golang/go/issues/21498#issuecomment-355125728, fleshed out a bit at https://github.com/golang/go/issues/21498#issuecomment-355353058 and https://github.com/golang/go/issues/21498#issuecomment-355427633.

In any context where an expression has a known function type, you are permitted to write func identifier-list { body }. There are no parentheses around the identifier-list, which distinguishes this case from that of a normal function literal. The types of the parameters and the results are taken from the known type. We can go further and say that if the body is an expression or list of expressions, the return keyword may be omitted.

For the initial expression in this issue this permits

    compute(func a, b { a + b })

This seems to be unambiguous and reasonably short. The main drawbacks are that it doesn't work with := (because the type is not known) and the fact that the syntax is perhaps too similar to ordinary func syntax in that it hinges on the absence of parentheses.

This was discussed above and then the discussion moved on. Does anybody see any problems with this? Thanks.

jimmyfrasche commented 2 years ago

so f(func a, b { a + b }) and f(func a, b { return a + b }) are identical?

ianlancetaylor commented 2 years ago

Yes.

DeedleFake commented 2 years ago

It looks a little odd to me without the parentheses, but overall I think it'll work quite nicely. The inclusion of func makes it slightly longer than I'd like, but it's still quite reasonable, definitely.

The lack of a short syntax has definitely become a bit more of an annoyance with generics, so I'm looking forward to something like this potentially being added. Not only will it make code more readable, it should help with refactoring, too.

Edit: Example of a few small cleanups from one of my own projects:

// Old:
slices.SortFunc(peers[1:], func(p1, p2 *ipnstate.PeerStatus) bool {
    return p1.HostName < p2.HostName
})

slices.EqualFunc(peers, old, func(p1, p2 *ipnstate.PeerStatus) bool {
    return p1.HostName == p2.HostName && slices.Equal(p1.TailscaleIPs, p2.TailscaleIPs)
})

// New:
slices.SortFunc(peers[1:], func p1, p2 { p1.HostName < p2.HostName })

slices.EqualFunc(peers, old, func p1, p2 {
    p1.HostName == p2.HostName && slices.Equal(p1.TailscaleIPs, p2.TailscaleIPs)
})

That first one in particular becomes significantly cleaner.

jimmyfrasche commented 2 years ago

Does f(func { fmt.Println("foo") }) require f take a func(), a func() (int, error), or in either case?

DeedleFake commented 2 years ago

@jimmyfrasche

I'd say that the type should be inferred from the usage, not from the return type. If you're passing it to something, and that thing wants a function with no returns, then the body being an expression is just ignored and there's no automatic, and incorrect, return. Otherwise, treat it as a return and do the type checking the usual way.

zigo101 commented 2 years ago

Personally, I think that increases the load of cognition burden much.

beoran commented 2 years ago

@jimmyfrasche In the case of func { fmt.Println("foo") } f must take an func() (int, error), of course. It is the most consistent and type safe way.

We could then allow to writef(func { _,_ = fmt.Println("foo") }) for an f that takes a func().

jimmyfrasche commented 2 years ago

Can you do func { nil, io.EOF } or would you need a return?

Does anything special need to be done for variadic parameters or are they silently variadic?

I'm :+1: on doing something here and :+1: on using func without ().

I'm neither :+1: or :-1: on overloading {} on this but leaning toward :-1:. I prefer a separate syntax as I've said earlier. It keeps it concise while staying clear enough at a glance

Reusing {} would also cause issues should #12854 be accepted as then func {{}} is legal and even if that's fine I imagine it would be easy to forget those outer {}.

beoran commented 2 years ago

func { nil, io.EOF } seems ok to me even with #12854 . Arguably this is a func literal, so it seems consistent.

magical commented 2 years ago

Reusing {} would also cause issues should https://github.com/golang/go/issues/12854 be accepted as then func {{}} is legal and even if that's fine I imagine it would be easy to forget those outer {}.

func() {{}} is already legal (a function containing a single, empty block). I would expect the paren-less version to have the same meaning.

MrTravisB commented 2 years ago

I would really like to see some kind of better syntax for anonymous functions but this seems like a marginal gain. The only real benefit is removing the need for a return statement for single line functions. Removing the parens around params actually makes it move difficult to read in my opinion.

ianlancetaylor commented 2 years ago

Does f(func { fmt.Println("foo") }) require f take a func(), a func() (int, error), or in either case?

Good question. I think that either case should compile.

ianlancetaylor commented 2 years ago

@MrTravisB

The only real benefit is removing the need for a return statement for single line functions.

I think the bigger benefit is not having to explicitly write the parameter and result types.

Of course, that may also make the code harder to read in some cases.

aarzilli commented 2 years ago

For the initial expression in this issue this permits

    compute(func a, b { a + b })

To me this looks too much like passing two arguments to compute: func a and b { a + b }.