golang / go

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

proposal: spec: lightweight anonymous function syntax #21498

Open neild opened 7 years ago

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

f-feary commented 2 years ago

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

It may be worth having the discussion of implicit return statements / single expression bodies, but I feel like that may be a distinct feature request.

Removing the type information, which adds a lot of code yet could be cheaply inferred, is definitely the obvious benefit of a shorthand syntax.

sammy-hughes commented 2 years ago

Coming in late as I had no idea there was an active discussion on a ticket like this. I'm literally gathering notes for a proposal that would have been a dup of this.

@griesemer suggested, as many have echoed, that the func keyword can be dropped. I think that misses the point. Back in September, @ct1n suggested a syntax that efficiently handles my pain-points, presents limited difficulty when lexing, and is effectively type-able.

The following are my biggest pain points:

  1. mentally computing the signature is more work than writing the actual logic,
  2. deeply-nested fsigs are legitimately hard to read
  3. IIF's become a sea of parentheses and braces.

My pain points all go away if I could elide the return type for single-expression functions, especially since I almost exclusively use this pattern to simplify terms, either to implement operators over properties of a structured type, or to elide terms from the final callsite by currying.

Here is a real-world example I was wrestling with last night. The class of behavior is a backoff-context, but here that's shown via immediately-invoked closures. I was writing a quadratically-scaling backoff timer as a function over attempts:

    quadraticCursor := (
        func(a, b int) func(int)int {
            return (
                func(c int) func(int)int {
                    return func(x int) int {
                        return a*x*x+b*x+c
                    }
                }
            )(a+b)
        )(Alpha.PropX.AsInt(), Beta.PropY.AsInt())
)

Call it a contrived case, but this is a real-world example, and I hate it. 90% of that is just dealing with Go versus actual logic.

In contrast, the following, any version of the syntax, would be magnificent!

