-
Notifications
You must be signed in to change notification settings - Fork 17.7k
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: add built-in result type (like Rust, OCaml) #19991
Comments
Language change proposals are not currently being considered during the proposal review process, as the Go 1.x language is frozen (and this is Go2, as you've noted). Just letting you know to not expect a decision on this anytime soon. |
final_value := doThing(a).
Then(func(resp) { doAnotherThing(b, resp.foo()) }).
Then(func(resp) { FinishUp(c, resp.bar()) })! I think this wouldn't be the right direction for Go. |
As someone said in the reddit thread: would definitely prefer proper sum types and generics rather than new special builtins. |
Perhaps I was unclear in the post: I would certainly prefer a result type be composed from sum types and generics. I was attempting to spec this in such a way that the addition of both (which I personally consider to be extremely unlikely) wouldn't be a blocker for adding this feature, and it could be added in such a way that, when available, this feature could switch to them (I even gave an example of what it would look like with a traditional generic syntax, and also linked to the Go sum type issue). |
I don't understand the connection between the result type and the goals. Your ideas about error propagation and combinators appear to work just as well with the current support for multiple result parameters. |
@ianlancetaylor can you give an example of how to define a combinator that works generically on the current result tuples? If it's possible I'd be curious to see it, but I don't think it is (per this post) |
@tarcieri That post is significantly different, in that |
The intent is not to couple
The post suggests: type Result<A> struct {
// fields
}
func (r Result<A>) Value() A {…}
func (r Result<A>) Error() error {…} ...so, to the contrary, that post specializes around Admittedly things like |
@tarcieri What's wrong with a struct? https://play.golang.org/p/mTqtaMbgIF |
@griesemer as is covered in the Error Handling in Go post, that struct is not generic. You would have to define one for every single combination of success and error types you ever wanted to use. |
@tarcieri Understood. But if that (non-genericity, or perhaps not having a sum type) is the problem here, than we should address those issues instead. Handling result types only is just adding more special cases. |
Whether or not Go has generics is orthogonal to whether a first-class result type is useful. It would make the implementation closer to something you implement yourself, but as covered in the proposal allowing the compiler to reason about it in a first class manner allows it e.g. to warn for unconsumed results. Having a single result type is also what makes the combinators in the proposal composable. |
@tarcieri Composition as you suggested would also be possible with a single result struct type. |
I don't understand why you wouldn't use an embedded or defined struct type. Why have specialized methods and syntax for checking errors? Go already has means of doing all of this. It seems like this is just adding features that don't define the Go language, they define Rust. It would be a mistake to implement such changes. |
To repeat myself again: Because having a generic result type requires... generics. Go does not have generics. Short of Go getting generics, it needs special-case support from the language. Perhaps you're suggesting something like this? type Result struct {
value interface{}
err error
} Yes, this "works"... at the cost of type safety. Now to consume any result we have to do a type assertion to make sure the That would be a major regression over what Go has now. For this feature to actually be useful, it needs to be type safe. Go's type system is not expressive enough to implement it in a type-safe manner without special-case language support. It would need generics at a minimum, and ideally sum types as well.
I covered as much in the original proposal: "I'll admit this approach comes with a bit of a learning curve, and as such can negatively impact the clarity of programs for people who are unfamiliar with combinator idioms. Though personally I love combinators for error handling, I can definitely see how culturally they may be a bad fit for Go." I feel like I have confirmed my suspicions and that a feature like this both isn't easily understood by Go developers and goes against the simplicity-oriented nature of the language. It's leveraging programming paradigms that, quite clearly, Go developers don't seem to understand or want, and in such case seems like a misfeature.
Result types aren't a Rust-specific feature. They're found in many functional languages (e.g. Haskell's |
Thank you for sharing your ideas, but I think the examples used above are unconvincing. To me, A is better than B: A
B
|
I don't think A is more readable. In fact, the actions aren't noticeable at all. Instead, the first glance reveals that a bunch of errors are being obtained and returned. If B were to be formatted so that the closure bodies were on new lines, that would've been the most readable format. Also, the last point seems a bit silly. If function call performance is so important, then by all means, go with a more traditional syntax. |
if err != nil {
return err
}
resp, err = doAnotherThing(b, resp.foo());
if err != nil {
return err
}
resp, err = FinishUp(c, resp.bar());
if err != nil {
return err
} |
One interesting observation from this thread: the original example I gave which people keep copying and pasting contained some errors (the first Though this particular class of error is the sort that would've been caught by the Go compiler, I think it's interesting to note that how with so much syntactic boilerplate, it becomes very easy to look past such errors when copying and pasting. |
This doesn't make the proposal better. It's an assumption that failing to return multiple values is a result of explicit error handling. You could have also made the same errors inside the functions, you just wouldn't have seen them due to their unnecessary encapsulation. |
I disagree, I think that is a strong point of this kind of proposal. If all a program is doing is returning the err and not processing it, then it is wasting cognitive overhead and code and making things less readable. Adding a feature like this would mean that (in projects that to choose to use it) code that deals with errors is actually doing something worth understanding. |
We will have to agree to disagree. The magic tokens in the proposal are easy to write, but difficult to understand. Just because we have made it shorter doesn't mean we've made it simpler. |
Making things less readable is subjective, so here's my opinion. All I see in this proposal is more complex and obscure code with magic functions and symbols (which are very easy to miss). And all they do is hide a very simple and easy to understand code in case A. For me, they don't add any value, don't shorten the code where it matters or simplify things. I don't see any value in treating them at a language level. The only problem that the proposal solves, that I could see clearly, is boilerplate in error handling. If that's the only reason, then it's not worth it to me. The argument about syntactic boilerplate is actually working against proposal. It's much more complex in that regard - all those magic symbols and brackets that are so easy to miss. Example A has boilerplate but it doesn't cause logic errors. In that context, there's nothing to gain from that proposal, again, making it not very useful. Let's leave Rust features to Rust. |
To clarify, I'm not wild about adding the err = foo()
if err != nil {
return err
} Even if that syntax is a keyword instead of a special symbol. It's my biggest complaint about the language (even bigger than Generics personally), and I think the littering of that pattern across the code makes it harder to read and noisy. I also would love to see something that enables the kind of chaining @tarcieri brings up, as I find it more readable in code. I think the complexity @creker alludes to is balanced by the better signal-to-noise ratio in the code. |
I don't fully understand how this proposal would achieve its stated goals.
In most well-written Go, this kind of error-handling boilerplate makes up a small fraction of code. It was a single-digit percentage of lines in my brief look at some Go codebases I consider to be well-written. Yes, it is sometimes appropriate, but often it's a sign that some redesign is in order. In particular, simply returning an error without adding any context whatsoever happens more often than it should today. It might be called an "anti-idiom". There's a discussion to be had around what, if anything, Go should or could do to discourage this anti-idiom, either in the language design, or in the libraries, or in the tooling, or purely socially, or in some combination of those. I would be equally interested to have that discussion whether or not this proposal is adopted. In fact, making that anti-idiom easier to express, as I believe is the aim of this proposal, might set up the wrong incentives. At the moment, this proposal is being treated largely as matter of taste. What would make it more compelling in my opinion would be evidence demonstrating that its adoption would reduce the total amount of bugs. A good first step might be converting a representative chunk of the Go corpus to demonstrate that some sorts of bugs are impossible or unlikely to be expressed in the new style — that x bugs per line in actual Go code in the wild would be fixed by using the new style. (It seems much harder to demonstrate that the new style doesn't offset any improvement by making other sorts of bugs more likely. There we might have to make do with abstract arguments about readability and complexity, like in the bad old days before the Go corpus rose to prominence.) With supporting evidence like that in hand, one could make a stronger case. |
I'd like to echo this sentiment. This if err := foo(x); err != nil {
return err
} should not be simplified, it should be discouraged, in favor of e.g. if err := foo(x); err != nil {
return errors.Wrapf(err, "fooing %s", x)
} |
Also, Rob Pike wrote an article about error handling as mentioned above. Whereas this approach seems to be "fixing" the problem it introduces another one: more code bloat with interfaces. |
I think it's important not to confuse "explicit error handling" with "verbose error handling". Go wants to force the user to consider error handling at every step rather than delegating it away. For each function you call that may throw an error, you need to decide in some what whether or not you want to handle the error, and how. Sometimes it means you ignore the error, sometimes it means you retry, often it means you just pass it up to the caller to deal with. Rob's article is great, and really should be a part of Effective Go 2, but it's a strategy that can only take you so far. Especially when dealing with heterogeneous callees, you have a lot of error handling to manage I don't think it's unreasonable to consider syntactic sugar or some other facility to help with error handling. I think it's important that it doesn't undermine the fundamentals of Go error handling. For instance, establishing a function-level error handler which handles all errors that occur would be bad; it means that we're allowing the programmer to do what exception handling typically does: move the consideration of errors from a statement level issue to a block- or function-level thing. That definitely is against the philosophy. |
@Billyh With regards to the "Error handling patterns in Go" article, there are other solutions: |
@egonelbre |
@urandom please show a realistic example then? Sure I can take a more complicated example:
I understand that these are not applicable to everywhere, but without a proper list of examples we want to improve there's no way to have a decent discussion. |
Disclaimer: I haven't used juju, nor have I read the code. It's just a 'production' product I know of the top of my head. I am reasonably sure that such type of error handling (where errors are checked in between independent operations) is prevalent in the go world, and I highly doubt there's anyone out there that hasn't stumbled into this. |
@urandom I agree. The main issue with discussing without real-world code is that people remember the "gist" of the problem, not the actual problem -- which often leads to over-simplified problem-statement. PS: I remembered one nice example in go. For example, from these real world examples we can see that there are several other things that need to be considered:
Not just the "happy" and "failure" path. I'm not saying that these cannot be solved, just that they need to be mapped out and discussed. |
@egonelbre here's another example from this week's Golang Weekly, in the article by Mario Zupan entitled, "Writing a Static Blog Generator in Go":
Note: I'm not implying any critique of Mario's code. In fact, I quite enjoyed his article. I'm not sure any programming language was designed with a primary goal of minimizing lines of code, but a) the ratio of meaningful code to boilerplate could be one (of many) valid measures of the quality of a programming language, and b) because so much of programming involves error handling, this pattern pervades Go code and therefore makes this particular case of excess boilerplate merit streamlining. If we can identify a good alternative, I believe it will be rapidly adopted and make Go even more enjoyable to read, write and maintain. |
Rebecca Skinner (@cercerilla) shared an excellent writeup of Go's error handling shortcomings along with an analysis of using monads as a solution in her slide deck Monadic Error Handling in Go. I particularly liked her conclusions at the end. Thanks to @davecheney for referring to Rebecca's deck in his article, Simplicity Debt Redux which enabled me to find it. (Thanks also to Dave for grounding my rose colored optimism for Go 2 with the grittier realities.) |
Every control flow control statement is important. The error-handling lines are critically important from the correctness point of view.
If someone considers error handling statements not meaningful then good luck with the coding and I hope to stay away from the results. |
To address one of the points covered in @davecheney's Simplicity Debt Redux (which I covered, but I think it bears repeating):
For something like this to become the "single" way errors are handled, it would have to be a breaking change done across the entire standard library and every "Go2" compatible project. I think that's unwise: the Python2/3 debacle shows how schisms like that can be damaging to language ecosystems. As mentioned in this proposal, if a result type could automatically coerce to the equivalent tuple form, you could have your cake and eat it too in terms of a hypothetical Go2 standard library adopting this approach across the board while still maintaining backwards compatibility with existing code. This would allow those who are interested to take advantage of it, but libraries which still wish to work on Go1 will just work out-of-the-box. Library authors could have their choice: write libraries that work on both Go1 and Go2 using the old style, or Go2-only using the monadic style. The "old way" and the "new way" of error handling could be compatible to the point users of the language wouldn't even have to think about it and could continue doing things the "old way" if they wanted. While this lacks a certain conceptual purity, I think that's much less important than allowing existing code to continue working unmodified and also allowing people to develop libraries that work with all versions of the language, not just the latest.
Them's the brakes: either leave the language frozen as-is, or evolve the language, adding incidental complexity and relegating previous ways of doing things to legacy warts. I really think those are the only two options as adding a new feature which replaces an old one, whether the old feature is deprecated-but-compatible or out the door in the form of a breaking change, is something I think users of the language will have to learn about regardless. I don't think it's possible to change the language but have newcomers avoid learning both the "old way" and "new way" of doing things, even if Go2 were hypothetically to adopt this outright. You'd still be left with a Go1 and Go2 schism, and newcomers will wonder what the differences are and will inevitably end up having to learn "Go1" anyway. I think backwards compatibility is helpful both for teaching the language and code compatibility: All existing materials teaching Go will continue to be valid, even if the syntax is outdated. There won't be a need to go through every bit of Go teaching material and invalidate the old syntax: teaching material could, at its leisure, add a notice that there's a new syntax. I understand "There Is More Than One Way To Do it" generally goes against the Go philosophy of simplicity and minimalism, but is the price that must be paid for adding new language features. New language features will, by their nature, obsolete older approaches. I'm certainly willing to admit that there might be a way of solving the same core problem in a way that's more natural for Gophers, though, and not such a jarring change from the existing approach. |
One more thing to consider: while Go has done an exemplary job of keeping the language easy-to-learn, that isn't the only obstacle involved in onboarding people to a language. I think it's safe to say there are a number of people who look at the verbosity of Go's error handling and are put off by it, some to the point they refuse to adopt the language. I think it's worth asking whether improvements to the language could attract people who are presently put off by it, and how this balances with making the language harder to learn. |
Doing something like monadic error handling goes against Go's philosophy of making you think about errors, however. Monadic error handling and Java-style exception handling are pretty close in semantics (though differnet in syntax). Go took a deliberately different philosophy of expecting the programmer to explicitly handle each error, rather than only adding error handling code when you think of it. In fact, the I feel that any attempts to address Go error handling should bear this in mind, and not make it easy to avoid thinking about errors. |
@alercah I pretty much have to beg to differ with everything you've just said...
Coming from Rust, I think Rust (or rather, the Rust compiler) actually makes me think about errors more than Go. Rust has a #[must_use] attribute on its The Rust type system enforces every error case is addressed in your code, and if not, it's a type error or, at best, a warning.
Let me break down why this isn't true: Error Propagation Strategy
Error Types
All-in-all I feel like Go is much closer to Rust than it is to Java when it comes to error handling: errors in Go and Rust are just values, they are not exceptions. You have to opt-in to propagation explicitly. You must convert errors of a different type to the one a given function returns, e.g. through wrapping. They both ultimately represent a success value / error pair, just using different type system features (tuples versus generic sum types). There are some exceptions where Rust does provide some abstractions that can be electively used on a crate-by-crate basis to do implicit error handling (or rather, explicit error conversion, you still have to manually propagate the error). For example the That's well outside of the scope of this proposal though, and involves several language features Go does not have working in tandem, so I don't think there's any sort of slippery slope where Go is "at risk" of supporting these types of implicit conversions, at least not until Go adds generics and traits/typeclasses. |
To toss in my two cents on this matter. I think this sort of functionality would be very useful for companies (such my own employer) where single applications talk to large numbers of subsidiary data sources and compose results in straight-forward fashion. Here's a representative data sample of some code flow we would have func generateUser(userID : string) (User, error) {
siteProperties, err := clients.GetSiteProperties()
if err != nil {
return nil, err
}
chatProperties, err := clients.GetChatProperties()
if err != nil {
return nil, err
}
followersProperties, err := clients.GetFollowersProperties()
if err != nil {
return nil, err
}
// ... (repeat X5)
return createUser(siteProperties, ChatProperties, followersProperties, ... /*other properties here */), nil
} I understand a lot of the pushback that Go is designed to force a user to think about errors at each point, but in codebases where the vast majority of functions return Moreover, the vast majority of this error handling logic is identical, and paradoxically the sheer amount of explicit error-handling in our codebases makes it hard to find code where the exceptional case is actually interesting because there's a bit of a 'needle in the haystack' phenomena at play. I can definitely see why this proposal in particular may not be the solution, but I do believe there needs to be some way of cutting down on this boilerplate. |
Some more idle thoughts: Rust's trailing
|
Related: "Draft Designs" for new error handling features: Feedback re the errors design: |
I like @alercah suggestion to solve jus this one annoying feature of go-lang that @LegoRemix is talking about, instead of creating separate return type. I'd just suggest to follow Rust's RFC even more to avoid guessing zero values and introduce So this: func generateUser(userID string) (*User, error) {
siteProperties, err := clients.GetSiteProperties()
if err != nil {
return nil, errors.Wrapf(err, "error generating user: %s", userID)
}
chatProperties, err := clients.GetChatProperties()
if err != nil {
return nil, errors.Wrapf(err, "error generating user: %s", userID)
}
followersProperties, err := clients.GetFollowersProperties()
if err != nil {
return nil, errors.Wrapf(err, "error generating user: %s", userID)
}
return createUser(siteProperties, ChatProperties, followersProperties), nil
} Becomes this DRY code: func generateUser(userID string) (*User, error) {
siteProperties := clients.GetSiteProperties()?
chatProperties := clients.GetChatProperties()?
followersProperties := clients.GetFollowersProperties()?
return createUser(siteProperties, ChatProperties, followersProperties), nil
} catch (err error) {
return nil, errors.Wrapf(err, "error generating user: %s", userID)
} And require that function that is using @bradfitz @peterbourgon @SamWhited Maybe there should be another issue for this? |
@sheerun Your |
It looks even better, for curious people this is how my code would look like with func generateUser(userID string) (*User, error) {
handle err { return nil, errors.Wrapf(err, "error generating user: %s", userID) }
siteProperties := check clients.GetSiteProperties()
chatProperties := check clients.GetChatProperties()
followersProperties := check clients.GetFollowersProperties()
return createUser(siteProperties, chatProperties, followersProperties), nil
} The only thing I'd change is to get rid of implicit func generateUser(userID string) (*User, error) {
handle err { return _, errors.Wrapf(err, "error generating user: %s", userID) }
siteProperties := check clients.GetSiteProperties()
chatProperties := check clients.GetChatProperties()
followersProperties := check clients.GetFollowersProperties()
return createUser(siteProperties, chatProperties, followersProperties), nil
} |
As the author of this proposal, I think it's worth noting that it is effectively invalidated by #15292 and work like https://go.googlesource.com/proposal/+/master/design/go2draft-contracts.md, as this proposal was written assuming generic programming facilities are not available. As such, it suggests new syntax to allow for type polymorphism for the special case of Since it looks like at least one of those is likely to wind up in Go 2, I'm wondering if this particular proposal should be closed, and if people are still interested in a result type as an alternative to (Note that I probably don't have time to do that work, but if someone else is interested in seeing this idea forward, go for it) |
@sheerun the place to file feedback & ideas on Go 2 error handling is this wiki page: and/or this comprehensive listing of Requirements to Consider for Go 2 Error Handling: |
This is a proposal to add a Result Type to Go. Result types typically contain either a returned value or an error, and could provide first-class encapsulation of the common
(value, err)
pattern ubiquitous throughout Go programs.My apologies if something like this has been submitted before, but hopefully this is a fairly comprehensive writeup of the idea.
Background
Some background on this idea can be found in the post Error Handling in Go, although where that post suggests the implementation leverage generics, I will propose that it doesn't have to, and that in fact result types could (with some care) be retrofitted to Go both without adding generics and without making any breaking changes to the language itself.
That said, I am self-applying the "Go 2" label not because this is a breaking change, but because I expect it will be controversial and, to some degree, going against the grain of the language.
The Rust Result type provides some precedent. A similar idea can be found in many functional languages, including Haskell's Either, OCaml's result, and Scala's Either. Rust manages errors quite similarly to Go: errors are just values, bubbling them up is handled at each call site as opposed to the spooky-action-at-a-distance of exceptions using non-local jumps, and some work may be needed to convert error types or wrap errors into error-chains.
Where Rust uses sum types (see Go 2 sum types proposal) and generics to implement result types, as a special case core language feature I think a Go result type doesn't need either, and can simply leverage special case compiler magic. This would involve special syntax and special AST nodes much like Go's collection types presently use.
Goals
I believe the addition of a Result Type to Go could have the following positive outcomes:
if err != nil { return nil, err }
"pattern" (or minor variations thereof) can be seen everywhere in Go programs. This boilerplate adds no value and only serves to make programs much longer.Syntax Examples
First a quick note: please don't let the idea get too mired in syntax. Syntax is a very easy thing to bikeshed, and I don't think any of these examples serve as the One True Syntax, which is why I'm giving several alternatives.
Instead I'd prefer people pay attention to the general "shape" of the problem, and only look at these examples to better understand the idea.
Result type signature
Simplest thing that works: just add "result" in front of the return value tuple:
More typical is a "generic" syntax, but this should probably be reserved for if/when Go actually adds generics (a result type feature could be adapted to leverage them if that ever happened):
When returning results, we'll need a syntax to wrap values or errors in a result type. This could just be a method invocation:
If we allow "result" to be shadowed here, it should avoid breaking any code that already uses "result".
Perhaps "Go 2" could add syntax sugar similar to Rust (although it would be a breaking change, I think?):
Propagating errors
Rust recently added a
?
operator for propagating errors (see Rust RFC 243). A similar syntax could enable replacingif err != nil { return _, err }
boilerplate with a shorthand syntax that bubbles the error up the stack.Here are some prospective examples. I have only done some cursory checking for syntactic ambiguity. Apologies if these are either ambiguous or breaking changes: I assume with a little work you can find a syntax for this which isn't at breaking change.
First, an example with present-day Go syntax:
Now with a new syntax that consumes a result and bubbles the error up the stack for you. Please keep in mind these examples are only for illustrative purposes:
NOTE: Rust previously supported the latter, but has generally moved away from it as it isn't chainable.
In all of my subsequent examples, I'll be using this syntax, but please note it's just an example, may be ambiguous or have other issues, and I'm certainly not married to it:
Backwards compatibility
The syntax proposals all use a
result
keyword for identifying the type. I believe (but am certainly not certain) that shadowing rules could be developed that would allow existing code using "result" for e.g. a variable name to continue to function as-is without issue.Ideally it should be possible to "upgrade" existing code to use result types in a completely seamless manner. To do this, we can allow results to be consumed as a 2-tuple, i.e. given:
It should be possible to consume it either as:
or:
That is to say, if the compiler sees an assignment from
result(T, E)
to(T, E)
, it should automatically coerce. This should allow functions to seamlessly switch to using result types.Combinators
Commonly error handling will be a lot more involved than
if err != nil { return _, err }
. This proposal would be woefully incomplete if that were the only case it helped with.Result types are known for being something of a swiss knife of error handling in functional languages due to the "combinators" they support. Really these combinators are just a set of methods which allow us to transform and selectively behave based on a result type, typically in "combination" with a closure.
Then()
: chain together function calls that return the same result typeLet's say we had some code that looks like this:
With a result type, we can create a function that takes a closure as a parameter and only calls the closure if the result was successful, otherwise short circuiting and returning itself it it represents an error. We'll call this function
Then
(it's described this way in the Error Handling in Go) blog post, and known asand_then
in Rust). With a function like this, we can rewrite the above example as something like:or using one of the proposed syntaxes from above (I'll pick
!
as the magic operator):This reduces the 12 lines of code in our original example down to three, and leaves us with the final value we're actually after and the result type itself gone from the picture. We never even had to give the result type a name in this case.
Now granted, the closure syntax in that case feels a little unwieldy/JavaScript-ish. It could probably benefit from a more lightweight closure syntax. I'd personally love something like this:
...but something like that probably deserves a separate proposal.
Map()
andMapErr()
: convert between success and error valuesOften times when doing the
if err != nil { return nil, err }
dance you'll want to actually do some handling of the error or transform it to a different type. Something like this:In this case, we can accomplish the same thing using
MapErr()
(I'll again use!
syntax to return the error):Map
does the same thing, just transforming the success value instead of the error.And more!
There are many more combinators than the ones I have shown here, but I believe these are the most interesting. For a better idea of what a fully-featured result type looks like, I'd suggest checking out Rust's:
https://doc.rust-lang.org/std/result/enum.Result.html
The text was updated successfully, but these errors were encountered: