Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

proposal: spec: lightweight anonymous function syntax #21498

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

proposal: spec: lightweight anonymous function syntax #21498

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

Comments

@neild
Copy link
Contributor

neild commented Aug 17, 2017

Many languages provide a lightweight syntax for specifying anonymous functions, in which the function type is derived from the surrounding context.

Consider a slightly contrived example from the Go tour (https://tour.golang.org/moretypes/24):

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

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

Many languages permit eliding the parameter and return types of the anonymous function in this case, since they may be derived from the context. For example:

// Scala
compute((x: Double, y: Double) => x + y)
compute((x, y) => x + y) // Parameter types elided.
compute(_ + _) // Or even shorter.
// Rust
compute(|x: f64, y: f64| -> f64 { x + y })
compute(|x, y| { x + y }) // Parameter and return types elided.

I propose considering adding such a form to Go 2. I am not proposing any specific syntax. In terms of the language specification, this may be thought of as a form of untyped function literal that is assignable to any compatible variable of function type. Literals of this form would have no default type and could not be used on the right hand side of a := in the same way that x := nil is an error.

Uses 1: Cap'n Proto

Remote calls using Cap'n Proto take an function parameter which is passed a request message to populate. From https://github.com/capnproto/go-capnproto2/wiki/Getting-Started:

s.Write(ctx, func(p hashes.Hash_write_Params) error {
  err := p.SetData([]byte("Hello, "))
  return err
})

Using the Rust syntax (just as an example):

s.Write(ctx, |p| {
  err := p.SetData([]byte("Hello, "))
  return err
})

Uses 2: errgroup

The errgroup package (http://godoc.org/golang.org/x/sync/errgroup) manages a group of goroutines:

g.Go(func() error {
  // perform work
  return nil
})

Using the Scala syntax:

g.Go(() => {
  // perform work
  return nil
})

(Since the function signature is quite small in this case, this might arguably be a case where the lightweight syntax is less clear.)

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

griesemer commented Aug 17, 2017

I'm sympathetic to the general idea, but I find the specific examples given not very convincing: The relatively small savings in terms of syntax doesn't seem worth the trouble. But perhaps there are better examples or more convincing notation.

(Perhaps with the exception of the binary operator example, but I'm not sure how common that case is in typical Go code.)

@davecheney
Copy link
Contributor

davecheney commented Aug 17, 2017 via email

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

I think this is more convincing if we restrict its use to cases where the function body is a simple expression. If we are required to write a block and an explicit return, the benefits are somewhat lost.

Your examples then become

s.Write(ctx, p => p.SetData([]byte("Hello, "))

g.Go(=> nil)

The syntax is something like

[ Identifier ] | "(" IdentifierList ")" "=>" ExpressionList

This may only be used in an assignment to a value of function type (including assignment to a parameter in the process of a function call). The number of identifiers must match the number of parameters of the function type, and the function type determines the identifier types. The function type must have zero results, or the number of result parameters must match the number of expressions in the list. The type of each expression must be assignable to the type of the corresponding result parameter. This is equivalent to a function literal in the obvious way.

There is probably a parsing ambiguity here. It would also be interesting to consider the syntax

λ [Identifier] | "(" IdentifierList ")" "." ExpressionList

as in

s.Write(ctx, λp.p.SetData([]byte("Hello, "))

@neild
Copy link
Contributor Author

neild commented Aug 17, 2017

A few more cases where closures are commonly used.

(I'm mainly trying to collect use cases at the moment to provide evidence for/against the utility of this feature.)

@faiface
Copy link

faiface commented Aug 18, 2017

I actually like that Go doesn't discriminate longer anonymous functions, as Java does.

In Java, a short anonymous function, a lambda, is nice and short, while a longer one is verbose and ugly compared to the short one. I've even seen a talk/post somewhere (I can't find it now) that encouraged only using one-line lambdas in Java, because those have all those non-verbosity advantages.

In Go, we don't have this problem, both short and longer anonymous functions are relatively (but not too much) verbose, so there is no mental obstacle to using longer ones too, which is sometimes very useful.

@jimmyfrasche
Copy link
Member

The shorthand is natural in functional languages because everything is an expression and the result of a function is the last expression in the function's definition.

Having a shorthand is nice so other languages where the above doesn't hold have adopted it.

But in my experience it's never as nice when it hits the reality of a language with statements.

It's either nearly as verbose because you need blocks and returns or it can only contain expressions so it's basically useless for all but the simplest of things.

Anonymous functions in Go are about as close as they can get to optimal. I don't see the value in shaving it down any further.

@bcmills
Copy link
Contributor

bcmills commented Aug 24, 2017

It's not the func syntax that is the problem, it's the redundant type declarations.

Simply allowing the function literals to elide unambiguous types would go a long way. To use the Cap'n'Proto example:

s.Write(ctx, func(p) error { return p.SetData([]byte("Hello, ")) })

@neild
Copy link
Contributor Author

neild commented Aug 24, 2017

Yes, it's the type declarations that really add noise. Unfortunately, "func (p) error" already has a meaning. Perhaps permitting _ to substitute in for an inferenced type would work?

s.Write(ctx, func(p _) _ { return p.SetData([]byte("Hello, ")) })

I rather like that; no syntactic change at all required.

@martisch
Copy link
Contributor

I do not like the stutter of _. Maybe func could be replaced by a keyword that infers the type parameters:
s.Write(ctx, λ(p) { return p.SetData([]byte("Hello, ")) })

@davecheney
Copy link
Contributor

davecheney commented Aug 25, 2017

Is this actually a proposal or are you just spitballing what Go would look like if you dressed it like Scheme for Halloween? I think this proposal is both unnecessary and in poor keeping with the language's focus on readability.

Please stop trying to change the syntax of the language just because it looks different to other languages.

@cespare
Copy link
Contributor

cespare commented Aug 25, 2017

I think that having a concise anonymous function syntax is more compelling in other languages that rely more on callback-based APIs. In Go, I'm not sure the new syntax would really pay for itself. It's not that there aren't plenty of examples where folks use anonymous functions, but at least in the code I read and write the frequency is fairly low.

@bcmills
Copy link
Contributor

bcmills commented Aug 25, 2017

I think that having a concise anonymous function syntax is more compelling in other languages that rely more on callback-based APIs.

To some extent, that is a self-reinforcing condition: if it were easier to write concise functions in Go, we may well see more functional-style APIs. (Whether that is a good thing or not, I do not know.)

I do want to emphasize that there is a difference between "functional" and "callback" APIs: when I hear "callback" I think "asynchronous callback", which leads to a sort of spaghetti code that we've been fortunate to avoid in Go. Synchronous APIs (such as filepath.Walk or strings.TrimFunc) are probably the use-case we should have in mind, since those mesh better with the synchronous style of Go programs in general.

@dimitropoulos
Copy link

I would just like to chime in here and offer a use case where I have come to appreciate the arrow style lambda syntax to greatly reduces friction: currying.

consider:

// current syntax
func add(a int) func(int) int {
	return func(b int) int {
		return a + b
	}
}

// arrow version (draft syntax, of course)
add := (a int) => (b int) => a + b

func main() {
	add2 := add(2)
	add3 := add(3)
	fmt.Println(add2(5), add3(6))
}

Now imagine we are trying to curry a value into a mongo.FieldConvertFunc or something which requires a functional approach, and you'll see that having a more lightweight syntax can improve things quite a bit when switching a function from not being curried to being curried (happy to provide a more real-world example if anyone wants).

Not convinced? Didn't think so. I love go's simplicity too and think it's worth protecting.

Another situation that happens to me a lot is where you have and you want to now curry the next argument with currying.

now you would have to change
func (a, b) x
to
func (a) func(b) x { return func (b) { return ...... x } }

If there was an arrow syntax you would simply change
(a, b) => x
to
(a) => (b) => x

@myitcv
Copy link
Member

myitcv commented Nov 6, 2017

@neild whilst I haven't contributed to this thread yet, I do have another use case that would benefit from something similar to what you proposed.

But this comment is actually about another way of dealing with the verbosity in calling code: have a tool like gocode (or similar) template a function value for you.

Taking your example:

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

If we assume we had typed:

var _ = compute(
                ^

with the cursor at the position shown by the ^; then invoking such a tool could trivially template a function value for you giving:

var _ = compute(func(a, b float64) float64 { })
                                            ^

That would certainly cover the use case I had in mind; does it cover yours?

@neild
Copy link
Contributor Author

neild commented Nov 6, 2017

Code is read much more often than it is written. I don't believe saving a little typing is worth a change to the language syntax here. The advantage, if there is one, would largely be in making code more readable. Editor support won't help with that.

A question, of course, is whether removing the full type information from an anonymous function helps or harms readability.

@mrkaspa
Copy link

mrkaspa commented Nov 20, 2017

I don't think this kind of syntax reduces readability, almost all modern programming languages have a syntax for this and thats because it encourages the use of functional style to reduce the boilerplate and make the code clearer and easier to maintain. It's a great pain to use anonymous functions in golang when they are passed as parameters to functions because you have to repeat yourself typing again the types that you know you must pass.

@hooluupog
Copy link

I support the proposal. It saves typing and helps readability.My use case,

// Type definitions and functions implementation.
type intSlice []int
func (is intSlice) Filter(f func(int) bool) intSlice { ... }
func (is intSlice) Map(f func(int) int) intSlice { ... }
func (is intSlice) Reduce(f func(int, int) int) int { ...  }
list := []int{...} 
is := intSlice(list)

without lightweight anonymous function syntax:

res := is.Map(func(i int)int{return i+1}).Filter(func(i int) bool { return i % 2 == 0 }).
             Reduce(func(a, b int) int { return a + b })

with lightweight anonymous function syntax:

res := is.Map((i) => i+1).Filter((i)=>i % 2 == 0).Reduce((a,b)=>a+b)

@firelizzard18
Copy link
Contributor

The lack of concise anonymous function expressions makes Go less readable and violates the DRY principle. I would like to write and use functional/callback APIs, but using such APIs is obnoxiously verbose, as every API call must either use an already defined function or an anonymous function expression that repeats type information that should be quite clear from the context (if the API is designed correctly).

My desire for this proposal is not even remotely that I think Go should look or be like other languages. My desire is entirely driven by my dislike for repeating myself and including unnecessary syntactic noise.

@griesemer
Copy link
Contributor

griesemer commented Jan 3, 2018

In Go, the syntax for function declarations deviates a bit from the regular pattern that we have for other declarations. For constants, types, variables we always have:

keyword name type value

For example:

const   c    int  = 0
type    t    foo
var     v    bool = true

In general, the type can be a literal type, or it can be a name. For functions this breaks down, the type always must be a literal signature. One could image something like:

type BinaryOp func(x, y Value) Value

func f BinaryOp { ... }

where the function type is given as a name. Expanding a bit, a BinaryOp closure could then perhaps be written as

BinaryOp{ return x.Add(y) }

which might go a long way to shorter closure notation. For instance:

vector.Apply(BinaryOp{ return x.Add(y) })

The main disadvantage is that parameter names are not declared with the function. Using the function type brings them "in scope", similar to how using a struct value x of type S brings a field f into scope in a selector expression x.f or a struct literal S{f: "foo"}.

Also, this requires an explicitly declared function type, which may only make sense if that type is very common.

Just another perspective for this discussion.

@dimitropoulos
Copy link

Readability comes first, that seems to be something we can all agree on.

But that said, one thing I want to also chime in on (since it doesn't look like anyone else said it explicitly) is that the question of readability is always going to hinge on what you're used to. Having a discussion as we are about whether it hurts or harms readability isn't going to get anywhere in my opinion.

@griesemer perhaps some perspective from your time working on V8 would be useful here. I (at least) can say I was very much happy with javascript's prior syntax for functions (function(x) { return x; }) which was (in a way) even heavier to read than Go's is right now. I was in @douglascrockford's "this new syntax is a waste of time" camp.

But, all the same, the arrow syntax happened and I accepted it because I had to. Today, though, having used it a lot more and gotten more comfortable with it, I can say that it helps readability tremendously. I used the case of currying (and @hooluupog brought up a similar case of "dot-chaining") where a lightweight syntax produces code that is lightweight without being overly clever.

Now when I see code that does things like x => y => z => ... and it is much easier to understand at a glance (again... because I'm familiar with it. not all that long ago I felt quite the opposite).

What I'm saying is: this discussion boils down to:

  1. When you aren't used to it, it seems really strange and borderline useless if not harmful to readability. Some people just have or don't have a feeling one way or another on this.
  2. The more functional programming you're doing, the more the need for such a syntax pronounces itself. I would guess that this has something to do with functional concepts (like partial application and currying) that introduce a lot of functions for tiny jobs which translates to noise for the reader.

The best thing we can do is provide more use-cases.

@firelizzard18
Copy link
Contributor

In response to @dimitropoulos's comment, here's a rough summary of my view:

I want to use design patterns (such as functional programming) that would greatly benefit from this proposal, as their use with the current syntax is excessively verbose.

@griesemer
Copy link
Contributor

@dimitropoulos I've been working on V8 alright, but that was building the virtual machine, which was written in C++. My experience with actual Javascript is limited. That said, Javascript is a dynamically typed language, and without types much of the typing goes away. As several people have brought up before, a major issue here is the need to repeat types, a problem that doesn't exist in Javascript.

Also, for the record: In the early days of designing Go we actually looked at arrow syntax for function signatures. I don't remember the details but I'm pretty sure notation such as

func f (x int) -> float32

was on the white board. Eventually we dropped the arrow because it didn't work that well with multiple (non-tuple) return values; and once the func and the parameters where present, the arrow was superfluous; perhaps "pretty" (as in mathematically looking), but still superfluous. It also seemed like syntax that belonged to a "different" kind of language.

But having closures in a performant, general purpose language opened the doors to new, more functional programming styles. Now, 10 years down the road, one might look at it from a different angle.

Still, I think we have to be very careful here to not create special syntax for closures. What we have now is simple and regular and has worked well so far. Whatever the approach, if there's any change, I believe it will need to be regular and apply to any function.

@bcmills
Copy link
Contributor

bcmills commented Jan 3, 2018

In Go, the syntax for function declarations deviates a bit from the regular pattern that we have for other declarations. For constants, types, variables we always have:
keyword name type value
[…]
For functions this breaks down, the type always must be a literal signature.

Note that for parameter lists and const and var declarations we have a similar pattern, IdentifierList Type, which we should probably also preserve. That seems like it would rule out the lambda-calculus-style : token to separate variable names from types.

Whatever the approach, if there's any change, I believe it will need to be regular and apply to any function.

The keyword name type value pattern is for declarations, but the use-cases that @neild mentions are all for literals.

If we address the problem of literals, then I believe the problem of declarations becomes trivial. For declarations of constants, variables, and now types, we allow (or require) an = token before the value. It seems like it would be easy enough to extend that to functions:

FunctionDecl = "func" ( FunctionSpec | "(" { FunctionSpec ";" } ")" ).
FunctionSpec = FunctionName Function |
               IdentifierList (Signature | [ Signature ] "=" Expression) .

FunctionLit = "func" Function | ShortFunctionLit .
ShortParameterList = ShortParameterDecl { "," ShortParameterDecl } .
ShortParameterDecl = IdentifierList [ "..." ] [ Type ] .

The expression after the = token must be a function literal, or perhaps a function returned by a call whose arguments are all available at compile time. In the = form, a Signature could still be supplied to move the argument type declarations from the literal to the FunctionSpec.

Note that the difference between a ShortParameterDecl and the existing ParameterDecl is that singleton IdentifierLists are interpreted as parameter names instead of types.


Examples

Consider this function declaration accepted today:

func compute(f func(x, y float64) float64) float64 { return f(3, 4) }

We could either retain that (e.g. for Go 1 compatibility) in addition to the examples below, or eliminate the Function production and use only the ShortFunctionLit version.

For various ShortFunctionLit options, the grammar I propose above gives:

Rust-like:

ShortFunctionLit = "|" ShortParameterList "|" Block .

Admits any of:

func compute = |f func(x, y float64) float64| { f(3, 4) }
func compute(func (x, y float64) float64) float64 = |f| { f(3, 4) }
func (
	compute = |f func(x, y float64) float64| { f(3, 4) }
)
func (
	compute(func (x, y float64) float64) float64 = |f| { f(3, 4) }
)

Scala-like:

ShortFunctionLit = "(" ShortParameterList ")" "=>" Expression .

Admits any of:

func compute = (f func(x, y float64) float64) => f(3, 4)
func compute(func (x, y float64) float64) float64 = (f) => f(3, 4)
func (
	compute = (f func(x, y float64) float64) => f(3, 4)
)
func (
	compute(func (x, y float64) float64) float64 = (f) => f(3, 4)
)

Lambda-calculus-like:

ShortFunctionLit = "λ" ShortParameterList "." Expression .

Admits any of:

func compute = λf func(x, y float64) float64.f(3, 4)
func compute(func (x, y float64) float64) float64) = λf.f(3, 4)
func (
	compute = λf func(x, y float64) float64.f(3, 4)
)
func (
	compute(func (x, y float64) float64) float64) = λf.f(3, 4)
)

Haskell-like:

ShortFunctionLit = "\" ShortParameterList "->" Expression .
func compute = \f func(x, y float64) float64 -> f(3, 4)
func compute(func (x, y float64) float64) float64) = \f -> f(3, 4)
func (
	compute = \f func(x, y float64) float64 -> f(3, 4)
)
func (
	compute(func (x, y float64) float64) float64) = \f -> f(3, 4)
)

C++-like:
(Probably not feasible due to ambiguity with array literals, but maybe worth considering.)

ShortFunctionLit = "[" ShortParameterList "]" Block .

Admits any of:

func compute = [f func(x, y float64) float64] { f(3, 4) }
func compute(func (x, y float64) float64) float64) = [f] { f(3, 4) }
func (
	compute = [f func(x, y float64) float64] { f(3, 4) }
)
func (
	compute(func (x, y float64) float64) float64) = [f] { f(3, 4) }
)

Personally, I find all but the Scala-like variants to be fairly legible. (To my eye, the Scala-like variant is too heavy on parentheses: it makes the lines much more difficult to scan.)

@ianlancetaylor
Copy link
Contributor

Personally I'm mainly interested in this if it lets me omit the parameter and result types when they can be inferred. I'm even fine with the current function literal syntax if I can do that. (This was discussed above.)

Admittedly this goes against @griesemer 's comment.

@DeedleFake
Copy link

DeedleFake commented Nov 7, 2024

I'm pretty sure that it's a special-case in the language.

On a side note, while looking into it, I found that apparently the parentheses are optional, though, so -> v1, v2 { v1 + v2 } is legal, which looks quite a bit like some of the suggestions from elsewhere in this massive chain of comments. Parentheses are optional in Ruby for function calls, but I didn't realize that that was also true for function definitions.

@jimmyfrasche
Copy link
Member

I can find that lambda is not special syntax and that -> was released in Ruby 1.9 but can't find anything official that says one way or the other but it does appear to be syntax as I cannot find it listed as a method anywhere like I can for things like == or +.

@jimmyfrasche
Copy link
Member

Actually, all of the below bracket asymmetrically with an initial word and final symbol instead of paired symbols like ( and )

Python (1): lambda args: expr
Elixir (47): fn args -> exprOrBlock end
F# (50): fun args -> exprOrBlock
Ocaml: fun args -> exprOrBlock
SML: fn arg => exprOrBlock

Python is still the major data point in that class but the MLs are popular as teaching languages so they may be more familiar than they are used.

If the criterion is extended to any two tokens that don't form a pair, the list expands by:

Swift (20): { args in exprOrBlock }
Kotlin (21): { args -> exprOrBlock }
Haskell (32): \ args -> exprOrBlock
Smalltalk: [:arg1 :arg2 | exprOrBlock ]

The (args) => exprOrBody still looks like the syntax that is being converged to, though.

@jcsahnwaldt
Copy link

Thanks a lot for the survey, @jimmyfrasche! Minor correction: The Wikipedia article is incomplete, C# lambdas also have exprOrBlock syntax, not just expr.

@jimmyfrasche
Copy link
Member

I attempted to double check that but did not find the good link you provided, thanks! Updated.

@hherman1
Copy link

hherman1 commented Nov 8, 2024

Is func args {} plausible?

So it’s just the normal syntax minus parentheses. the rule would be that if you don’t have parentheses you don’t need types for your arguments or return value, but you could only use it in a place where the types can be inferred

@griesemer
Copy link
Contributor

Thanks for your language overview, @jimmyfrasche, that is helpful.

I just note that the => (or ->) notation was not well received in the context of my experiments two years ago. Also, at least in the code base at that time, most function literals were significantly larger than just returning a single expression.

@jimmyfrasche
Copy link
Member

One thing that I do not have for the overview is the date that the syntax was added to each language. I think if you grouped them by that that the overwhelming trend would be that newer entries were more likely to be =>. I have a real sense that this is eventually just going to end up being THE syntax for lambdas the way the syntax for string concatenation is just overwhelmingly and unquestionably +. Even if my hunch is correct, that's not a strong argument for choosing the syntax but neither is it toothless.

I also did a brief unscientific survey earlier this year for what properties the syntax should have #21498 (comment) The => syntax has all the relevant popular (as of today) properties: omits func, omits types, allows expression or block.

I recently skimmed the thread and didn't see a lot of backlash to =>. There was not universal praise or preference for it. There were a lot of alternate designs. There were few people saying not to do that specifically that weren't also saying we shouldn't do anything in general. That's also heavily biased toward people who have read this thread and replied to it, which is not the majority of the community.

I doubt there will be any syntax that makes everyone happy, and I get the sense that most people would be satisfied with just having something. Maybe the viable designs should be put to a wider audience, perhaps as a survey where each alternate design collects additional info like how many years have you used other languages with this syntax, etc. It would be good to see the correlation between favorability and familiarity with these. If a lot of people like a design on paper but everyone who's actually used it strongly dislikes it, that would be as good to know!

@DeedleFake
Copy link

I much prefer -> to =>. I think it reads 10 times better despite looking like such a small difference.

@jimmyfrasche
Copy link
Member

I have no preference aesthetically but would lean toward => due to it being more popular in other languages and clashing less with channel operations: -> <- vs => <-.

@jimmyfrasche
Copy link
Member

Fact checking myself somewhat (I didn't say anything wrong per se but there is some nuance):

  • => is used in 8 langs
  • -> is used in 9 langs
  • => is in 3 top 20 langs
  • -> in in 1 top 20 langs
  • => is in 5 top 50 langs
  • -> is in 7 top 50 langs
  • => is in 5 (args) => exprOrBlock
  • -> is in 3 (args) -> exprOrBlock

@griesemer
Copy link
Contributor

Writing something like (x, y) => x + y does feel pretty natural. That said, single-expression function literals just don't seem that common in existing Go code. Maybe that's about the change but we don't know yet. Writing (x, y) => { ... } does feel odd to me for Go because we don't use => to indicate a function, we use a keyword.

(As an aside, in the very early days, when we were discussing the syntax of function signatures, we were briefly considering an arrow to separate between incoming and outgoing parameters, but Ken, Rob, and I opted against it at that time. I believe the keyword was still there, irrespectively.)

One problem with using => over -> is that something like this (x, y) => x <= y or (x, y) => x >= y (which I suspect might not be uncommon for filter functions) looks confusing. The single arrow reads better: (x, y) -> x <= y or (x, y) -> x >= y, respectively.

@griesemer
Copy link
Contributor

@hherman1 Yes, your suggestion is plausible, was prototyped, and applied to the standard library: CL 406076. See my comment from 2022.

The problem with any notation that doesn't use any form of bracketing around incoming parameters is readability: lightweight function literals will be passed as arguments to other functions. If their parameters are not bracketed, except for the func keyword it looks like those parameters are additional arguments to the called function. I believe we need a form of bracketing to avoid that.

This has been discussed repeatedly in this thread.

@tmaxmax
Copy link

tmaxmax commented Nov 14, 2024

I've implemented a parser for the func { params -> stmts } and similar forms ("->"/"|" separator, always keeping "return" or sometimes eliding it) discussed above, and ran an experiment on the Go standard library code and my work codebase.

Here's a PR with the implementation: tmaxmax#1. In the PR I've detailed some additional relevant discoveries.

Here are PRs with results and statistics:

The standard library has a size of 2231483 lines of Go code (including comments and blanks) as counted with:

$ find ./src -name '*.go' ! -path '*testdata*' ! -path './src/vendor*' | xargs -n 6000 wc -l

Here's a sample of the statistics I've collected for the standard library code:

found 7132 function literals, 4480 (62.8%) rewritten into lightweight form
1804 lightweight function literals (40.3%) are in test files
1852 lightweight function literals contain a single statement
        (from which 333, 18.0% are long)
684 lightweight function literals contain a single return statement
        (15.3% of all rewritten literals, 36.9% of single statement literals)
return value count histogram for single-return lightweight function literals:
        [0 610 72 2 0 0 0 0 0 0]

By "long" function literal I mean a lightweight function literal containing a single statement long enough to make the function not fit nicely on a single line.

Here are the ways in which these observations may diverge from those made in the initial experiment:

  • technically speaking the majority of lightweight functions are not in test files (percentage under 50%); the density of lightweight functions in tests is about 2.5 times greater than in normal code, though (3.8/kLoC vs 1.5/kLoC; read as "n occurences of lightweight functions over every one thousand lines of code")
  • while out of all lightweight functions only 14.2% benefit from the elision of the return keyword, it must be noted that the keyword was elided for 30.3% (percentages obtained using probabilities) of the functions which span a single line

For comparison, on my work codebase (a monolithic backend for a SaaS product), which amounts to 265785 lines of Go code using the same measurement, the following statistics are reported:

found 1805 function literals, 1642 (91.0%) rewritten into lightweight form
228 lightweight function literals (13.9%) are in test files
1094 lightweight function literals contain a single statement
	(from which 90, 8.2% are long)
1058 lightweight function literals contain a single return statement
        (64.4% of all rewritten literals, 96.7% of single statement literals)
return value count histogram for single-return lightweight function literals:
	[0 1049 9 0 0 0 0 0 0 0]

Here are the significant ways in which the results from my work codebase diverge from those from the standard library:

  • ratio-wise 3 times more lightweight functions would be used (6.2/kLoC vs 2/kLoC) than in the standard library
  • 4.2 times more function literals have a single return (64.4% vs 15.3%) and the vast majority of them can be written on a single line (only 7.9% of single-return lightweight functions would still be broken into multiple lines)
  • 2.9 times less function literals are used in tests compared to the standard library (13.9% vs 40.3%)
    • density-wise across this codebase lightweight functions would be used 1.6 times more in normal code than in test code (6.7/kLoC vs 4.3/kLoC)
    • for reference, Go's codebase is 21.5% test code and this codebase is 20.2% test code
  • 91% of all literals could be lightweight, compared to 62.8% in the standard library
    • the case for lightweight functions is stronger here, as normal literals would be in the extreme minority, whereas in the standard library the situation is more nuanced
    • consider especially the fact that in the standard library usage of lightweight functions across non-test code is much sparser than in this codebase (1.5/kLoC vs 6.7/kLoC); it's much easier to justify lightweight functions when there is very frequent usage in normal code rather than relatively frequent usage in test code

Furthermore, out of all these single-statement function literals:

  • 427 usages would benefit from return type deduction based on function body
    • all of these cases are exclusively usages slicesx.Map (392), set.FromFunc (33) or iter.Map (2) helpers
      • these helpers do the same things but for different data structures: slices, map[T]struct{} and an iterator implementation which predates Go's iterators used only for single-pass heavy inputs (so no slices converted to iterators)
      • they are used to convert data at API boundaries (for example, DB to API models) or pick relevant properties only (for example, take only the IDs out of some input models, because some API requires only IDs).
    • there are no "chained" usages in the codebase (stuff like slicesx.Filter(slicesx.Map(...) ...)); we actively avoid writing that kind of code.
    • all these usages are single-line, single-return, making up 42% of all single-line, single-return lightweight functions
  • almost all other single-line usage is, in order of occurence:
    1. slices.ContainsFunc or an internal slicesx.Has which predates the standard library counterpart
    2. slices.SortFunc or slices.SortStableFunc
    3. lazy values/factory functions
    • in my view all of these would benefit from eliding the return keyword
  • there is exactly 1 occurrence of a lightweight function which may have the danger to turn into the pathological case discussed above; in its current form deduction would succeed without surprises (only interfaces are returned)

I've started this experiment mainly to confirm my feeling that the trends observed in the standard library do not necessarily apply to all Go codebases. Numbers show this to be true. We should consider more datapoints in our decision.

And luckily I've hopefully made it easy enough to test that. To run the experiment on your codebase:

  • checkout the funclight branch of my repository – it is based on the 1.23 release so it should work with all current code
  • cd src && ./make.bash
  • use the gofmt binary outputted to the bin folder to format your codebase

We could also run it on some other relevant open source codebases and see the results.

@jimmyfrasche
Copy link
Member

@griesemer

Writing something like (x, y) => x + y does feel pretty natural. That said, single-expression function literals just don't seem that common in existing Go code. Maybe that's about the change but we don't know yet. Writing (x, y) => { ... } does feel odd to me for Go because we don't use => to indicate a function, we use a keyword.

(x, y) => x + y is also a function so I don't see how that's okay but the other is not? I've written plenty of code with => in javascript and the majority of it is the args => {} variety. It's as fine a syntax as any.

You could require a func prefix like func(args) => expr or func(args) => block. It's a bit redundant but it does let you know you're reading a func before you get to the => but otoh it makes it look like a regular func until the => so you will expect types before you realize it's the other form. You could say that in (a, b, c, d) => you don't know that tuple is a parameter until you get to => which is fair but you get to => fairly quick and it's not like it looks like anything else particular in the meantime.

One problem with using => over -> is that something like this (x, y) => x <= y or (x, y) => x >= y (which I suspect might not be uncommon for filter functions) looks confusing. The single arrow reads better: (x, y) -> x <= y or (x, y) -> x >= y, respectively.

c -> <-c looks weirder to me than (x, y) => x <= y but I'm used to => so that just reads as function of x and y returning x <= y to me. I suppose c -> <-c would be far, far less common, regardless. -> is fine, too.

You could use ~> if you wanna really stand out. 😆

@tmaxmax
Copy link

tmaxmax commented Nov 14, 2024

Some other thoughts I've had:

That is an excellent point about Kotlin/Ruby/Smalltalk, though: the reason they take the arguments in the block is because they allow it to make regular functions look like syntax for DSLing, which is something that Go will presumably never do so it would be very strange to use that style without the promise of that corresponding feature.

@jimmyfrasche The conclusion doesn't necessary follow from the premise – just because other have done it doesn't mean we should also do it. What is important here is that we find a syntax which fits in the current Go code, and if this syntax fits the bill then it is a good choice.

About (x, y) -> exprOrBlock syntax, I think it would look nice. I've never really had a problem with it. Though if we allow eliding the return keyword then you'd have two more distinct forms:

(x, y) -> x + y
(x, y) -> (x + y, x - y)

Of course, unless we choose not to support expression lists and force people to use return. This restriction seems arbitrary to me. The func { params -> exprOrStmts } doesn't have this particularity.

If we decide to not implement return type inference based on body, extending the syntax with at least return types might be appropriate – otherwise there'll be no support for map-like helpers. Here is how I'd imagine it:

(x) string -> strconv.Itoa(x * x)
func { (x) string -> strconv.Itoa(x * x) }

(x) (string, error) -> strconv.Atoi(x)
func { (x) string, error -> strconv.Atoi(x) }

() (int, error) -> fmt.Println("Hi")
func { () int, error -> fmt.Println("Hi")

(x) string -> {
    x *= x
    return strconv.Itoa(x)
}
func { (x) string ->
    x *= x
    return strconv.Itoa(x)
}

A colon could also be used instead of brackets for the func { params -> ... } syntax:

func { x: string -> strconv.Itoa(x) }

From this standpoint I'm not really sure which one's better.

As a final note, I've updated my experiment comment to fix some issues which skewed the results. I've also added some more numbers to create a better image of the results.

@entonio
Copy link

entonio commented Nov 14, 2024

The problem with any notation that doesn't use any form of bracketing around incoming parameters is readability: lightweight function literals will be passed as arguments to other functions.

I believe that a number of us are considering func and -> as the bracketing tokens.

@jimmyfrasche
Copy link
Member

@tmaxmax

The conclusion doesn't necessary follow from the premise – just because other have done it doesn't mean we should also do it.

My point is that if you copy a form fitted for a specific situation without copying that situation as well it seems out of place. They are two pieces designed to fit together so it's weird to have just one. Putting the parameters in the block is a solution to a problem we do not have.

About (x, y) -> exprOrBlock syntax, I think it would look nice. I've never really had a problem with it. Though if we allow eliding the return keyword then you'd have two more distinct forms:

Again, you don't have to do that. You can say that the expr syntax is only for the special case of a single expression. That's reasonable. It makes really simple cases very short and everything else comfortably short.

If we decide to not implement return type inference

If we're not doing inference the regular func syntax is fine so there is no need to consider this.

@earthboundkid
Copy link
Contributor

You could use fat arrow and skinny arrow, with one being the form for return expression and the other being the form for statement block. The downside of that is moving from a single expression to a statement block is more typing, but it's very clear for readers.

@jimmyfrasche
Copy link
Member

That would require two tokens and remembering which is which.

@DeedleFake
Copy link

I feel like we're retreading ground that was covered, at least in part, before the func { a, b -> expr } and func { a, b; statements } syntaxes were proposed and that they were designed specifically to address.

@jimmyfrasche
Copy link
Member

Straw poll for favored kind of syntax. This is just about general preference. Vote for as many as you like.

Assume that they all have equal expressive power and that any issues will be worked out.

  • 🚀 => or ->
  • 🎉 arguments in the brackets like { a, b, c |
  • ❤️ func a, b, c
  • 👀 something else

@tmaxmax
Copy link

tmaxmax commented Nov 14, 2024

Reasons how I see func { params -> exprOrStmts } being better:

  1. Easiness to parse.
    The following form can be parsed today, from my experience working with the Go parser, without adding any additional complexity to the code:
FuncLight = "func" "{" [ IdentifierList ] "->" [ Expression | StatementList ] "}" .
  1. Has func at the beginning, which was something people where very vocal towards in the past
  2. Less annoying to work with:
    • typing it the first time is less annoying
      • for the (params) -> expr | { stmts } form you always have to jump over the closing bracket in order to type further. For a normal user this implies pressing the right arrow key, for a Vim user this implies going to Normal mode, pressing L and going back to Insert mode.
      • for the func { params -> exprOrStmts } you never have to do any cursor movements or switch between modes in modal editors – one press of Shift for the braces and you're good to go
    • changing it is less annoying
      • inserting new statements does not imply wrapping everything in braces
      • removing all statements except one does not imply removing the braces (albeit gofmt -s could rewrite that)
    • personal note: these are reasons why I've never really liked the JS syntax

Here are various examples of both, picked up from the standard library:

nextToken := func { ->
	cntNewline--
	tok, _ := buf.ReadString('\n')
	return strings.TrimRight(tok, "\n")
}
nextToken := () -> {
	cntNewline--
	tok, _ := buf.ReadString('\n')
	return strings.TrimRight(tok, "\n")
}

slices.SortFunc(r.fileList, func { a, b -> fileEntryCompare(a.name, b.name) })
slices.SortFunc(r.fileList, (a, b) -> fileEntryCompare(a.name, b.name))

// assuming expression list return
compressors.Store(Store, Compressor(func { w -> &nopCloser{w}, nil }))
compressors.Store(Store, Compressor((w) -> (&nopCloser{w}, nil)))
// without
compressors.Store(Store, Compressor(func { w -> return &nopCloser{w}, nil }))
compressors.Store(Store, Compressor((w) -> { return &nopCloser{w}, nil }))

b.Run("same", func { b -> benchBytes(b, sizes, bmEqual(func { a, b -> Equal(a, a) })) })
b.Run("same", (b) -> benchBytes(b, sizes, bmEqual((a, b) -> Equal(a, a)))) // would this be allowed? `benchBytes` returns nothing
// or, on multiple lines
b.Run("same", func { b ->
    benchBytes(b, sizes, bmEqual(func { a, b -> Equal(a, a) }))
})
b.Run("same", (b) -> {
    benchBytes(b, sizes, bmEqual((a, b) -> Equal(a, a)))
})

removeTag = func { v -> v &^ (0xff << (64 - 8)) }
removeTag = (v) -> v &^ (0xff << (64 - 8))

forFieldList(fntype.Results, func { i, aname, atype -> resultCount++ })
forFieldList(fntype.Results, (i, aname, atype) -> { resultCount++ })

arch.SSAMarkMoves = func { s, b -> }
arch.SSAMarkMoves = (s, b) -> {}

return d.matchAndLog(bisect.Hash(pkg, fn), func { -> pkg + "." + fn }, note)
return d.matchAndLog(bisect.Hash(pkg, fn), () -> pkg + "." + fn, note)

ctxt.AllPos(pos, func { p -> stk = append(stk, format(p)) })
ctxt.AllPos(pos, (p) -> { stk = append(stk, format(p)) })

i := sort.Search(len(marks), func { i -> xposBefore(pos, marks[i].Pos) })
i := sort.Search(len(marks), (i) -> xposBefore(pos, marks[i].Pos))

sort.Slice(scope.vars, func { i, j -> scope.vars[i].expr < scope.vars[j].expr })
sort.Slice(scope.vars, (i, j) -> scope.vars[i].expr < scope.vars[j].expr)

queue = func { work -> workq <- work }
queue = (work) -> { workq <- work }

desc := func { -> describe(n) }
desc := () -> describe(n)

check := hd.MatchPkgFunc("bar", "0", func { -> "note" })
check := hd.MatchPkgFunc("bar", "0", () -> "note")

var unparen func(ir.Node) ir.Node
unparen = func { n ->
	if paren, ok := n.(*ir.ParenExpr); ok {
		n = paren.X
	}
	ir.EditChildren(n, unparen)
	return n
}
var unparen func(ir.Node) ir.Node
unparen = (n) -> {
	if paren, ok := n.(*ir.ParenExpr); ok {
		n = paren.X
	}
	ir.EditChildren(n, unparen)
	return n
}

var do func(Node) bool
do = func { x -> cond(x) || DoChildren(x, do) }
var do func(Node) bool
do = (x) -> cond(x) || DoChildren(x, do)

if withKey(func { key -> C._goboringcrypto_EVP_PKEY_set1_RSA(pkey, key) }) == 0 {
	return pkey, ctx, fail("EVP_PKEY_set1_RSA")
}
if withKey((key) -> C._goboringcrypto_EVP_PKEY_set1_RSA(pkey, key)) == 0 {
	return pkey, ctx, fail("EVP_PKEY_set1_RSA")
}

b.Run("2048", func { b -> benchmarkDecryptPKCS1v15(b, test2048Key) })
b.Run("3072", func { b -> benchmarkDecryptPKCS1v15(b, test3072Key) })
b.Run("4096", func { b -> benchmarkDecryptPKCS1v15(b, test4096Key) })
b.Run("2048", (b) -> benchmarkDecryptPKCS1v15(b, test2048Key))
b.Run("3072", (b) -> benchmarkDecryptPKCS1v15(b, test3072Key))
b.Run("4096", (b) -> benchmarkDecryptPKCS1v15(b, test4096Key))

slices.SortFunc(list, func { a, b -> bytealg.CompareString(a.Name(), b.Name()) })
slices.SortFunc(list, (a, b) -> bytealg.CompareString(a.Name(), b.Name()))

testMul("Mul64 intrinsic", func { x, y -> Mul64(x, y) }, a.x, a.y, a.hi, a.lo)
testMul("Mul64 intrinsic symmetric", func { x, y -> Mul64(x, y) }, a.y, a.x, a.hi, a.lo)
testDiv("Div64 intrinsic", func { hi, lo, y -> Div64(hi, lo, y) }, a.hi, a.lo+a.r, a.y, a.x, a.r)
testDiv("Div64 intrinsic symmetric", func { hi, lo, y -> Div64(hi, lo, y) }, a.hi, a.lo+a.r, a.x, a.y, a.r)
testMul("Mul64 intrinsic", (x, y) -> Mul64(x, y), a.x, a.y, a.hi, a.lo)
testMul("Mul64 intrinsic symmetric", (x, y) -> Mul64(x, y), a.y, a.x, a.hi, a.lo)
testDiv("Div64 intrinsic", (hi, lo, y) -> Div64(hi, lo, y), a.hi, a.lo+a.r, a.y, a.x, a.r)
testDiv("Div64 intrinsic symmetric", (hi, lo, y) -> Div64(hi, lo, y), a.hi, a.lo+a.r, a.x, a.y, a.r)

baseHandler := http.HandlerFunc(func { rw, req ->
	fmt.Fprintf(rw, "basepath=%s\n", req.URL.Path)
	fmt.Fprintf(rw, "remoteaddr=%s\n", req.RemoteAddr)
})
baseHandler := http.HandlerFunc((rw, req) -> {
	fmt.Fprintf(rw, "basepath=%s\n", req.URL.Path)
	fmt.Fprintf(rw, "remoteaddr=%s\n", req.RemoteAddr)
})

// no expression list return
req.GetBody = func { -> return NoBody, nil }
req.GetBody = () -> { return NoBody, nil }
// with expression list return
req.GetBody = func { -> NoBody, nil }
req.GetBody = () -> (NoBody, nil)

and so on.

Some neutral examples from my work codebase:

assetPaths := slicesx.Map(assets, func { a -> a.Path })
assetPaths := slicesx.Map(assets, (a) -> a.Path)

slicesx.Map(o.Features, func { f -> string(f) }) // an enum of some sort
slicesx.Map(o.Features, (f) -> string(f))

slicesx.Map(items[i:], func { it -> it.price.Mul(it.quantity).Round(2) })
slicesx.Map(items[i:], (it) -> it.price.Mul(it.quantity).Round(2))

slices.SortFunc(tags, func { i, j -> strings.Compare(i.key(), j.key()) })
slices.SortFunc(tags, (i, j) -> strings.Compare(i.key(), j.key()))

@jimmyfrasche
Copy link
Member

@txmaxmax

Easiness to parse

A good property indeed, and we shouldn't have anything that's hard to parse, but it's not top of the list.

Has func at the beginning, which was something people where very vocal towards in the past

Some people said that but I believe more people voted against it in #21498 (comment)

I don't have a problem with involving func personally but it's not a must. I've used => a lot in other langs and it's perfectly fine.

Less annoying to work with: typing it the first time is less annoying [...] changing it is less annoying

writability and editability are good properties, especially in a convenience syntax, but readabiltiy is still the main one. One of the reasons a short form is good is because by removing all the redundant information you focus on what's important. For a lot of the places I want short functions I get the regular function written out by my editor so it's not like it would really be saving me many key presses.

It's true if you have x -> x and you want to change it to a block there's a small bit of typing to change it to x -> { fmt.Println(x); return x } but the important bit is that the first is very clearly a simple expression while something more complicated is happening in the last one.

Most of the time expression lambdas are something like filter(x -> x < 0) and block lambdas are stuff like t.Run(t -> { //... so it's pretty clear which one you want upfront and it's unlikely to change often and even if it does change we're still not talking about a tremendous amount of physical effort .

I think any syntax that does not have separate forms is bad. It makes it harder to tell what is happening at a glace when it should be making it easier to tell what's happening at a glance.

If there is some fatal flaw in arrow functions the next best syntax is python/ocaml/sml/f# style, extended to have expression or block forms:

func x, y: expr
func x, y { block }

When you count func as the opening bracket, that has all the desired properties. (: could also be =).

In some ways I think it's superior, but it's just not as common or as popular as arrow functions so there needs to be a real showstopping good reason not use arrow functions before anything else is considered.

@tmaxmax
Copy link

tmaxmax commented Nov 15, 2024

One of the reasons a short form is good is because by removing all the redundant information you focus on what's important.

The func { -> } syntax would make a further distinction redundant – whether the statement inside evaluates to something or not. When you see the code:

slices.SortFunc(s, func { a, b -> strings.Compare(a.Name, b.Name) })

the only thing that matters now is that you're sorting by the name of whatever values are there. If a lightweight function form which distinguishes between expression form and statement form were used, then suddenly the type the expression evaluates to, the type of the return value of the function become important. It is not important that the sort predicate returns int – all I care is that I sort by name. The func { -> } syntax provides a higher level of abstraction than the arrow syntax, in the same way type inference is a form of abstraction.

The point would be even further driven if that code were to look like:

slices.SortBy(s, { a, b -> strings.Compare(a.Name, b.Name) })

Of course, it will never look like this in Go (though the more I read the parser code, the more this looks possible to parse). And maybe that's the issue with this syntax – that in Go it will never be brought to its full potential. I'm not referring to the DSL-like stuff Kotlin and Swift do with having these literals outside of the call parens as a block; this looks like syntax abuse to me and I'd honestly never write code like this:

slices.SortBy(s) { a, b -> strings.Compare(a.Name, b.Name) }

I'm referring to the following:

slices.SortBy(s, { a, b ->
    aName := normalize(a.Name)
    bName := normalize(b.Name)
    strings.Compare(aName, bName)
})

Implicit returns won't be a thing in Go, and without implicit returns the supposed additional abstraction capacity of the syntax is half-baked. If you think about it, without implicit returns this syntax still has two forms – when converting between one expression and multiple statements you'd have to add/remove the return keyword. Which is basically the same as adding/removing braces but way easier to forget.

The annoyance I'd have with adding/removing braces would be the same when seeing yet again the compiler error "function returns () but call site expects (int)" or similar.

This one value proposition I keep vouching, on further thought, doesn't seem to stand.

I think any syntax that does not have separate forms is bad. It makes it harder to tell what is happening at a glace when it should be making it easier to tell what's happening at a glance.

I don't think this is bad in absolute terms, as you seemingly put it, for the reasons I've described above. I think the real issue here is that this sort of abstraction couldn't really be supported by Go. Introducing implicit returns just for lightweight functions is probably not a viable direction for the language.

The point I'm trying to make here is that the discussion is more nuanced than "single form is bad". I personally find a distinct elegance in these sorts of abstractions – for example, in the way an OCaml function is fully typed just by using the right operators. Go as a language took another direction, though, which implies rather a lack of abstraction in order to favour explicitness. Go tries to find a pragmatic middle ground between implicit and explicit, favouring the latter. For some people, for example, this feature would cross their acceptable threshold of implicitness – this is why there are people opposed to the feature altogether. It's also why arguments like "but you still use := every day" aren't persuasive – := doesn't cross their acceptable threshold of implicitness. This threshold is in the end subjective.

Philosophically at least I can agree that the arrow functions seem to be a better fit.


Now, purely from an aesthetic standpoint, if I compare the two syntaxes above it's really a give and take:

  • In some scenarios I appreciate having the func keyword: test functions, callbacks in longer parameter lists, in general function literals used in more imperative contexts
  • In some other scenarios that func stutters: namely in contexts of a http.HandlerFunc, slices.SortFunc etc., for those recursive functions where there's a func just above, or for lazy values
    • the Go libraries have made it a priority to signal where comes a func and where it does not – having that func again is just superfluous
  • In most cases I like the separation the braces provide, clearly distinguishing the function body from the rest of the code
    • the expression version of the arrow syntax has this habit of transforming the call site into a ) soup, given that it's not wrapping the body in any way; the braces do a very nice job at preventing that
  • In some cases the braces are just noise – look at the simple mappings or sorts
    • though the moment there's a function call in the predicate I start to prefer it, just so the parens don't stick together
  • The func { -> } syntax has the pathological case func { work -> workq <- work }, which reminds me of the --> "operator" from C++. No.
  • The arrow functions kinda get lost in longer parameter lists, especially when they are short

They both have their strengths and weaknesses. What makes me lean towards the func { -> } syntax is that it avoids the ) tails, which I personally dislike more than the stuttering funcs.

About the func a, b: expr and func a, b { stmts } syntax, it's been already established that they don't look good in parameter lists. Personally I dislike the Python syntax the most and the OCaml lambda function syntax is the only syntax from that language I find really odd and unfitting.


In the end, my conclusion would be that the arrow function does the job in a straightforward but mildly annoying way, with various inconveniences. The func { -> } syntax, albeit more elegant looking, may indeed confuse with respect to the code meaning – whereas the arrow function, albeit sometimes requiring to disentangle chains of closing brackets, will never do that.

The arrow function is boring, familiar, does what it's asked to do and nothing more. Just like Go. And given public preference, maybe it's the way to go.


Relevant links so they don't get lost:

@DeedleFake
Copy link

The point would be even further driven if that code were to look like:

slices.SortBy(s, { a, b -> strings.Compare(a.Name, b.Name) })

Of course, it will never look like this in Go (though the more I read the parser code, the more this looks possible to parse).

I actually proposed this before: #21498 (comment). My idea at the time for avoiding the ambiguity problems was to only allow it in function arguments, as there is currently nothing that starts with { as the first token in that context. Although, thinking about it again, it would probably be fine in any expression context, which is all of the places that short functions make sense anyways.

@entonio
Copy link

entonio commented Nov 15, 2024

My reasons to favour the

func x, y -> expression

syntax:

  • it goes from left to right without backtracking (anything involving parenthesis or brackets will result in IDE inserting the matching tokens but leaving the cursor after it, it could be different but it's what always happens;
  • it requires only two symbols (the arrow) which are among the easiest to type in any keyboard layout (you don't begin to imagine the hoops non-US programmers have to go through to insert {}, of course we do it quickly and easily after a while, but it's never pleasant (that's also why I prefer -> over =>);
  • visually it relies more on text and context than on symbols, which makes it more go-like in my book;
  • it's distinct from the syntax that takes a block, which to me is an advantage of readability and even structural discipline (as I feel that the two kinds answer distinct problems, and if in some piece of code the single expression syntax isn't applicable maybe it's a sign that the structure of that piece of code should be different); (this is the single most important point to me)
  • I find the func keyword, balanced with the ->, establish the mental switch to 'this is a function' better than the more polysemic (), which moreover will often come after another (.

Some of the above are specific to how I expect go code to read, others are more general.

As to the disadvantages that have been mentioned, my opinion is that they're hypothetical rather than practical.

We can do polls, but those will only represent the people in this thread who happen to see the specific poll. I think the solution should be justified not only on popularity, but also on go-ish-ness.

@jimmyfrasche
Copy link
Member

@tmaxmax

The func { -> } syntax would make a further distinction redundant – whether the statement inside evaluates to something or not.

That is not redundant information. It may not always matter or be important but when it does it is.

About the func a, b: expr and func a, b { stmts } syntax, it's been already established that they don't look good in parameter lists.

Some people have stated that and it's certainly a -1 and moves it down the list but it's not a fatal flaw imo.

It is a bit inaesthetic but it's fine when you get used to it. It only looks wrong if you don't understand the syntax and are parsing it wrong in your head, but if you're looking at code and you don't understand the syntax you'd then need to look that up so the problem solves itself.

No one likes Python's lambda but the reason isn't because of the commas in f(x, lambda a, b: a*y + b). Anyone familiar reads that as the application of f taking two arguments one of which is a lambda of two arguments. It doesn't require further bracketing. Python's lambda is disliked because the RHS can only be an expression.

@tmaxmax
Copy link

tmaxmax commented Nov 15, 2024

@jimmyfrasche

That is not redundant information. It may not always matter or be important but when it does it is.

This feels dismissive of everything else I've written. I've even argued in that text against hiding this information in the context of Go.

Some people have stated that and it's certainly a -1 and moves it down the list but it's not a fatal flaw imo.

The other forms don't have this flaw, so why should we accept it?

Python's lambda is disliked because the RHS can only be an expression.

I also dislike it for the unparenthesised parameter list. Some others do. "Getting used to it" is not really an argument, as one can get used to anything if it is required.

@entonio Ignoring that this syntax or similar has been discussed already, taking each of the points you make in order:

  1. that's not my experience: in any editor I've used the cursor was always placed inside the symbol pair; i'll agree it's easier to type anyway
  2. what do you do with the block form? You still need {} for that. Is this a reaction only to the func { -> } syntax? How about a comparison with the arrow syntax, which is even more relevant than that one?
  3. http.HandleFunc(func w, r -> ...) why have func twice? It depends on the context whether it helps or not, seems again dismissive of my previous comment, where I've addressed the usefulness of having the keyword or not
  4. how's it different in this regard to the arrow function syntax? Again seems dismissive of my previous comment and of general sentiment – neither I am (anymore), nor the general public is strongly advocating for func { -> } but for the arrow syntax
  5. again – http.HandleFunc(func w, r -> ...) – there are quite a few places where the additional func stutters and the mental switch is already made through the assignment scope's name
    • on the left side you can also never have more than two required (; the right side is the issue, and neither the arrow syntax, nor this syntax you propose solve that

my opinion is that they're hypothetical rather than practical.

And your opinion is based on what? Have you seen the code? Do you like the following:

f.walk(arg, ctxExpr, func f, arg, context { ... })
forFieldList(fntype.Results, func i, aname, atype -> argField(atype, "r%d", i))
testDiv("Div64 intrinsic", func hi, lo, y -> Div64(hi, lo, y), a.hi, a.lo+a.r, a.y, a.x, a.r)
slices.SortFunc(list, func a, b -> bytealg.CompareString(a.Name(), b.Name()))

Arrow functions at least keep things cohesive:

f.walk(arg, ctxExpr, (f, arg, context) -> { ... })
forFieldList(fntype.Results, (i, aname, atype) -> argField(atype, "r%d", i))
testDiv("Div64 intrinsic", (hi, lo, y) -> Div64(hi, lo, y), a.hi, a.lo+a.r, a.y, a.x, a.r)
slices.SortFunc(list, (a, b) -> bytealg.CompareString(a.Name(), b.Name()))

but also on go-ish-ness

What is "go-ish-ness"? This whole thread is sprinkled with debates over that.


I get it, some like syntax A, some like syntax B, and they really want to see them live. But this isn't the way. At least have the courtesy to take in consideration previous arguments, try as much as possible to base your opinions on something concrete and to accommodate others' critiques or concerns in your proposals. There's no hope for consensus otherwise.

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

No branches or pull requests