ApproachA := (func(a, b int) (func(c int) func(x int) a*x*x+b*x+c)(a+b))(Alpha.PropX.AsInt(), Beta.PropY.AsInt())
ApproachB := (func(a, b int) return (func(c int) return func(x int) return a*x*x+b*x+c)(a+b))(Alpha.PropX.AsInt(), Beta.PropY.AsInt())
ApproachC := (func(a, b int):  (func(c int): func(x int): a*x*x+b*x+c)(a+b))(Alpha.PropX.AsInt(), Beta.PropY.AsInt())
ApproachD := (func(a, b int): return (func(c int): return func(x int): return a*x*x+b*x+c)(a+b))(Alpha.PropX.AsInt(), Beta.PropY.AsInt())
ApproachE := (a, b => (c => x => a*x*x+b*x+c)(a+b))(Alpha.PropX.AsInt(), Beta.PropY.AsInt())
ApproachF := (a, b int => (c int => x int => a*x*x+b*x+c)(a+b))(Alpha.PropX.AsInt(), Beta.PropY.AsInt())
ApproachG := (a, b -> (c -> x -> a*x*x+b*x+c)(a+b))(Alpha.PropX.AsInt(), Beta.PropY.AsInt())
ApproachH := (a, b int -> (c int -> x int -> a*x*x+b*x+c)(a+b))(Alpha.PropX.AsInt(), Beta.PropY.AsInt())
ApproachI := (func a, b {(func c {func x {a*x*x+b*x+c})(a+b)})(Alpha.PropX.AsInt(), Beta.PropY.AsInt())
ApproachJ := (func a, b int {(func c int {func x int {a*x*x+b*x+c})(a+b)})(Alpha.PropX.AsInt(), Beta.PropY.AsInt())
ApproachK := (func a, b int {(return (func c int {return (func x int {return a*x*x+b*x+c})(a+b)})(Alpha.PropX.AsInt()), Beta.PropY.AsInt())

I personally think Approaches B and D are the most flexible with the fewest sacrifices, while still looking very much like existing Go code.

f-feary commented 2 years ago

@sammy-hughes It seems like what you're looking for is just inferring the return type from any function body, as opposed to inferring more or less the whole function signature from its usage (as an anonymous function), which seems to be the common denominator with most of the suggestions floated around here.

However I would say that approaches A-K are all horrifyingly difficult to reason with, in my opinion even moreso than the original.

leaxoy commented 2 years ago

Don't create new ugly syntax, just learn from other language pls.

bradfitz commented 2 years ago

A concrete example I shared with @griesemer and @ianlancetaylor earlier.

Imagine code like this:

// Read is a generic version of DB.Read to use a func that returns (T, error)
// instead of the caller having to store the result into a closed over variable.
//
// It runs fn with a new ReadTx repeatedly until fn returns either success or
// an error that's not an etcd.ErrTxStale.
func Read[T any](ctx context.Context, db *DB, fn func(rx *Tx) (T, error)) (T, error) {
    var ret T
    err := db.Read(ctx, func(rx *Tx) (err error) {
        ret, err = fn(rx)
        return
    })
    if err != nil {
        var zero T
        return zero, err
    }
    return ret, nil
}

Currently, to use that function, caller code ends up looking like:

    authPath, err := cfgdb.Read(ctx, s.db, func(rx *cfgdb.Tx) (*dbx.AuthPath, error) {
        return rx.AuthPath(r.URL.Path)
    })
...
    usage, err := cfgdb.Read(ctx, s.db, func(rx *cfgdb.Tx) (billingpolicy.Usage, error) {
        return billingpolicy.BillingUsage(rx, user.Domain)
    })
...
    return cfgdb.Read(ctx, grh.ctl.DB(), func(rx *cfgdb.Tx) (scim.Resource, error) {
        return grh.getRx(rx, scimID)
    })

... which is pretty annoying, writing all the types.

Under the latest proposal, that becomes:

    authPath, err := cfgdb.Read(ctx, s.db, func rx { rx.AuthPath(r.URL.Path) })
...
    usage, err := cfgdb.Read(ctx, s.db, func rx { billingpolicy.BillingUsage(rx, user.Domain) })
...
    return cfgdb.Read(ctx, grh.ctl.DB(), func rx { grh.getRx(rx, scimID) })

Which IMO is great.

seankhliao commented 2 years ago

would you be able to do

func x { something; somethingelse }

if yes, what would the canonical style look like, and how do you make the judgement call between old and new style

DeedleFake commented 2 years ago

@seankhliao

We can go further and say that if the body is an expression or list of expressions, the return keyword may be omitted.

If I'm reading it correctly, then no, that would not be allowed unless somethingelse included a return or it was a void function. The elision of return is only allowed if the body consists entirely of a single expression or comma-separated list of expressions. Essentially, if you can write return <something to return>, you can remove the return keyword if the entire body is just <something to return>.

seankhliao commented 2 years ago

I meant for my example to represent functions that span multiple lines, and the urge to cram it all into a single line

f-feary commented 2 years ago

@seankhliao Simple: Don't. The objective is to make code easier to read and write; not harder.

neild commented 2 years ago

@bradfitz

Under the latest proposal, that becomes:

authPath, err := cfgdb.Read(ctx, s.db, func rx { rx.AuthPath(r.URL.Path) })

Does it? That requires a very complex interaction with type inference:

func rx { rx.AuthPath(r.URL.Path) }

Here we know that we have a func that takes a single parameter. We don't know the type of that parameter, we don't know whether the func returns any parameters or the types of those parameters (if any). If we assign the literal to a variable with a known type, however, we immediately know all of that.

However, in this case we're assigning this func to a variable (function input parameter) of type func(*cfgdb.Tx) (T, error) for some as-yet-unknown T.

Type inference needs to identify the type of rx as *cfgdb.Tx, resolve the return types of the func based on that, and then use those return types to identify the type T. We need to dance between deriving the type of the func literal and the type parameter of Read. That may be technically possible, but I don't think its implicit in this proposal that it would be allowed. I don't believe there are any existing cases in the current generics design where we perform type inference using an unknown or partially-specified type as input.

ianlancetaylor commented 2 years ago

@neild I think that we would want to apply "lightweight function type determination" before type inference. In this example, we would see that the type of the function is func(*cfgdb.Tx) (T, error). So we know that rx is *cfgb.Tx, and that in turn tells us the type of the result of the function body rx.AuthPath(r.URL.Path) (in this case *dbx.AuthPath, error). All of that happens before type inference.

When we get to type inference, we have to infer that T is *dbs.AuthPath, which is straightforward.

Perhaps I am missing something.

ianlancetaylor commented 2 years ago

@seankhliao

would you be able to do

func x { something; somethingelse }

if yes, what would the canonical style look like, and how do you make the judgement call between old and new style

Yes, you could write that. But you can already write your function literals that way today.

    sort.Slice(s, func(a, b int) bool { if a < 0 { panic("!!") }; return a < b })

I'm not sure this syntax suggestion introduces any wholly new problems.

neild commented 2 years ago

@ianlancetaylor Consider these cases:

// Case A: authPath is inferred to be *dbx.AuthPath
authPath, err := cfgdb.Read(ctx, s.db, func rx { rx.AuthPath(r.URL.Path) })

// Case B: authPath is explicitly `any`.
authPath, err := cfgdb.Read[any](ctx, s.db, func rx { rx.AuthPath(r.URL.Path) })

In case B, we have explicitly defined the type parameter, so the third parameter to Read has type func(*Tx) (any, error). The lightweight function seems like it should work here, since we could write this with explicit types:

authPath, err := cfgdb.Read[any](ctx, s.db, func (rx *Tx) (any, error) {
  return rx.AuthPath(r.URL.Path)
})

But for that to work, we need to derive the type of the result in cases A and B in entirely different ways.

We could always infer the return type of a lightweight func from the input types and the func body (which makes case B fail to compile), but that's limiting--returning a concrete type that implements error from a lightweight func would result in the func return type being inferred as that concrete type rather than error.

I think this is much simpler if a lightweight func can only be assigned to a variable whose type is fully known.

ianlancetaylor commented 2 years ago

I think that we should never set the result type of a lightweight function from the function body We should only set it from the type to which it is being assigned.

The question is what we should do when the lightweight function is being assigned to a function of generic type. In that case we set the result type to something like, in this case, (T, error). Once we have made that determination, we can, during type inference, unify the type from the function body with the type T. Of course we can only do that if the type from the function body is known a priori.

It's true that there are several moving parts here. They don't yet seem impossible.

  1. lightweight function of type func(X) Y
  2. function is assigned to parameter of type func(*cfgdb.Tx) (T, error)
  3. therefore X is *cfgdb.Tx and Y is (T, error)
  4. start type inference: must deduce T from the type of the lightweight function
  5. lightweight function is already type T, so we need to look at the function body (this is a new inference step)
  6. function body is type *dbx.AuthPath, so unify that with T

In case B the result type is (any, error), and there is no type inference.

ianlancetaylor commented 2 years ago

I think this is much simpler if a lightweight func can only be assigned to a variable whose type is fully known.

By the way, that is undeniable. But I think we should put some extra effort into making this play well with generics, because generics encourages greater use of small function literals.

jimmyfrasche commented 2 years ago

Does what this print depend on the order of the constraints?

func f[F func() | func() (int, error)](v F) {
    fmt.Printf("%T", v)
}

func main() {
    f(func { fmt.Println() })
}
sammy-hughes commented 2 years ago

TypeScript has separate semantics for the return-half of a function declaration and the return-half of a function type. For the declaration signature, inputs: outputs. For the type signature, inputs => outputs. As observed above, by @nelid. I would like a way to add constraints on elided, genericized in/out param types, like so:

A := func x, y: func(int8)int16 {func v {x*y*v}}
B := func x, y int8 {func v: int16 {x*y*v}}
C := func x, y {func v {int16(int8(x)*int8(y)*int8(v))}}
D := func p {func v {(func c, ok { ok && c.X*c.Y*v || 0 })(p.(Point))}}

I think the story on D is "Geeze. Chill, dude, you're already possibly getting lambdas the same year as generics, and now you want ternary expressions?!?". I'm just trying to tease out how one might express that sort of thing, or failing that, if it is agreed that it's ok for this new syntax to be what it is, change the world for some people, and be a total meh for others.

DeedleFake commented 2 years ago

@jimmyfrasche

I would imagine that that should result in a compile-time error of an inability to infer F. If you specify the type manually, it's pretty straightforwards:

f[func()](func { fmt.Println() })
f[func() (int, error)](func { fmt.Println() })

The question is whether or not detecting that is feasible without too much trouble, which I don't know.

sammy-hughes commented 2 years ago

@f-feary, all enumerated forms that I gave as examples were condemned, but they represented 11 lines compressed into 1. Of course it's hard to read when compressed like that. The challenge is to ensure they are actually still readable.

IforTheAth := (
    func a, b {func x {a*x*x+b*x+a+b}}
)(Alpha.PropX.AsInt(), Beta.PropY.AsInt())

That is the very same thing as my example of current, live Go.

IforTheBth := (
    func a, b {
        func x {
            a*x*x+b*x+a+b
}})(Alpha.PropX.AsInt(), Beta.PropY.AsInt())

The point is to explore how various forms read when deeply nested, in any syntax that's been discussed (but could actually work), and how partial type declarations might look. I definitely preferred the func(<inputs) return <expression> syntax, as I think it leaves clear lexical markers for specifying types as needed, but it does feel like most folks are going to the func <inputs> {<expression>} form.

Splizard commented 2 years ago

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

If Go is going to have this level of type-inference, then it needs to be consistent across the language and include struct type-inference (#12854).

However, I believe it would be better served to avoid the rabbit hole of complex type-inference and stick with well defined function types, which unlike structs, currently lack a convenient constructor.

Here's an example:

http.ListenAndServe(":8080", http.HandlerFunc w, r {
    fmt.Fprint(w, "Hello World")
})

Since the literal function is strongly typed, it can be assigned to variables using := and is easier to read, developers only need to be familiar with the function type in order to understand the arguments. It's also trivial to lookup the declaration of http.HandlerFunc to check the argument types, and this syntax enables the function to passed as an interface implementation which you cannot do with a naked func (at least not without wrapping it inside of an http.HandlerFunc anyway).

It also works well with generics, where there are already plenty of type-inference possibilities to explore.

type Adder[T any] func(a, b T) T

fn := Adder[int] a, b { a + b }
sammy-hughes commented 2 years ago

@Splizard, I think I'm likely the only one asking for partial type declaration, so take that as warning, but I don't like that your suggested syntax drops the func keyword. That said, given what has generally been established....

A := func a, b { a + b} Adder[int]
B := func a, b { a + b } as Adder[int]
C := Adder[int](func a, b { a + b })
D := Adder[int]{ a + b }

I think I just fell in love with example D. If a function is declared in context of a type-coercion, I'm unclear on whether the desired effect would be a runtime coercion on the inferred types, or if rather I prefer the coercion serve as constraint on possible types, statically.

Even if I'm changing what you said, I think I at least get your point.

Seriously, though, I really like form D above. I think @griesemer suggested named types as a way to specify parameter signatures, and I think it just clicked for me. If both facts are true, that form D above is received well and that function types which are assignable for every term across both signatures are themselves assignable types, the version of that suggestion in form D above is the most versatile I've yet seen.

ianlancetaylor commented 2 years ago

@jimmyfrasche Nice example. I think it has to be an error. I think it would be fine to say that we can't infer anything from an argument that is a lightweight function literal. That would leave your example with no way to infer F.

ianlancetaylor commented 2 years ago

@Splizard We are already inconsistent about type inference. Given x = 1 we infer the type of 1 from the type of x. But we don't do that for x = {1, 2, 3}.

I'm not saying that we can't do #12854, just that a decision on that is independent from a decision on this issue.

jimmyfrasche commented 2 years ago

@ianlancetaylor wouldn't it work if there were a separate syntax for the return-a-single-expression case? It also seems better for readability as you could disambiguate those cases at a glance. Is there a reason for preferring to reuse the same syntax for both cases?

DeedleFake commented 2 years ago

@jimmyfrasche

Is there a reason for preferring to reuse the same syntax for both cases?

One point that I can think of is that it makes refactoring easier. If you want to change from a single-expression function to a regular full body, all you have to do is add a few newlines and a return. If you have a separate syntax, such as func a, b -> a + b, you have to remove the -> and add {}, which is a lot more annoying. I think the ease of refactoring is worth the annoyance of a few minor edge cases, but that's just my personal preference. Little things like that can make the difference between a simple language like Go or Ruby and a language like Java where essentially have to use an IDE that can do a bunch of transformations of various kinds for you to be remotely productive in it.

ianlancetaylor commented 2 years ago

@jimmyfrasche Yes, I think that would make your example work, but I don't see a reason to index on that case. I think we should try to support generic, but I don't see why people would write generic functions with a type parameter that accepts various different function types. If that case is indeed rare then I don't think we need to introduce syntax to support it.

Splizard commented 2 years ago

@ianlancetaylor There is an asymmetry in Go with regards to function types versus composite types and basic types.

I can define a struct, map, slice, or basic type and then use them without referring to the builtin expression for that type.

type MyBasicType int
_ = MyBasicType(1)

type MyStruct struct { a,b int }
_ = MyStruct{1, 2}

type MyMap map[int]int
_ = MyMap{1: 2}

type MySlice []int
_ = MySlice{1, 2}

type MyFunc func(a, b int)
_ = MyFunc(func(a, b int) {})

(observe how MyFunc sticks out).

We are already inconsistent about type inference.

I disagree, Go is consistent about type-inference, {1, 2, 3} is not a valid Go expression or value, nor do I believe it should be. However, it could be inferred as a composite value, in the same fashion as func a,b {a + b}. In these cases, the components of the type are being inferred, and if Go supported this level of inference, it should apply to both composite types and function types in order to remain consistent.

just that a decision on that is independent from a decision on this issue.

The language is a whole, I don't think it's healthy for the language to be considering changes in isolation, what links this issue to #12854, is the level of type-inference that the syntax func a,b {a + b} introduces.

Having a constructor for defined function types, that avoids the func keyword, would provide a lighterweight anonymous function syntax, without closing the door on adding extra type-inference capabilities in the future. I like the style of func a,b {a + b}.

More technically, I would propose that a function type can be followed by zero or more argument names that name the arguments of that function within the scope of the function's block.

_ = func(int,int) int a, b { a + b }

type MyFunc func(int,int) int
_ = MyFunc a, b { a + b }

@sammy-hughes I imagine that you would prefer that the arguments are optionally named (or that they cannot be renamed)?

_ = func(a, b int) int { a + b }

type MyFunc func(a, b int) int
_ = MyFunc{ a + b }
jimmyfrasche commented 2 years ago

@ianlancetaylor Another one: func { <-c } probably means returning the result of <-c or it could be a callback that waits and c is chan struct{}.

Ambiguities around type inference would be annoying but I'm more worried about ambiguities around reading the code, though perhaps that's overblown and the context within the line will be sufficient. The fact that this works 99%+ of the time but has some weird edges seems like it would be if anything more troublesome since when it does show up it would be all the more confusing.

@DeedleFake I've used languages with different expression and block syntax and it is a little annoying to switch up the forms but it doesn't happen often and it's not a very big deal (and entirely automatable).

sammy-hughes commented 2 years ago

@Splizard, I love this idea. A version of it, what you've expressed or something close, would rapidly infect every greenfield project I write, if it goes live.

I think we're dealing with a related but different API. I think it's direct, well-constrained, and broadly useful; orthogonality with single-function-interfaces in the best way. @bradfitz's example earlier was a prime candidate for just such a feature.

If the version I'm describing of the version @Splizard described becomes adopted, it would actually put the lambda/single-expression function in a very similar boat to structs, as regards #12854. This functionality would cover a lot of ground, but I can't prove to myself that they're the same feature. Shorthand, type-inferred functions and named-type function instantiation are distinct features, even if neighbors...I want them both, please, sirs?

Dropping the func keyword for such a case feels like a "javascript-grade" move, but I now recognize it.

type A struct{x int; y int;}
type BFunc func(*cfgdb.Tx)(billingpolicy.usage, error)
type CFunc func(tx *cfgdb.Tx)(rx billingpolicy.usage, err error)
D := A struct{n0, n1}
E := A{n0, n1}
F := A{x: n0, y: n1}
G := BFunc(tx) {return rx, err}
H := BFunc(txPtr) {return rx, err}
I := BFunc(tx) (rx, err) {rx, err = ....; return}
J := CFunc(tx) (rx, err) {rx, err = ....; return}
K := BFunc tx {rx, err}
L := CFunc tx, rx, err {...?...?}
var M BFunc = func tx {rx, err}

I'm still wondering how much overlap there is between these two features, but if ever there was a "distinctively Go" way to implement lambdas, this would be it. At the same time, I'm quite certain this will feel alien to many. I assert thematic congruity, not fair reception.

Quick note on x = {1, 2, 3}, I opined quite pontifically in #48499, but I find nothing inherently problematic with that sort of expression. Guaranteeing compatibility decades down the road....."unclear", which Go-style is an alias for "invalid".

Meanwhile, @jimmyfrasche, I don't accept that the type inferred for the function, func { <-c }, could be ambiguous. Channel c has a definite type. It could be a definite parametric type, but that is still a definite type, as an instance of func blocking on a channel read, and eventually producing someType(0), false or someType(value), true

sammy-hughes commented 2 years ago

additional to the above but distinctly, @Splizard, both the API you describe and I describe, I suggest the feature(s) be presented with named function types always beginning with "Fn" or "fn", as with gopls rule for package-declared error names.

EDIT: Hah! Thanks, @DeedleFake, right. Names always ending with "Func"

DeedleFake commented 2 years ago

@sammy-hughes

Go already has a naming convention for named function types: They end with Func.

sammy-hughes commented 2 years ago

@Splizard, looks like someone beat you to the idea ... by a few years. Conveniently enough, it was shot down for reasons you addressed!

The core issue was that the API as proposed put the naming authority with the type declaration. This would have required a change in the type API, so the issue was closed. A further concern was, for the API as described, difficulty distinguishing between an interface literal and a function definition, which is again a non-issue.

Three questions, @ianlancetaylor, and if unclear, see examples below:

  1. Is my intuition justified That @Splizard has suggested a parallel and potentially competing feature?
  2. Given that this is similar to the proposal of #36855, and addresses the mortal issues, should the ticket be reopened?
  3. Do you see any technical reason why this version of a lightweight function syntax would be inferior to the type-inference version, either from maintenance or implementation difficulties?

Examples:

type AFunc func(int, int) int
type BFunc func(int) int
func C(a, b int) int { return a+b }
var D AFunc = func(a int, b int) int {return a+b}
var E AFunc = func a, b {a+b}
F := Afunc a, b {a+b}
G := AFunc(a, b) {return a+b}
H := AFunc(func a, b {a+b})
I := (func(int, int)int)(func a, b {a+b})
J := []AFunc{
    nil, //0 ... sorry. It was bothering me.
    AFunc(a, b) c {c = a+b; return}, //1
    AFunc(a, b) c {c = a+b; c}, //2
    AFunc a, b (c) {c = a+b; c}, //3
    func a, b (c) {c = a+b; c},//4
    func a, b to c {c = a+b; c},//5
    func a, b -> c {c = a+b; c}//6
}

Note that J above is not an argument against multiple-expression "lightweight anonymous function syntax." I met block-level expressions in Rust, where the last evaluated expression in a block is what the outer block sees. I love the feature, and I'll fight for it....in its own ticket.

ianlancetaylor commented 2 years ago

Is my intuition justified That @Splizard has suggested a parallel and potentially competing feature?

I'm not sure I really understand @Splizard 's suggestion. If I do, then it seems to me that it requires me to write out the function type including all the parameter and result types, in order to have a name that can be used for the lightweight literal. If that is correct, then, yes, I think that seems unrelated to this proposal. This proposal is about having a way to write lightweight function literals that infer the parameter and result types from the context in which the function literal appears. If I have to write out those types explicitly, in a different type definition, then I'm not getting the full benefit of this proposal.

Given that this is similar to the proposal of #36855, and addresses the mortal issues, should the ticket be reopened?

No, but a new proposal could be opened. But I'm not too persuaded by the idea. People don't write function types very often, so there wouldn't be much use today. And for future uses I don't see a big advantage to writing the types in one place and using them in another. A small advantage, yes, as the function type can be reused, but not a big advantage. The main argument in favor may be consistency, but Go has many inconsistencies of this sort; consistency arguments are valid but not in themselves persuasive.

Do you see any technical reason why this version of a lightweight function syntax would be inferior to the type-inference version, either from maintenance or implementation difficulties?

Yes: it requires me to explicitly write down the types somewhere.

jba commented 2 years ago

Yes: it requires me to explicitly write down the types somewhere.

In the common case of a function argument, someone already has to do that in the formal parameter. The problem with the idea is that it would effectively require every

func Foo(f func(int, int) bool)

to become

type FooFunc func(int, int) bool
func Foo(f FooFunc)

so that callers could take advantage of the shorter syntax. That is too much to expect of every function author.

DeedleFake commented 2 years ago

That is too much to expect of every function author.

And if the author doesn't, then requiring me to do it in turn eliminates about 90% of the benefit of this proposal.

beoran commented 2 years ago

So, taking a step back from the details of the syntax for this proposal, it seems one important benefit that this proposal should have is that we would not have to write out the types of the parameters and of the return values.

An implicit return might be considered as a separate feature.

With that said I think we can limit the syntax choices to the three following ones:

now: func(a int, b string) (string, error) { return foo(a,b) }
a: func a,b  { return foo(a, b) }
b: -> a,b { return foo(a,b)  }
c: => a,b { return foo(a,b)  }
fzipp commented 2 years ago

With that said I think we can limit the syntax choices to the three following ones:

now: func(a int, b string) (string, error) { return foo(a,b) }
a: func a,b  { return foo(a, b) }
b: -> a,b { return foo(a,b)  }
c: => a,b { return foo(a,b) }

I find -> too similar to the channel send/receive operator <-, and => too similar to the relational operator <=. Go has a keyword for functions (func), so I'd expect it to indicate any form of a function.

beoran commented 2 years ago

@fzipp That is a good argument. So the three important requirements are having the func keyword, not having types for the parameters and return values, and backwards compatibility.

Then really only the syntax that remains is the func a,b {...} syntax as proposed by @ianlancetaylor. I hope we can focus the discussion on that syntax then.

sammy-hughes commented 2 years ago

it requires me to write out the function type including all the parameter and result types

@ianlancetaylor, not really. The suggestion was that you could, optionally, write out a function type, not that it is required. Applied to standard syntax for anonymous functions, it could be considered a competing proposal, but when applied to the consensus syntax for lightweight anonymous functions, it covers edge-cases when inference is problematic. I frequently use higher-order functions, but it appears @Splizard and I overrated how common that is.

Acknowledging that, I still want some space between "Specify everything" and "Specify nothing", and I assume partially-specified types invalid, as corollary to the named in/out params rule. My fussiness stems from assumptions about possible implementations, but I want the following to work:

type exampleFunc func(t int64, v int64) int64
type exampleBox struct{f exampleFunc}
A := exampleBox{ 
    func a, b { a+b }
}
B := exampleFunc(func a, b { a+b })

If the underlying type is either func(int,int) int or func[T interface{~int64}, U interface{~int64}, V interface{~int64}](a T, b U) V, then both imperatives above are invalid, as neither are compatible with func(int64, int64) int64 would not be valid.

An implicit return might be considered as a separate feature.

@beoran, while I prefer the return token be included, @ianlancetaylor suggested omitting the return token. The discussion I pushed above was assuming consensus on the grammar for lightweight syntax as func ident-list { body }.

To serve the specific questions I put to @ianlancetaylor, and any discussion regarding motivating usecases, I gave examples contrasting examples for the standard syntax for anonymous functions, consensus proposal syntax for single-expression functions, the special case proposed by @Splizard, and extension of standard syntax for anonymous functions with the special case applied. I had intended to also cover examples for named-parameter cases, but quickly realized that combining named outparams and the consensus proposal syntax is not reasonable, even supposing a variety of possible separation tokens.

The problem with the idea is that it would effectively require every ... to become ...

@jba, I think you're missing something. A named-type as alias for a particular function signature is compatible with the unnamed type, the function signature that is the underlying type. If you're talking about a different concern, and if it's still relevant with the special-case suggestion having been tabled, feel free to clarify.

beoran commented 2 years ago

@sammy-hughes I am only consideing the "outer" syntax at this time. Whether or not we can omit the return might be a different issue. Other proposals have been rejected due to implicit returns being not go like, so I assumed it is better to keep it for now

sammy-hughes commented 2 years ago

@beoran, I mean, I prefer the explicit return token. It helps anchor the pattern for tokenizing, and it also does a better job of saying "Hey! This is a function defintion!" to anyone reading my code, including future me. I had assumed that the return was being elided, but frankly, this is a "change the world" feature for me, return token or no. I want this capability ASAP.

Historical, not an argument for either, but I observed the stipulation that the return token is implied, and that the body may only ever be an expression or list of expressions that are valid in a return clause. If that changed at some point, my confusion would be explained.

dolmen commented 2 years ago

@Splizard @sammy-hughes See also #30931 (which is a proposal I would vote for if it was still open).

dolmen commented 2 years ago

@ianlancetaylor

What aboutl lightweight function given to an any target?

Long version:

If I have the following function:

func compute(f func()) {
    fmt.Printf("%T\n", f)
}

func main() {
    compute(func() {
    })
}

If I change the signature to func compute(f any) the code still works and clients don't have to be modified.

In contrast, with the proposed syntax that relies on type inference, compute(func {}) might not compile anymore.

At least the behavior of a lightweight anonymous function would have to be specified when given as any.

sammy-hughes commented 2 years ago

@dolmen, I'd object that for the example, you are correct only if the return token is elided, but it doesn't represent a breaking change or a meaningful problem.

If you're pointing out the larger class of behavior surrounding fmt.Printf("%T\n", func <params> {<expressions>}), then I agree that it might be a difficulty, as with generics presently. I'm not sure. I'd suggest that if the function is supplied in that manner, the types inferred for the signature would be the first valid signature found for the types inferred during the expression. E.G. If there's a single parameter and single return, with the expression being a numeric operation, the most likely signature would be func(int) int or func(float) float. If an argument is supplied, being itself typed, that impinges on inferences, and the inferred signature can be expected with more certainty.

For the example you gave, though, I don't see any issue. For a closure that takes no argument and returns no value, provided there isn't a special case to disallow func <no params> {<no-return expression-list>}, there isn't any reason why it shouldn't work. If the return token is elided as assumed for the lightweight syntax, then the only possible expressions to fit the signature func() must not produce any value. If the return token is not elided, and is still explicit, there's no change to the kind of statement that might fit your example.

That is to say, "I don't see any particular difficulty posed by your example, but if I guess at the larger class of problem you reference, I think the problem is more interesting but equally benign."

jba commented 2 years ago

func compute(f func()) { ... } ... If I change the signature to func compute(f any) the code still works and clients don't have to be modified.

Since compute is unexported, the "clients" are all in the same package, so changing the calls is easy.

But let's say it was exported as Compute. Then this is a breaking change to your package's API, even though call sites don't have to change. A client package might have code like

var f func(func()) = pkg.Compute

which would no longer compile.

ianlancetaylor commented 2 years ago

What aboutl lightweight function given to an any target?

It's not permitted.

Your example, which already works today, is not using a lightweight function literal.

Code like the following, which I think is what you are suggesting, will not compile.

func compute(f any) {}
func F() {
    compute(func {})
}
sammy-hughes commented 2 years ago

@ianlancetaylor, is there intention to support usage as in the snippet below? That is,

  1. Will both A.f and B return a value having unnamed type 'int64'
  2. Will both A.f and B have the named type <package>.exampleFunc?
    type exampleFunc func(t int64, v int64) int64
    type exampleBox struct{f exampleFunc}
    A := exampleBox{f: func a, b { a+b }}
    B := exampleFunc(func a, b { a+b })
DeedleFake commented 2 years ago

@sammy-hughes

A.f was declared to be exampleFunc, so it will be no matter what. The question is whether or not the expression exampleBox{f: func a, b { a + b }} is allowed, which it seems likely that it should be.

griesemer commented 2 years ago

The A case can be viewed as an assignment to A.f. The type of A.f is exampleFunc, the function literal matches that type and one can properly infer the function signature. If we would allow such a use (which seems possible), the type of A.f remains exampleFunc (of course). Calling that function returns a value of int64 (not an untyped value).

Similarly, for B, the same applies assuming we allow such a conversion (which is plausible).

rodcorsi commented 2 years ago

I think omitting the return statement inside of block IMO only makes sense if we have block expressions (int a = { 1 + 5 }), because of that, the syntax func a, b { a+b } doesn't appear correct to me. Omit return statement with a syntax like func(a,b) a+b or func(a,b): a+b could be better