Hacker News new | ask | show | jobs
by hutao 143 days ago
One 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/. 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-... 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)?

1 comments

That 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.

I'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

[2] https://softwareengineering.stackexchange.com/questions/8033...

Yes, afaiu at least, effects and resumable exceptions are nearly the same, and you can implement one with the other.
Pretty 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.
This 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.