5 ms·
I suggest you read up on Lisp conditions and restarts. They are not exceptions. There is no goto other than a function call and a loop back to the spot the prog
by gavinhoward 4y ago
I suggest you read up on Lisp conditions and restarts. They are not exceptions. There is no goto other than a function call and a loop back to the spot the programmer picks. And there is no comefrom.
The C equivalent would look like this:
do
{
err = some_operation_that_can_return_errs();
}
while (err != NULL && handleError(err) == ERROR_HANDLED);
if (err != NULL)
{
panic("Could not handle error: %s\n", err_str(err));
}
That is not goto or comefrom. Far from it. And of course, the panic at the end is not necessary; you could then return the error, or throw an exception, etc.
- Schroedingersat 4y agoI'm struggling to understand how your example is not just something any language with error types can do trivially. Every explanation of conditions and restarts I can find explicitly mentions searching the call stack for a handler and jumping into it, then reserving the capability to return to the function the error originated in. This is explicitly non-local and hidden control flow (ie. the much maligned goto of exceptions). The only advantage I can see asserted of having handlers (exceptions or conditions) that way is that you don't have to look for the error and pass it up manually but ! does that anyway so it doesn't seem worth throwing out your ability to reason about invariants in the context of a language with mutability and such. Then there's returning to the stack frame you started in (the part I somewhat unfairly called comefrom). You need to drag around all the machinery required for continuations in order to do this. If the goal is to have available some of the context of an outer stack frame, you could pass the relevant references down and just handle it where you wished to resume. Further thoughts: In some ways errdefer is the polar opposote of this concept. It allows the function to clean up and preserve invariants whilst passing control up the call chain.
- ghoward 4y ago> Every explanation of conditions and restarts I can find explicitly mentions searching the call stack for a handler and jumping into it, then reserving the capability to return to the function the error originated in. > This is explicitly non-local and hidden control flow (ie. the much maligned goto of exceptions). No, it's a function call. That's how you jump to code while reserving the capability to return. I mean, you explicitly "raise" an error, right? What's wrong with code like: raise AllocationFailure; desugaring to a function call? I thought function calls were local and non-hidden control flow, so why do you think otherwise? > The only advantage I can see asserted of having handlers (exceptions or conditions) that way is that you don't have to look for the error and pass it up manually but ! does that anyway so it doesn't seem worth throwing out your ability to reason about invariants in the context of a language with mutability and such. One example is that perhaps you do want to ignore certain errors, but let others through. I have code that returns an error on EOF. Usually. In reality, it calls an error handler and lets the handler decide. When I open and read files to the end, I'll encounter EOF, but I want to. So I'll push a handler onto the stack of error handlers that ignores EOF but handles all other errors. If I did that with exceptions, the call stack would be destroyed. Not good. Another example is memory allocation failure. Maybe one part of your program caches allocations and needs to be told to give them up if allocation failed. So in the handler for allocation failure, it calls a function to release those cached allocations, and when the handler returns, the allocator can try again. Hence, the restart. > You need to drag around all the machinery required for continuations in order to do this. If the goal is to have available some of the context of an outer stack frame, you could pass the relevant references down and just handle it where you wished to resume. No, you don't. If you did, I wouldn't have been able to implement it in C, but I did. [1] > Further thoughts: In some ways errdefer is the polar opposote of this concept. It allows the function to clean up and preserve invariants whilst passing control up the call chain. Passing control up the call chain on error should be a last resort because you're destroying context, but I agree, it should still exist. The cool thing is that you can do this in C too, even with cleaning up and preserving invariants. See [2], [3], and [4]. [1]: https://git.yzena.com/Yzena/Yc/src/branch/master/src/concurrency/strucon.c#L262-L325 https://git.yzena.com/Yzena/Yc/src/branch/master/src/concurr... [2]: https://git.yzena.com/Yzena/Yc/src/branch/master/src/alloc/stackpool.c#L238-L271 https://git.yzena.com/Yzena/Yc/src/branch/master/src/alloc/s... [3]: https://git.yzena.com/Yzena/Yc/src/branch/master/src/alloc/stackpool.c#L321-L360 https://git.yzena.com/Yzena/Yc/src/branch/master/src/alloc/s... [4]: https://git.yzena.com/Yzena/Yc/src/branch/master/src/alloc/stackpool.c#L790-L858 https://git.yzena.com/Yzena/Yc/src/branch/master/src/alloc/s...
- Schroedingersat 4y ago> raise AllocationFailure; This is exception semantics which is hidden control flow present in every language with exceptions. Even in checked exceptions it is often obfuscated (ie. a function call that can raise is not distinguishable from one that can't wkthout manually looking at the types, and if they are distinct, then you just have something equivalent to error types). The point is that, upon calling a function that might transitively call a function that raises, you have no idea if the next line will execute or whether you will wind up in some other piece of code altogether. It's not equivalent to calling a function because it destroys the stack context and violates the invariants and ability to reason about control flow for every function above it in the call stack. I am starting to grok algebraic effects, and how they can make exceptions-but-not-objectively-incorrect by preserving context during the interrupted control flow and returning everything how it was left, but they come at a price. You could guarantee that the stack will be reconstructed and execution will continue where it left off. But now 'raise' implicitly does a bunch of stuff including allocating memory on the heap and copying every stack frame between your error and your handler onto it. How does this function if your error is that you are unable to allocate? Even then, you don't know where control will end up without hunting for the registration of the handler, which may not even be near the call site. Sure you can build a stack on the heap in userland in C and dump everything there so as to reconstruct it (I skimmed your code and it seems like some implementation of coroutines or similar, I'm notnsure if this is part of the error machinery). There's nothing in zig that prevents you from doing this, or even using that exact library. > The cool thing is that you can do this in C too, even with cleaning up and preserving invariants. See [2], [3], and [4]. I either can't follow your code with the amount of effort I'm willing to expend (likely) or there isn't anything resembling the descriptions I can find for conditions and restarts there. Are you saying that the following can happen? I can have some function foo that is regular C code with the exception of registering that it handles some condition. Then foo can call, bar, which calls baz which calls qux, all of which are regular C code. Then qux can call something which raises a condition of the kind foo registered. Then control flow passes to foo which handles the error using the context in its current stack frame. Then control passes back into qux with information provided by foo's handling code. Then qux returns, followed by baz and bar returning without having to have any knowledge of or participation in your system. And that all of this happens without constructing a closure over foo's context and sneaking it into qux via a global.
- Schroedingersat 4y agoActually, come to think of it, I'm pretty sure this can be achieved with a combination of async/await and error types. If the inner function yields either a value (or none) or an error, the outer scope handles the error amd returns control to the async function (optionally with state gleaned from the handler) you have all the pieces. I believe yield lacks any kind of syntactic sugar (planned), but is possibly in current async functions. Viewing it through that lens, I can see it as a viable alternative to eschewing exceptions altogether.
- ghoward 4y agoAdding async to your error handling would just mean that you now have two problems.
- Schroedingersat 4y agoThat you said that seems to heavily imply that you already agree with the point that I'm trying to make about control flow. Here's my best attempt at demonstrating conditions and restarts with zig-native tools. Edit: fixed my dumb. https://zig.godbolt.org/z/creoT47Mj https://zig.godbolt.org/z/creoT47Mj Note that it'd get substantially more erganomic if yield were finished with all of the usual niceties of the monad-ish error keywords. Also edit: Upon further reflection to be actually correct conditions/restarts, you'd have to unwind starting from inside the inner function if the handler gave up. This is doable by passing some state back down (handled/unhandled) from the outer scope, and then just returning an error from the bottom scope to begin either unwinding or let a non-restartable error handling mechanism do its thing. Additionally I don't think this is compatible with also using async for its intended purpose without comptime shenanigans.
- ghoward 4y agoNo, I don't really agree. I don't like async because of something you accused conditions and restarts of: hidden control flow. In your example, I don't see any, which is great. But async can have hidden control flow. (I wonder why Andrew Kelley added it when he is so against even hidden function calls.) On top of that, I don't think async is a good paradigm, but that's neither here or there to the conversation.