4 ms·
Does anyone know of a great write up on exceptions vs union or either typed returns? I'm building a new language, somewhat similar to TypeScript in some ways,
by spankalee 9mo ago
Does anyone know of a great write up on exceptions vs union or either typed returns?
I'm building a new language, somewhat similar to TypeScript in some ways, and so far I have exceptions and try/catch expressions, but also Optional<T> and Result<T, E> types.
I'm familiar and used to exceptions, so I included them so at least near-fatal errors (ie, actually exceptional) could be caught at high levels in the stack. But I'm unsure if there's a strong argument that resonates with me yet that the language shouldn't have exceptions at all. Arguments that exceptions are untyped can be solved with things like checked exceptions, and I do find Go-style code to be quite verbose.
What's the best current reading on this?
- hutao 9mo agoOne of the most thorough articles on error handling in programming language design that I've read is this one: https://joeduffyblog.com/2016/02/07/the-error-model/ https://joeduffyblog.com/2016/02/07/the-error-model/. It was written by Joe Duffy, who worked on Microsoft's experimental Midori language. Another relevant article is Robert Nystrom's "What Color is Your Function?": https://journal.stuffwithstuff.com/2015/02/01/what-color-is-your-function/ https://journal.stuffwithstuff.com/2015/02/01/what-color-is-... This article is about async/await, but the same principles apply to error handling. This article uses colors as an analogy, but is really about monads. Both IO and exceptions can be denoted as a monad. What this means is that a function inside the programming language, A -> B, can actually be denoted by a mathematical function of the signature [[A]] -> M [[B]], for some monad M. For example, if we are dealing with the exception monad, M would be _ + Exception. A language such as Java implicitly executes in the IO + exception monad. However, the monad can also be exposed to the programmer as an ordinary data type, which is what Haskell does. When people talk about the tradeoff of exceptions versus Result<T, E>, or the tradeoff between preemptive concurrency and async/await, they are really talking about the tradeoff between making the monad implicit or explicit. (A language where all functions may throw is like one where all functions implicitly return Result<T, E>. A language where all functions may be preempted is like one where all functions are implicitly async, and all function calls are implicitly await points.) The theoretical technique of using monads to model the implicit effects of a programming language was pioneered by Eugenio Moggi, and the idea of making them explicit to the programmer was pioneered by Philip Wadler. Something else to think about is how monads stack. For example, how would you handle functions that are both async/await and throw exceptions? Does the answer change when the monad is implicit (e.g. throwing exceptions) or explicit (e.g. returning a result)?
- spankalee 9mo agoThat Midori article looks great, I'll give that a closer read. I actually used to work with Bob, and am familiar with the (wonderful!) function color article. I think my biggest question might be addressed in the Midori article: with things like bounds checks and checked casts you already have exceptions (or panics), so should you have a way to capture them anywhere on the stack? Are they recoverable in some programs? So should you have try/catch even if you try to make most errors return values? Another set of questions I have is around reified stacks. Once you have features like generators and async functions, and can switch stacks around, you're most of the way to resumable exceptions. I don't yet fully grok how code as the resume site is supposed to deal with a resume, but maybe resumable exceptions are a reason to keep them.
- hutao 9mo agoI'd never heard of "resumable exceptions" before, so I searched them up [1][2]. Is this another name for the language feature called "effect handlers" in OCaml 5? [1] https://osa1.net/posts/2024-11-04-resumable-exceptions.html https://osa1.net/posts/2024-11-04-resumable-exceptions.html [2] https://softwareengineering.stackexchange.com/questions/80336/do-resumable-exceptions-make-any-sense https://softwareengineering.stackexchange.com/questions/8033...
- spankalee 9mo agoYes, afaiu at least, effects and resumable exceptions are nearly the same, and you can implement one with the other.
- tome 9mo agoPretty much every language has a form of resumable exception known as a "function call". It's hard for me to understand why no one in the algebraic effects/effect handlers community has noticed this yet.
- hutao 9mo agoThis is the difference between functions and effect handlers, to my understanding: Functions map inputs to outputs, with a type signature that looks like A -> B. Functions may be composed, so if you have f: A -> B and g: B -> C, you have gf: A -> C. Function composition corresponds with how "ordinary" programming is done by nesting expressions, like g(f(x)). Sometimes, the function returns something like Option<B> or Future<B>. "Ordinary" function composition would expect the subsequent function's input type to be Future<B>, but frequently you need that input to have type B. Therefore, optionals or futures require "Kleisli composition," where given f: A -> Future<B> and g: B -> Future<C>, you have gf: A -> Future<C>. Kleisli composition corresponds with "monadic" programming, with "callback hell" or some syntactic sugar for it, like: let y = await f(x); g(y) Effect handlers allow you to express the latter, "monadic" code, in the former, "direct style" of ordinary function calls.
- thesz 9mo ago> But I'm unsure if there's a strong argument that resonates with me yet that the language shouldn't have exceptions at all. Result <E, T> is the type Either e t in Haskell. And Either is a Monad: https://hackage-content.haskell.org/package/base-4.22.0.0/docs/Data-Either.html https://hackage-content.haskell.org/package/base-4.22.0.0/do... This means you can have a computation inside Either e monad (notice missing result type) which can occasionally produce an exception, and these exceptions are checked (Either String cannot produce SNAFU-type exceptions, only textual descriptions of what was wrong). So, if you are developing your language, please consider embedding it in Haskell first. That would allow you to experiment with different type representations, at the very least: Result <e, t> of yours is a Result (e, t) in Haskell, which is very distinct from Result e t (Either e t).
- spankalee 9mo agoI'm targeting only WASM at the moment. I wish I could get into Haskell, but I just can't read it. I really can't parse what's going on with types like `Result e t (Either e t)`.
- hutao 9mo agoThe `Either a b` type in Haskell is equivalent to the `Result<T, Error>` type in other languages. The only difference is in the naming: "Result" semantically implies error handling, while "Either" implies a more general usage as the alternative between two options. Either and Result are the binary sum type (the disjoint union between two types). In Haskell, Either is defined as data Either a b = Left a | Right b Note that in Haskell, type parameters are lowercase. Source code: https://gitlab.haskell.org/ghc/ghc/-/blob/3f5e8d80b32063d265aeead6b62604c9ad4a34da/libraries/ghc-internal/src/GHC/Internal/Data/Either.hs#L127 https://gitlab.haskell.org/ghc/ghc/-/blob/3f5e8d80b32063d265... Contrast the definition of the Result type in Swift: public enum Result<Success: ~Copyable & ~Escapable, Failure: Error> { /// A success, storing a `Success` value. case success(Success) /// A failure, storing a `Failure` value. case failure(Failure) } Source code: https://github.com/swiftlang/swift/blob/256ff127c93d7e59a75f819372602eca0f97c788/stdlib/public/core/Result.swift https://github.com/swiftlang/swift/blob/256ff127c93d7e59a75f... I'm not sure what the parent commenter meant when they claimed that "Result <e, t> of yours is a Result (e, t) in Haskell." In Haskell, `(e, t)` would be the pair type (the binary product type).
- mamcx 9mo agoAlternatively look at https://dlang.org/articles/exception-safe.html https://dlang.org/articles/exception-safe.html. The problem of mixing paradigms is that get confusing. Ideally all is represented equally (ie: All errors are `Result`) but is the handling that get confusing. Each option is a totally different control flow. And it not compose (even if you use effects ) (and I mean ergonomically*) so you need to pick wich one to make first class P.D: I'm pretty certain about the "not compose, in practice", I have seen lots of options and none looks nice, but open to corrections! P.D.2: It should also consider the things on the dlang handling, and the midori article...
- antonvs 9mo agoBoth Rust and Go are good examples of modern languages with no exceptions. Not coincidentally, both provide a panic mechanism, which is intended for failures that are either unrecoverable or at least not locally recoverable. Both languages allow you to "catch" panics, but this mechanism is constrained and impractical to use for normal error handling. Instead, it's used to e.g. prevent a service from crashing if individual requests fail unrecoverably. The point is that these languages both demonstrate a simple design for exception-free languages. (Although Rust's approach is better in several ways, partly because it has a better type system.)