golang / go

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

proposal: spec: reduce noise in return statements that contain mostly zero values #21182

Open jimmyfrasche opened 7 years ago

jimmyfrasche commented 7 years ago

Update: the current proposal is to permit return ..., v to return the zero value for all but the last result, and to return v as the last result. The most common use will be return ..., err.

A variant under discussion include return ..., v1, v2 to return zeroes for all but the last N.

Another variant is to permit return ... to return zeroes for all.

In general ... is only permitted if it omits one or more values--func F() err { return ..., errors.New("") } is not permitted.


Proposal

In return statements, allow ... to signify, roughly, "and everything else is the zero value". It can replace one or more zero values.

This is best described by example:

Given the function signature func() (int, string, *T, Struct, error):

return 0, "", nil, Struct{}, nil may be written return ...

return 0, "", nil, Struct{}, err may be written return ..., err

return 0, "", nil, Struct{X: Y}, nil may be written return ..., Struct{X: Y}, nil

return 1, "", nil, Struct{}, nil may be written return 1, ...

return 1, "a", nil, Struct{}, nil may be written return 1, "a", ...

return 1, "", nil, Struct{}, err may be written return 1, ..., err

return 1, "a", nil, Struct{X: Y}, err may be written return 1, "a", ..., Struct{X: Y}, err

The following is invalid:

return ..., Struct{X: Y}, ... — there can be at most one ... in a return statement

Rationale

It is common for a function with multiple return values to return only one non-zero result when returning early due to errors.

This creates several annoyances of varying degrees.

When writing the code one or more zero values must be manually specified. This is at best a minor annoyance and not worth a language change.

Editing the code after changing the type of, removing one of, or adding another return value is quite annoying but the compiler is fast enough and helpful enough to largely mitigate this.

For both of the above external tooling can help: https://github.com/sqs/goreturns

However, the unsolved problem and motivation for the proposal is that it is quite annoying to read code like this. When reading return 0, "", nil, Struct{}, err unnecessary time is spent pattern matching the majority of the return values with the various zero value forms. The only signal, err, is pushed off to the side. The same intent is coded more explicitly and more directly with return ..., err. Additionally, the previous two minor annoyances go away with this more explicit form.

History

This is a generalized version of a suggestion made by @nigeltao in https://github.com/golang/go/issues/19642#issuecomment-288559297 where #19642 was a proposal to allow a single token, _, to be sugar for the zero value of any type.

I revived the notion in https://github.com/golang/go/issues/21161#issuecomment-317891763 where #21161 is the currently latest proposal to simplify the if err != nil { return err } boilerplate.

Discussion

This can be handled entirely with the naked return, but that has greater readability issues, can lead too easily to returning the wrong or partially constructed values, and is generally (and correctly) frowned upon in all but the simplest of cases.

Having a universal zero value, like _ reduces the need to recognize individual entries as a zero value greatly improving the readability, but is still somewhat noisy as it must encode n zero values in the common case of return _, _, _, _, err. It is a more general proposal but, outside of returns, the use cases for a universal zero value largely only help with the case of a non-pointer struct literal. I believe the correct way to deal that is to increase the contexts in which the type of a struct literal may be elided as described in #12854

In https://github.com/golang/go/issues/19642#issuecomment-288644162 @rogpeppe suggested the following workaround:

func f() (int, string, *T, Struct, error) {
  fail := func(err error) (int, string, *T, Struct, error) {
    return 0, "", nil, Struct{}, err
  }
  // ...
  if err != nil {
    return fail(err)
  }
  // ...
}

This has the benefit of introducing nothing new to the language. It reduces the annoyances caused by writing and editing the return values by creating a single place to write/edit the return values. It helps a lot with the reading but still has some boilerplate to read and take in. However, this pattern could be sufficient.

This proposal would complicate the grammar for the return statement and hence the go/ast package so it is not backwards compatible in the strict Go 1 sense, but as the construction is currently illegal and undefined it is compatible in the the Go 2 sense.

Possible restrictions

Only allow a single value other than ....

