-
Notifications
You must be signed in to change notification settings - Fork 17.6k
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: cleaner error handling happy path via limited pattern matching #65266
Comments
like |
It has some similarities unfortunately, yes. I'm one of those "disgusted" with // Very backwards incompatible, too.
use result, nil := something() else {
return ???
} My primary goal with the proposal was to try to come up with an alternate way of handling errors that isn't specific to |
The proposed syntax to this is very interesting. It is much cleaner, more useful, more predictable, and more controlled then the dreadful We like the pain of handling errors around here. |
As
|
The happy path is an interesting idea. Indenting the body of almost every function inside a The use of In general it's not clear that we gain much from the pattern matching. It does permit |
Go Programming Experience
Experienced
Other Languages Experience
JavaScript, Elixir, Kotlin, Dart, Ruby
Related Idea
Has this idea, or one like it, been proposed before?
I don't think so, but considering how many error handling proposals there have been over the years it is possible.
Does this affect error handling?
Yes. It differs in that it doesn't attempt to handle errors in a magical way, but instead introduces a new syntax that can be used for several different types of common data handling.
Is this about generics?
No.
Proposal
Prior Art
In Elixir, pattern matching is often used as a way to deal with errors. Functions that can return error values usually return a tuple of the form
{:error, error_string}
, and will usually return{:ok, result}
when they don't fail. This results in code that looks likeThat code checks to make sure that the first element is
:ok
and assigns the second element to a new variable,result
. If the pattern does not match, it will crash. When you want to handle the error, however, you generally use something like acase
expression, which allows checking multiple possible patterns:This is fine until you need to do a bunch of things in a row and several of them can fail:
To help deal with this, Elixir has a
with
expression which allows handling multiple pattern matches in a row. The following code is functionally identical to the previous snippet:Proposal
My proposal is to adopt a variant of the second syntax above,
with
, and make it a bit more Go-like, and use it to allow deduplication of repeated, nearly identical error handling. This would work by adding a new keyword, though what exactly I'm not sure. To illustrate, I'll usewith
to match the Elixir code, but it doesn't really need to be. Normally a new keyword would be a problem in terms of backwards compatibility, but it should be possible to tell from context if the keyword should be treated as such here. It would only be legal to use the keyword if it was immediately followed by a{
and was the first part of a statement. In all other cases, the keyword would be treated as an identifier. I don't know if this is too complicated in and of itself, but if so there may be alternative syntaxes for this that wouldn't cause such issues. I haven't thought of them, though.Inside of a
with
block and only inside of awith
block, very limited pattern matching would be possible. It would use a new assignment operator which I shall assume to be~=
for illustration purposes. Pattern matching would only work on direct results of functions, not on the internals of values, and would be simple==
checks.Here's an example:
The
else
block would be a list of cases looking similar to aswitch
or aselect
. Each case would be a comma-separated list of identifiers, with each being the same as a pattern match above. If any pattern match in thewith
block fails, the same values are run against the cases of theelse
block in top-to-bottom order. If one succeeds, it is run instead and then the whole block exits.Pattern matches would be very limited in terms of what they could do. Along with being able to check against literals and non-shadowed predefined values, such as
true
,nil
, etc., they could each be wrapped in what looks like a type conversion. If they are, they are constrained to that specific type. This is demonstrated in the error handling case above. If no cases in theelse
block match, the entire block panics. Or does nothing. I'm not sure which makes more sense.Comparison Example
As another example, here's a program that opens two files and copies the contents of one into the other. Here it is without
with
:And with
with
:Primary Concerns
I have three primary issues with my own proposal. I think both of them are solvable, but I'm not quite sure at the moment how to do so.
One is the specific definition for the rules surrounding variable creation. In the above examples, I just kind of assumed that
src
anddst
were being created by the~=
operator, but maybe that assumption doesn't make sense. Should it just work exactly like:=
in terms of variable creation, shadowing, etc? Or should it never create new variables? What are the rules surrounding what is a value on the left-hand side and what is not? Should it only be predeclared values, or should it be possible to match against values stored in variables themselves? Elixir uses the pin operator to declare that a variable should not be declared during a match, i.e.{^v, r} = something()
meaning thatr
is a new variable but^v
should just match against the value already stored inv
. Maybe something like that makes sense? I'm not sure, and I could probably be persuaded either way.Similarly, I'm worried about ways to differentiate between similar cases that should be handled separately. For example, what if I wanted to add custom error messages to the above example? If all I wanted to add it to was
io.Copy()
, it would be easy enough to just not use pattern matching for that call and handle it the old-fashioned way there. Another option would be to add a third value to indicate, something likeAnd finally, how should overlapping types be handled in
else
cases? For example, if I used the above code in the example before, the_, error(err)
case would have two different possible types for_
,*os.File
andint64
. Is that a problem? Maybe it could work similarly to #65031. That could get kind of messy, though.Conclusion
As you can probably tell from the concerns section, I'm not entirely sold on my own proposal, but I think it's a possibility for a completely different approach to cleaning up error handling code. It doesn't solve every problem people have with error handling, but I think that it leverages multiple returns as part of handling errors more than most proposals, many of which are trying to come up with ways to make functions with multiple returns including an error act as though they are not. That being said, even if this proposal is rejected, maybe it'll inspire a better one.
Language Spec Changes
Two main changes: Add a
with
block and add the~=
operator and associated rules.Informal Change
No response
Is this change backward compatible?
I think so, but it should be possible to create an alternative that is if it is not. The parts that are potentially no backwards compatible are basically just implementation details.
Orthogonality: How does this change interact or overlap with existing features?
It enables separation of repeated error handling from the happy path of the code.
Would this change make Go easier or harder to learn, and why?
Slightly harder, but I don't think that it's overly complicated. It adds a new type of control flow, but I think that the main complication would probably come from the rules surrounding how the pattern matching works.
Cost Description
Some extra complexity. Possible runtime cost, but very minimal if it exists at all. It's mostly just syntax sugar. Most possible cost could come from some potentially unnecessary type switches, but it might be possible to optimize those away at compile-time in most cases.
Changes to Go ToolChain
Everything that parses Go code would be affected.
Performance Costs
Likely minimal in both cases.
Prototype
No response
The text was updated successfully, but these errors were encountered: