Open DeedleFake opened 7 months ago
like try/catch
. Based on my observation, people are disgusted with try/catch
in the community
It has some similarities unfortunately, yes. I'm one of those "disgusted" with try
/catch
myself. I don't think this has some of the main problems that try
/catch
does, though, particularly that the only real way to handle multiple errors with try
/catch
is to wrap the entire thing around every single line, which winds up right back where if err != nil
was, but worse. I had an alternate that was per-line without the extra indentation, but I thought it was so close to if err != nil
that it didn't really warrant mentioning. It also was lacking a number of key parts without which it completely doesn't work. It looked something like
// 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 error
. I'm not sure I really succeeded, though, unfortunately. As I said, I'm not thrilled with how it came out, either, but I do think that pattern matching as a way to more succinctly check multiple returns has some merit as a mechanism to investigate.
The proposed syntax to this is very interesting. It is much cleaner, more useful, more predictable, and more controlled then the dreadful ~try/catch~ in my opinion. I wish every language which utilizes the try/catch uses a syntax like this instead. However, I think this additional syntax to golang goes against the simplicity we hold so dear here. With this proposal, and all the multi-error-handling proposals like it, we end up having 2 syntax choices doing the same thing ("should I use if-not-null chains or with block?")... with if-not-null chains being much more readable for someone who even knows nothing about go, whereas this is more complex (albeit, much cleaner than try/catch).
We like the pain of handling errors around here.
As with
might be too generic to associate it with error handling, why not go for something self-descriptive from what English has on offer?
fallible {
fool, err := bar(42)
} fallen {
case ...
}
The happy path is an interesting idea. Indenting the body of almost every function inside a with
statement seems less desirable.
The use of result, nil ~= f()
is very subtle. Here result
is meant to be a variable to be set, while nil
is meant to be a pattern. But nil
in Go is just an identifier, and nil := 0
is valid (if unusual) Go code. We can't treat nil
specially in a statement of this form. The scoping rules of the pattern matching variables also seem unclear.
In general it's not clear that we gain much from the pattern matching. It does permit val, true ~= f()
. But there isn't much call for that. Can we specialize this just for errors? Would we lose much?
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