Only allow it on the left (return ..., err).

Do not allow it in the middle (return 1, ..., err).

While these restrictions are likely how it would be used in practice anyway, I don't see a need for the limitations. If it proves troublesome in practice it could be flagged by a linter.

Possible generalizations

Allow it to replace zero or more values making the below legal:

func f() error {
  // ...
  if err != nil {
    return ..., err
  }
  // ...
}

This would allow easier writing and editing but hurt the readability by implying that there were other possible returns. It would also make this non-sequitur legal: func noop() { return ... }. It does not seem worth it.

Allow ... in assignments. This would allow resetting of many variables to zero at once like a, b, c = ... (but not var a, b, c = ... or a, b, c := ... as their is no type to deduce) . In this case I believe the explicitness of the zero values is more a boon than an impediment. This is also far less common in actual code than a return returning multiple zero values.

OneOfOne commented 7 years ago

Why not named returns? func f() (i int, ss string, t *T, s Struct, err error) {}

rogpeppe commented 7 years ago

I like this idea, but I can't think of any place where I'd use it for anything other than filling in all but the last value. Given that, I think one could reasonably make it a little less general and allow only this form (allowing any expression instead of err, naturally)

return ...err

Note the lack of comma. I'm not sure whether it's better with a space before "err" or not.

jimmyfrasche commented 7 years ago

@OneOfOne Naked returns are fine for very short functions but are harder to scale: it gets too easy to accidentally return partially constructed values or the wrong err because of shadowing. Other than that, or maybe because of that, I like the explicit syntax better. A naked return says "go up to the function signature to see what can be returned, then trace through the code to see what actually gets returned here" whereas return ..., err says "I'm returning a bunch of zero values and the value bound to the err in scope"

@rogpeppe that was the original syntax proposed that I based this proposal off of. I don't like it because it appears to be a spread operator common in dynamic languages so it's a bit confusing. Having the comma makes it superficially more similar to "⋯" in mathematical writing and with like purpose. I agree that this would almost always be used as return ..., last where last is a bool or error and sometimes as a return ..., but I don't see any particular reason to artificially limit it to that. If it's not very useful it will not get used often. Is there any particular concern that a line like return 7, ... would be more confusing or error prone than a return ..., true?

rogpeppe commented 7 years ago

@jimmyfrasche I just don't see that it would ever be used, and given that, the comma seems like unnecessary overhead for what would be a very commonly used idiom.

How many places in existing code can you find where eliding all but the first argument (or all but several arguments) would be useful?

ianlancetaylor commented 7 years ago

If we permit both ..., v and v, ... how do we justify the exclusion of ..., v, ...? Except for that fact that it is impossible to implement?

jimmyfrasche commented 7 years ago

@rogpeppe return ...aSlice looks too much like https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator That's not a strong argument against it, but I'd still rather not see it. While I would be surprised if this proposal is accepted in any form, I certainly wouldn't be mad if that variant was the one accepted, as it is the most common case. I really don't see any justification for the limitation, personally. It would be interesting to run an analysis on go-corpus to see if there are any. I don't have the time to do that for a bit, so anyone can feel free to beat me to it.

@ianlancetaylor that's a fine point. Impossible to implement is justification enough for me. Though it would be possible to implement in some cases, where v is of a type returned only once, but then subtle and distance changes could make it suddenly ambiguous. I would note that the other two existing uses of ... are also "at most once". You can't do func(a ...string, b ...int) or append(bs, s1..., s2...) even though the first is only sometimes unambiguous and the latter would be merely inefficient (or rather non-obviously inefficient).

Another option, that I'm fairly sure is a bad idea, would be to allow "keyed returns" to work in conjunction with named returns, by analogy with keyed struct literals:

func() (a int, b string, err error) {
  // ...
  if err != nil {
    return err: err
  }
  // ...
  return b: name, a: -x // I purposely flipped the order here
}

though that would interact poorly with the semantics of the naked return. If a is non-zero what happens when I write return err: err? The answer should clearly be that only err is returned, but it's still a potential source of confusion.

