-
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: error handling via iterator-inspired handler functions #69734
Comments
The "ret" argument to the handle function is potentially quite powerful. What happens if the handle function passes "ret" to some other function, and that function calls "ret"? What happens if you send "ret" over a channel to some other goroutine, and that goroutine calls it? What happens if you store "ret" in a global variable, and then something else calls it later on? In general we need pin down rules for how "ret" can be used, while keeping in mind that Go is meant to be an orthogonal language. Also, what happens for something like a top-level var? What is the "ret" argument passed down? var Global = os.Open("file") ? handle If we aren't able to inline the handle function, then "ret" has to be implemented as some sort of panic. We have to recover at the point of the ?, and look for a special type holding the return values. If we see that type, we stop the panic and return. If we don't see that type, we have to silently continue the panic, since we don't want to disturb the stack trace. |
I think similar rules to
I'm not quite sure, but my initial thought is to just not allow it outside of contexts where |
Does this require a different handler function per combination of function / enclosing function return args? I don't see a clear way to annotate errors with this, there doesn't look like a way to pass extra args to the handler. I feel like it's only really suited to replacing |
Unfortunately, quite likely it would for a lot of cases, although some could be covered by a generic function as shown in the example.
No, this is quite straightforward, actually, and again works the same way that iterators do: func handle(annotation string) func(ret func(error), v int, err error) int {
return func(ret func(error), v int, err error) int {
if err != nil {
ret(fmt.Errorf("%v: %w", annotation, err))
}
return v
}
}
func Example() error {
v := doSomething() ? handle("something failed")
// ...
} The problem with this, though, is that if you try to make it generic than the inference will fail as reverse inference from the usage doesn't work in Go.
Perhaps, although the existing error handling would very much still work just fine, so anything complicated where this didn't work could just not be done that way. Alternatively, the handler functions can be anonymous, so, to take the example from Errors are Values: var err error
write := func(buf []byte) {
if err != nil {
return
}
_, err = w.Write(buf)
}
write(p0[a:b])
write(p1[c:d])
write(p2[e:f])
// and so on
if err != nil {
return err
} could become this instead, which would allow for an early return: handle := func(ret func(error), n int, err error) {
if err != nil {
ret(err)
}
}
w.Write(p0[a:b]) ? handle
w.Write(p1[c:d]) ? handle
w.Write(p2[e:f]) ? handle That also removes the need for a top-level type with a method and all the other complications that go along with that as suggested next in the blog post. |
In the main example in the description, should the handle function return |
Yes, yes it should. I made a few tweaks as I was writing it and seem to have missed one. Thanks. |
Then, I throw a suggestion into the ring: Elefant-in-the-room question: It is possible to extend this proposal with a default handle, i.e. behaviour that the language specifies in case that there is nothing after the |
|
Yes, I did not thought of that. Agreed. |
Go Programming Experience
Experienced
Other Languages Experience
Elixir, JavaScript, Ruby, Kotlin, Dart, Python, C
Related Idea
Has this idea, or one like it, been proposed before?
This idea involves several ideas, including handler functions and guard clauses, but it goes about it quite differently.
Does this affect error handling?
Many previous error handling proposals had issues with return values other than errors and the semantics around how handler functions were called and scoped. This attempts to work around those problems by instead using a similar higher-order function approach to what range-over-func uses.
Is this about generics?
No.
Proposal
With the range-over-func, Go, for the first time, has functionality in which calling a function can directly cause another function to return. The compiler generates a function from the body of the loop, but that function has special semantics around returns which will cause the function containing the loop to return when a return is executed inside of the loop body.
Despite this, the functionality is not particularly useful for error handling, as it is not only unweildy, but it actually often pollutes the code more than the standard
if err != nil
does:Proposal
I propose adding a concept of guard functions which are invoked via an operator (
?
, perhaps, but I'm not so stuck on this. I also thought of||
.) after a function call. These functions would take as arguments the return types of the function as well as a special function created automatically by the compiler. This extra function argument would take as arguments the returns types of the function in which the guard clause was used and, when called, would cause the function to return with those arguments. The guard function would return the same types as those it was called with, minus the extra function.That explanation is a bit obtuse, so here's an example:
In the example, the
ret
argument tohandle()
is a special function created by the compiler. Whenhandle()
calls it, it causesExample()
to return with the values that it was given. The arguments after the first tohandle()
are the return values ofstrconv.ParseInt()
. The entiref() ? handle
expression returns the return value ofhandle()
as long asret
is never called.I believe that this approach solves a number of problems with previous proposals. For one thing, it fits cleanly with generics to allow for crafting custom handler functions that work across a range of types. It also isn't error-specific: Handler functions could be written with any custom conditions on calling
ret
that they want to.The syntax is not something that I'm stuck on, as mentioned previously. I think it should be operator-based just to avoid potential problems with new keywords, but the primary idea here is the special
ret
function, not the syntax. An alternative syntax could even be to put the guard function before the function being called, i.e.v := handle?strconv.ParseInt(str, 10, 0)
or something.Language Spec Changes
The section on function calls would be changed to mention guard clauses and guard functions, and these would then also be given their own section explaining their semantics.
Informal Change
Guard functions are regular functions designed to be called via a guard clause which are passed, along with other things, a special function that can be used to cause the calling function to return.
Is this change backward compatible?
Yes.
Orthogonality: How does this change interact or overlap with existing features?
I think it fits well with generics and the existing error handling mechanisms.
Would this change make Go easier or harder to learn, and why?
Harder. People got surprisingly confused over how the new iterator functions work, although they seem to have gotten used to them pretty quickly, but I expect that the reaction to this would be pretty similar due simply to its higher-order function based design.
Cost Description
Slightly more complexity in the language's syntax.
Changes to Go ToolChain
Anything that parses Go code would be affected.
Performance Costs
Likely a very small compile-time penalty, but probably essentially no run-time penalty with proper optimization.
Prototype
No response
The text was updated successfully, but these errors were encountered: