It's a pity that the section on exceptions didn't do a more detailed analysis on the criticism of exceptions; in my personal view I haven't liked exceptions, in a similar vein to how I'm not a huge fan of the implementation of POSIX signals or setjmp / longjmp.
Although I very much see the reason why it was developed, in effect it comes closer to a glorified form of "come from" (and people thought that goto was considered harmful).
Everybody chooses a favorite depending on their domain.
A function executes, and some error happens:
- Return error value: try to handle the error ASAP. The closer to the error the more detailed the information. Higher probability of recovery. Explicit error code handling throughout the code. Example: maybe you try again in one millisecond because the error is a very low probability but possible event.
- Exception: managing errors requires a high-level overview of the program state. Example: no space left on device, inform the user. You gather the detailed information where the error happened. The information is passed as-is or augmented with more information as it bubbles up the stack until someone decides to take action. Pros: separate error handling and happy path code, cleaner code. Cons: separate error handling and happy path code, unhandled errors.
Worst case scenario: you program in C. You don't have exceptions. You are forbidden to use setjmp because rules. A lot of errors are exposed directly to the programmer because this is a low-level language. You return error codes. Rules force you to handle every possible return code. Your code gets incorporated as an appendix to the Necronomicon.
I'm the opposite - I really like checked exceptions in Java because it's very easy to see how developers are handling errors and they also form part of the function signature.
Most functions will just pass on exceptions verbatim so it's better than error return values because with them the entire codebase has to be littered with error handling, compared to fewer try catch blocks.
setjmp, etc. are like unchecked exceptions, so I'm also not a fan, but I use this occasionally in C anyway.
Why would errors as return values have to propagate any farther in the codebase compared to errors as exceptions? If exceptions can be handled, so can the value based errors.
Exceptions done well should outperform return value checking. However it’s very difficult to make it perform well and for some reason people prefer -> Result<T, E> instead of -> T throws E which is basically the same thing.
The difference is the monadic capabilities of the result type. Thrown exceptions pepper codebases in ways that are even more unfortunate than monadic composition, which is already kind of iffy, but at least has generic transforms for error recoveries, turning errors to successes of a different type and so on. You end up with far less boilerplate.
I’m not so sure. The page goes over how it works in Scala 3 and it’s a little bit cleaner. But there is some nicety in handling return and exception uniformly in some cases.
As some readers may not be familiar with the name Xavier Leroy, I just want to emphasize that he is one of the people behind OCaml and a leading figure promoting Rocq/Coq.
Does implementing algebraic effects requires stack switching support? If so, I wonder what runtime cost we must pay when heavily using algebraic effects. Is there any empirical study on the performance of algebraic effects implementations?
In OCaml 5, we’ve made it quite fast: https://kcsrk.info/papers/drafts/retro-concurrency.pdf. For us, the goal is to implement concurrent programming, for which a stack switching implementation works well. If you use OCaml effect handlers to implement state effect, it is going to be slower than using mutable state directly. And that’s fine. We’re not aiming to simulate all effects using effect handlers, only non-local control flow primitives like concurrency, generators, etc.
Suppose one of your effects is `read()`, and you want to be able to drop in an asynchronous implementation. Then you'll either require something equivalent to stack switching or you'll require one of the restrictions to asynchronicity allowing you to get away without stack shenanigans -- practical algorithms usually start requiring stack switching though.
You can get a lot of mileage out of algebraic effects without allowing such ideas though. Language constructs like allocation, logging, prng, database sessions, authorization, deteterministic multithreaded prng, etc, are all fairly naturally described as functions+data you would like to have in scope (runtime scope -- foo() called bar() -- as opposed to lexicographic scope), potentially refining or changing them for child scopes. That's a weaker effect system than you would get with the normal AE languages, but there are enough concepts you feasibly might want to build on such a system that it's still potentially worthwhile.
It's a pity that the section on exceptions didn't do a more detailed analysis on the criticism of exceptions; in my personal view I haven't liked exceptions, in a similar vein to how I'm not a huge fan of the implementation of POSIX signals or setjmp / longjmp.
Although I very much see the reason why it was developed, in effect it comes closer to a glorified form of "come from" (and people thought that goto was considered harmful).
Everybody chooses a favorite depending on their domain.
A function executes, and some error happens:
- Return error value: try to handle the error ASAP. The closer to the error the more detailed the information. Higher probability of recovery. Explicit error code handling throughout the code. Example: maybe you try again in one millisecond because the error is a very low probability but possible event.
- Exception: managing errors requires a high-level overview of the program state. Example: no space left on device, inform the user. You gather the detailed information where the error happened. The information is passed as-is or augmented with more information as it bubbles up the stack until someone decides to take action. Pros: separate error handling and happy path code, cleaner code. Cons: separate error handling and happy path code, unhandled errors.
Worst case scenario: you program in C. You don't have exceptions. You are forbidden to use setjmp because rules. A lot of errors are exposed directly to the programmer because this is a low-level language. You return error codes. Rules force you to handle every possible return code. Your code gets incorporated as an appendix to the Necronomicon.
I'm the opposite - I really like checked exceptions in Java because it's very easy to see how developers are handling errors and they also form part of the function signature.
Most functions will just pass on exceptions verbatim so it's better than error return values because with them the entire codebase has to be littered with error handling, compared to fewer try catch blocks.
setjmp, etc. are like unchecked exceptions, so I'm also not a fan, but I use this occasionally in C anyway.
Why would errors as return values have to propagate any farther in the codebase compared to errors as exceptions? If exceptions can be handled, so can the value based errors.
I think the sweet spot is to use exceptions for bugs. If the error is expected, make it data.
Exceptions done well should outperform return value checking. However it’s very difficult to make it perform well and for some reason people prefer -> Result<T, E> instead of -> T throws E which is basically the same thing.
The difference is the monadic capabilities of the result type. Thrown exceptions pepper codebases in ways that are even more unfortunate than monadic composition, which is already kind of iffy, but at least has generic transforms for error recoveries, turning errors to successes of a different type and so on. You end up with far less boilerplate.
I’m not so sure. The page goes over how it works in Scala 3 and it’s a little bit cleaner. But there is some nicety in handling return and exception uniformly in some cases.
How do you feel about algebraic effects?
Although INTERCAL's "come from" provided the opportunity to implement a gloriously concise multi-threading syntax/mechanism [1]!
[1] https://esolangs.org/wiki/Threaded_INTERCAL
As some readers may not be familiar with the name Xavier Leroy, I just want to emphasize that he is one of the people behind OCaml and a leading figure promoting Rocq/Coq.
That, and the main author of CompCert [1], as well as (apparently) the author of the original threading support in the Linux kernel [2].
[1]: https://en.wikipedia.org/wiki/CompCert
[2]: https://en.wikipedia.org/wiki/Xavier_Leroy
For French speaking people, Mr. Leroy gave a complete lecture at Collège de France. Available on Youtube.
https://www.youtube.com/watch?v=ck9DjekcK4M Looks like there are 14 videos.
Does implementing algebraic effects requires stack switching support? If so, I wonder what runtime cost we must pay when heavily using algebraic effects. Is there any empirical study on the performance of algebraic effects implementations?
Not necessarily. You can implement them using a monad. See https://dl.acm.org/doi/10.1145/2804302.2804319. That said, in GHC Haskell the implementation on top of stack switching seems to outperform other implementation strategies. See https://github.com/ghc-proposals/ghc-proposals/blob/master/p....
In OCaml 5, we’ve made it quite fast: https://kcsrk.info/papers/drafts/retro-concurrency.pdf. For us, the goal is to implement concurrent programming, for which a stack switching implementation works well. If you use OCaml effect handlers to implement state effect, it is going to be slower than using mutable state directly. And that’s fine. We’re not aiming to simulate all effects using effect handlers, only non-local control flow primitives like concurrency, generators, etc.
Kind of.
Suppose one of your effects is `read()`, and you want to be able to drop in an asynchronous implementation. Then you'll either require something equivalent to stack switching or you'll require one of the restrictions to asynchronicity allowing you to get away without stack shenanigans -- practical algorithms usually start requiring stack switching though.
You can get a lot of mileage out of algebraic effects without allowing such ideas though. Language constructs like allocation, logging, prng, database sessions, authorization, deteterministic multithreaded prng, etc, are all fairly naturally described as functions+data you would like to have in scope (runtime scope -- foo() called bar() -- as opposed to lexicographic scope), potentially refining or changing them for child scopes. That's a weaker effect system than you would get with the normal AE languages, but there are enough concepts you feasibly might want to build on such a system that it's still potentially worthwhile.