mewmew commented 7 years ago

Counter-proposal that has been suggested elsewhere in the past (https://github.com/golang/go/issues/19642). Allow _ to be used as the zero value of any type in expressions. Currently _ in var _ T = foo may be thought of as foo > /dev/null (from Effective Go). Similarly, _ in var foo T = _ could be thought of as foo < /dev/null. Then _ could be used in return statements.

E.g. given the function signature func() (int, string, *T, Struct, error):

return 0, "", nil, Struct{}, nil may be written return _, _, _, _, _

return 0, "", nil, Struct{}, err may be written return return _, _, _, _, err

return 0, "", nil, Struct{X: Y}, nil may be written return _, _, _, Struct{X: Y}, _

return 1, "", nil, Struct{}, nil may be written return 1, _, _, _, _

return 1, "a", nil, Struct{}, nil may be written return 1, "a", _, _, _

return 1, "", nil, Struct{}, err may be written return 1, _, _, _, err

return 1, "a", nil, Struct{X: Y}, err may be written return 1, "a", _, Struct{X: Y}, err

davecheney commented 7 years ago

When your functions has so many return values that typing them becomes a chore, that's a sign that you need to redesign your function, not the language.

mewmew commented 7 years ago

When your functions has so many return values that typing them becomes a chore, that's a sign that you need to redesign your function, not the language.

I think the example was given simply to provide a one of each function signature that is useful as a showcase. While not likely to see so many return values in real world code, returning the zero value of a struct using _ rather than image.Rectangle{} may improve readability; at least that's the idea of the proposal.

jimmyfrasche commented 7 years ago

@davecheney indeed.

My argument is that the primary benefit of the succinct syntax is that it improves the readability. If it makes it easier to type that's just a bonus.

If you see return ..., err you don't need to think about what else is being returned.

You don't need to double check for things that are suspiciously close to a zero value like return Struct{''}, err or return O, err or return nil, err (when nil has been shadowed for some reason).

It's immediately obvious that the only relevant value is err. Idioms such as

if err != nil {
  return ..., err
}

can be pattern matched by your brain as a unit without having to actually inspect anything. I'm sure we all do that now with similar blocks that contain one or more zero value-like expression. It's bitten me once or twice when I was debugging and my eye glazed over something that looked too close to a zero value making it hard to spot the obvious problem (I of course do not admit publicly to being the person who shadowed nil . . .).

I'm fine with how it is, however. This is just a potential way to make it a little bit easier.

@mewmew yes this proposal is based on a comment from that proposal (see the History section). I don't particularly see the point of the generic zero value except in the case of returns. It would solve the same problem, of course.

(I would like to be able to use {} as the zero value of any struct when its type can be deduced from the rest of the statement.)

nigeltao commented 7 years ago

@davecheney sometimes it's not the number of return values, it's their struct-ness. Typing return image.Rectangle{}, err can be a chore. The image.Rectangle{} is the longest but also least important part of the line.

That said, this particular proposal is not the only way to sooth that chore, as the OP noted.

ianlancetaylor commented 5 years ago

Reopening per discussion in #33152.

earthboundkid commented 5 years ago

I am in favor of allowing only the return ..., x form. If other forms are good, they could be added later.

The main benefit of this for me is that it would let me use a dumb macro to expand ife into

if err != nil {
   return ..., err
}

Yes, a sufficiently smart IDE macro could look at the function return arguments to fill those in for me, but why not just simplify it so only the important information is emphasized?

bradfitz commented 5 years ago

What about letting return take 1 thing regardless of how many results the func has, as long as the assignment is unambiguous to exactly 1 of the return types?

So:

    func foo() (*T, error) {
        if fail {
             return errors.New("foo")
        }
        return new(T)       
    }
ianlancetaylor commented 5 years ago

Does anybody see any problems with this language change?

We think we should consider just the simple case: return ..., val1, val2 where val1 and val2 become the final results of the function. Typically, of course, this would be just return ..., err. The other cases don't seem to arise enough to worry about. This would only be permitted if there are other results; it could not be used in a function that returns only error. The omitted results would be set to the zero value, even if the result parameters are named and currently set to some non-zero value.

-- for @golang/proposal-review

jimmyfrasche commented 5 years ago

@bradfitz that seems like it could cause too much fun when the return signature changes in a long func or one of the return types now satisfies an interface. It would also be unusable with interface{}, though that's a bit niche.

bradfitz commented 5 years ago

It would also be unusable with interface{}, though that's a bit niche.

That's a feature.

robpike commented 5 years ago

Is this legal?

var err e
func f() error {
   return ..., e
}

Is this?

func f() {
   return ...
}

Is this?

func f() int {
   return ...
}
griesemer commented 5 years ago

It seems confusing to have ... stand for nothing. I would require ... to stand for one or more zero values. So in your examples above, I'd only consider the last one permissible.

earthboundkid commented 5 years ago

There’s a typo in the first example but otherwise I would permit all three.

ghost commented 5 years ago

I'd reject all three and only allow return ..., something (when there are 2+ return values).

nigeltao commented 5 years ago

The ... in func Printf(format string, a ...interface{}) means zero or more.

alanfo commented 5 years ago

The ... in func Printf(format string, a ...interface{}) means zero or more.

True, though ... is used there as a type qualifier and is adjoined to the type.

Under this proposal, it would represent default return arguments and would be separated from the final non-default arguments by a comma. So, in this usage, I don't think it's unreasonable for it to represent a minimum of one argument and (to me at least) it would be both strange and potentially confusing for it do otherwise.

I'd be in favor of this proposal - in the restricted form described in @ianlancetaylor's latest post - though I'm not sure it's worth going beyond return ..., val in practice.

It's backwards compatible and looks like it should not be too difficult to implement. Whilst it can be argued that specifying individual return values is always clearer, an abbreviation seems reasonable when (in the case of a final error or bool) you don't care about the values of the other return values and so having to specify them is just noise.

One of the things I liked about the try proposal was that the default values of any other parameters would be returned, albeit under the hood, in the event of an error.

This proposal would achieve the same thing (and therefore save a little error handling boilerplate) but in a visible fashion which shouldn't upset those who didn't like try and would be optional in any case.

avi-yadav commented 4 years ago

Rather than return ...val operator, I would like to go with return _, val1,_, val2approach. It gives more flexibility in inserting val anywhere without having to read a lot of rules around the operator. In functions having 2 or 3 return values, both are equally readable.

robpike commented 4 years ago

@avi-yadav That would be the first time _ is used as an identifier to be read rather than written. This issue has come up before, and ... is actually more consistent with Go usage. For instance, in array declarations it stands for the count of the array, or as one might say here, "the obvious thing".

jimmyfrasche commented 4 years ago

My current thoughts on the three examples @robpike showed are

  1. allow all three
  2. discourage cases where ... represents 0 return values by linting
  3. have a tool that removes ... when it represents 0 return values

Allowing all three allows code generators working with a variable number of returns to avoid special cases. @carlmjohnson demonstrated a useful case earlier: paste the if err != nil { return ..., err } snippet regardless of the return-arity. Easy for the writer and it's easy to have any unnecessary ... removed on save or pre-commit for the reader.

return ... is the most interesting.

In the case where it could be rewritten return 0 there's little point. If the type of the value to return is unknown because it was created by code generation or it's a generic function and the syntactic form of the zero value is unknown, it's more useful.

In regular code, if ... stands for the zero of a struct value it's more concise, but that case would, imo, be better served by extending type elision.

There's also this case to consider:

func f() (T, error) {
  if someCond() {
    // zero of T suffices, no error
    return ...
  }
  // create a T in way that may fail
}

It would be strange to have to write return ..., nil there and return T{}, nil isn't much better. It's something of an uncommon case and deserves comment no matter what.

seebs commented 4 years ago

Idle thought: What about named return values?

I can't figure out whether I'd expect return ..., err to overwrite them with zeros or leave them alone.

Intuitively, I think, I would expect a _ in a return list to imply not-overwriting a named return value which is already set, but I don't know why I'd expect that.

flibustenet commented 4 years ago

This feature will be very fine when we have not goreturns with gopls

lolbinarycat commented 4 years ago

I would just like to emphasize the parallel to array declarations, [...]. This is a fairly similar usage (getting rid of the obvious thing), and so I think it fits with go more than _, although I wouldn't mind that either.

aleass commented 2 years ago

Toward to php?Why not named returns?

gregwebs commented 2 years ago

I want to make a principled argument for what exact form should be supported. Principles 1) Go code is trying to turn a result type of success/failure 2) It is good to reduce the noise in a diff when altering code

Success/Failure

For practical purposes we only need ... to represent the fact that we are returning an error and zeroing everything else.

Although not every go function returns an error, that is the common case that we should ensure that we address well. In this case, the community has standardized on returning a single error value as the last value in the function. Additionally, the typical case is that when there is no error, the non-error returned values are all non-empty. We can model this function as:

func(in any) (ok any, err error)

If Go had first class tuples and we need multiple values, we could use a tuple for the ok parameter. Tuples are not first class in Go, but effectively they are constructed at the return site and de-structured at the call site.

So for the common case ... is only needed to model the fact that we are attempting to just return error and don't care about the rest. We can support more than that, but that is only to avoid arbitrary language restrictions rather than to be practically useful.

Reducing diff noise

If I can write return ..., err in any function whose last return result is error, that means, regardless of the change of the success type, I don't have to change the return statements in my function. Thus, ... should stand for 0 or more.

So I can start from this.

func(x int) (error) { return ..., errors.New("error") }

When I add a return value, I don't need to change the error return statement

func(x int) (int, error) { return ..., errors.New("error") }

Supporting other cases

I don't think there is a practical benefit to the vast majority of code to supporting anything other than return ..., error. I suggest we can just start with focusing on this. Of course, we don't need to restrict it to error though: it can be used to return just the last return value of any function. If users bring up a use case, or, if it makes the implementation simpler to make it less restrictive, then we can come back to this proposal and consider expanding what is allowed.

Conclusion

This issue seems paralyzed based on details that won't be noticeable to the vast majority of code. A solution is to just pick the most restrictive version (have ... mean 1 or more as well if you like) and then make it less restrictive over time as needed.

andig commented 2 years ago

It would need to be clarified what ... means in context of named return values. Would it always imply the zero value? What if the named return had been assigned before?

gregwebs commented 2 years ago

... always implies zero values. If you want to use the named return values then you can name them. Named return values are relatively rarely used, so we shouldn't let them complicate this feature.

IMHO named return values are an anti-pattern most of the time because it creates unitialized (zero value) variables to keep track of: it is preferred to create the variable when you can assign a value. They have been very useful for me in functions that recover from a panic to ensure that the err error value is set. Arguably it is useful to use for err error if you are not going to assign every error variable a new variable name, but they rarely seem useful for success results.

andig commented 2 years ago

I would find this unexpected or at least unlearned when s would return the zero string:

func foo() (s string, err error) {
    s = "unexpected"
    return ..., err
}

It might make more sense to say that named return values maintain their value even with .... That would make it equivalent with

return

in that case.

vatine commented 2 years ago

But in this case, return ..., err is not equivalent to return s, err it is rather equivalent to return "", err and I would expect that to set the named return value.

gregwebs commented 2 years ago

@andig would you have this same expectation if naked returns were deprecated? If this proposal were accepted, the use case for naked returns would further diminish.

gregwebs commented 2 years ago

There is an interesting hack in the Golang code base to achieve return ..., err

In tls.go

func X509KeyPair(certPEMBlock, keyPEMBlock []byte) (Certificate, error) {
    fail := func(err error) (Certificate, error) { return Certificate{}, err }
...
            return fail(errors.New("tls: failed to find any PEM data in certificate input"))

I wouldn't have though this possible until very recently when I learned that Go can accept multiple return values from one function as arguments to another without intermediate destructuring.

gregwebs commented 2 years ago

@ianlancetaylor can we get this proposal for return ..., err submitted as a Github discussion or otherwise move it through the proposal process? I am happy to help in any way I can.

ianlancetaylor commented 2 years ago

Moving to main proposal process.

robpike commented 2 years ago

I favor the direction (mentioned in various ways above; this is not an original idea) that we instead provide a unified way to express the zero of any type. And for my money, the best way to do that is with a predefined identifier called "zero". It would overlap with some uses of "nil", and we'd need to decide what to do about that (I say, nothing), but on the other hand we can open a path to a less confusing model for nil.

In other words, rather than say, "return values with zeros are hard so let's fix them, we say "zero values should be easier to use so they work well everywhere, including in return statements".

andig commented 2 years ago

The obvious zero values seems to be _?

cespare commented 2 years ago

I think that some variant of the original proposal here is better than adding zero.

For one, zero only partially mitigates the annoyances of return sites. Yes, zero is easier to type and read than image.Rectangle{}, but it's longer than "" or 0 or nil. Additionally, it doesn't solve the problem that changing the function signature leads to a diff where all the returns change (now from e.g. return zero, err to return zero, zero, err). With return ..., err, adding another return parameter wouldn't change most err != nil return sites.

Furthermore, except when writing returns, I've rarely wanted for a general-purpose way to write the zero value. In most other contexts, the zero value is close to hand without needing to name it at all. For example:

var s string // I get the zero value without mentioning it
x := struct{X int, Y float64}{Y: 3.5} // I get the zero value for X by simply eliding it

Perhaps there are cases I'm not thinking of (maybe it's more common with generics?) but I just can't think of many places I'd use zero other than returns. And so if zero mainly improves return, rather than being a broadly useful tool, it seems better to go with a more comprehensive and targeted improvement to return instead.

atdiar commented 2 years ago

I would tend to agree with @robpike

The issue imho is overloading a symbol such as ... or _. Now a beginner might see the same symbol in different contexts and that will make it confusing.

Also, I don't find the overall idea that compelling from my own experience writing Go. And that's actually what makes it moot: at best, it just makes things easier to write by handwaving some information. We don't know if that might not lead to mistakes when changing function signatures. But more importantly, it makes the code less consistent with several types of returns: fully specified and elided. Recognizing immutable landmark idioms is easier.

On the topic of the zero identifier, that could help normalize things if writing a zero returning function is not enough (such as:

func Zero[T any] (v ...T) T{
   var z T
   return z
} 

)

gregwebs commented 2 years ago

Let's take a step back and clarify what problem we are solving. This proposal is trying to solve the issue of letting the programmer communicate that they are just returning an error. It can be used slightly more generally, but that is a less common use case.

The main issue is that Go forces us to write down zero values to return an error because we re-use a more general feature to return multiple return values. So the Go language is not optimized for this specific use case, which in some programs accounts for the majority of returns. That obscures readability because when reading code I need to recognize that a zero value is actually being returned at every return site.

As @cespare stated, having a generic zero value is not a problem that needs to be more solved more generally. But it also doesn't solve this specific proposal. I don't want an easier way to write down zero- I want to not have to write them at all.

However, let's look at a generic zero as a pragmatic solution of using _ as a generic zero:

If there is a choice between adding _ or even zero as a generic zero, or doing nothing at all, I would gladly take the generic zero. However, if we compare a generic zero to the existing proposed solution, the existing proposed solution cleanly solves all the above three points of reading, writing, and diffing.

If we are worried we are solving this issue too specifically, that is certainly a valid concern. If we look around at other languages (for example Rust) we will see that they have solved this problem more generally with tagged unions, a powerful feature to reduce defects by properly modeling state, and the missing feature of Go for anyone that has programmed with them. If this feature were added, it would be easy for end users to define a Result type just as Rust has and solve this issue on their own.

type Result[T any] enum {
  Ok T
  Err error
}

Writing return Err(err) is an optimal solution. The problem is that for the caller of that function, they have to de-structure the Result and deal with the error. Rust has added a special error return operator ?, but it isn't clear that this is an optimal approach. De-structing a tagged union with the existing Go switch statement would be fine for most tagged unions, but the end result for the common case of returning errors will be more verbose. What Golang does now with de-structing the return immediately works pretty well for the common case, and it is not clear how to achieve the same when returning a proper Result type.

Will Go be able to add tagged unions or a Result type soon? I hope so. In the next 5 years will Go do this and solve the issue of how to conveniently deal with the result in the calling function? I am doubtful. In the mean time, it would be great if we could add ... and dramatically improve the readability and diffing of the functions we are writing every day.

earthboundkid commented 2 years ago

Parts of #35966 are about adding zero as the predeclared identifier for the zero value of any type. (And all of #52307, which was closed as a dupe.) I still think it's a good idea.

earthboundkid commented 2 years ago

Will Go be able to add tagged unions or a Result type soon? I hope so. In the next 5 years will Go do this and solve the issue of how to conveniently deal with the result in the calling function? I am doubtful. In the mean time, it would be great if we could add ... and dramatically improve the readability and diffing of the functions we are writing every day.

If Go programmers move towards using result types instead of multiple returns, then it will be a shame that we added special syntax just for doing a thing that modern Go code won't do anymore.

gregwebs commented 2 years ago

If Go programmers move towards using result types instead of multiple returns, then it will be a shame that we added special syntax just for doing a thing that modern Go code won't do anymore.

I am an optimist, I think Go will figure out how to add tagged unions eventually now that they have Generics. But to be a realist or just a pragmatist, the existing error handling works pretty well other than what is pointed out in this proposal and it is a much bigger change to make it work with a result type- I don't think it is realistic to think that Go will ever make that change.

The impact of improving Zero values

For the few times that I have needed a generic zero, and several of the uses pointed out in those tickets, defining func Zero[T any] (v T) T and similarly IsZero in the standard library somewhere would suffice, and personally I am okay with just defining it in my own code. I point this out not because I am at all against adding zero. My point is that even though improving zero values seems like a larger issue, it's actual impact on lines of code is much smaller- for me personally I would probably be returning errors 100x more than the times then where zero would improve code readability (== zero could be used in place of == nil, but I don't think that improves readability). The proposal here addresses what exists in the majority of functions that I work with. So let's not have zero values take the driver seat since trying to use them as a solution to the issue at hand is sub-optimal with comparatively noisier lines and diffs.

zephyrtronium commented 2 years ago

An option would be to add zero for #35966 and change this proposal to return zero..., err. Going further, we could say that (to use the spec's EBNF) 'return' {PrimaryExpr ','} PrimaryExpr '...' {',' PrimaryExpr} expands to lexically duplicating the PrimaryExpr preceding ... to all output parameters not set positionally. So, something like this:

func() (string, int, float64, complex64, error) {
    return "", 1..., nil
}()

would return string(""), int(1), float64(1), complex64(1), error(nil). I don't think that would be especially useful outside of returning zero values, but it strikes me as a more orthogonal approach.

gregwebs commented 2 years ago

@zephyrtronium nice suggestion:return zero..., err would satisfy the use case of "I just want to return an error" equally well.

However, I don't think we want to blur the semantics of x...- it currently expects x to be an array.

It would kind of make sense to use both dot forms at once! return (...zero)..., err where ...zero used at the value level means an array of zero values just as ...param means an array of type param at the type level. return x... would work similar to the existing Go ... to expand the array, but also set the length. But I don't think constructing arrays with ...x is that useful otherwise, and anyways that's too many dots for my taste!

Another approach would be to pre-define an array of zeros as zeros, so we would have return zeros.... But again, I don't think zeros would be very useful outside of this use case.

gregwebs commented 1 year ago

I opened a proposal to not require ... and instead just allow return err when there are multiple return values. I think this new proposal makes sense and avoids bike-shedding about several things in this proposal. At least this new form will seem significantly better or worse to some, so it seemed worthy or a separate proposal. I don't care what solution we come up with as long as Go will let me just return an error :)