Structurally-Typed Condition Handling
journal.infinitenegativeutility.com
journal.infinitenegativeutility.com
In an exception system, when you signal a condition (“throw”) the stack is unwound until the exception is caught or the stack exhausted.
In a general condition system the stack is searched for a handler; that handler is free to return (at which point the stack is unwound, and then the handler exits, same as catching an exception) or continue (e.g. free some disk space and then have the write operation retry, or return infinity from a divide by zero error).
If you want to play with "structurally typed condition handling", then the Koka language has "row-typed algebraic effect handlers" that compile to C: <http://koka-lang.org>
It's the name given to a specific idea being (re)explored in programming languages. Basically it's a try-catch except when you catch the error, you can do something, then return to where the exception was thrown and continue from there.
The second half of the article is similar to the idea of Algebraic Effects.
Here's another article explaining Algebraic Effects: https://overreacted.io/algebraic-effects-for-the-rest-of-us/
In high-school algebra, the variables 'x', 'y', etc. are mostly assumed to stand for numbers; but we still solve problems by chaining-together some basic 'things we can do'. For example, if we're given 'a + b = 2 × b' and we want to find an expression for 'a', we can do the following:
a + b = 2 × b (given)
(a + b) - b = (2 × b) - b (subtracting from both sides preserves equality)
a + (b - b) = (2 × b) - b (re-group + and -)
a + 0 = (2 × b) - b (replace 'x - x' with '0')
a = (2 × b) - b (replace 'x + 0' with 'x')
a = (b + b) - b ('2 × x' with 'x + x')
a = b + (b - b) (regroup again)
a = b + 0 (another 'x - x')
a = b (another 'x + 0')
Note that the above doesn't actually rely on 'a' and 'b' being numbers, or +/-/× being numeric addition/subtraction/multiplication; they can be anything, as long as they satisfy 'x - x = 0', 'x + 0 = x', etc."Interfaces", as found in Java, PHP, StandardML, etc. are algebras: as long as our value implements some interface Foo, we don't care what the implementation actually is. In fact, OOP was originally described in terms of "mini algebras" (what we would now call a "public interface", or "API").
The idea behind Algebraic Effects is that side-effects, like printing out strings, or sending network requests, can be treated in terms of 'what they do' (like an interface), rather than having to care about the implementation. A good example is code which has an input-reading effect: to run it, we must provide some implementation of 'input reading'; but the code will work the same regardless of whether we implement that effect by e.g. reading from the process's stdin handle, or from a GUI text box, or from a hard-coded string, or whatever.
So to evaluate a expression print("a") we pause evaluation, do the printing, and then resume it by returning () (or whatever value that print returns). So a language that has algebraic effects (or, equivalently, Common Lisp condition system) will be able to implement expressions that print to the terminal.
https://gitlab.com/xmdr/ananke/-/blob/master/drafts/notes/co...
Another issue is just in performance. In an error or try/catch system you unwind the stack along the way. In a condition system, you don't. If you insisted on manually passing them up until you find a handler (or not, in which case it may become an effective noop), you have to touch all of that "is it a condition or result" logic all the way up, and then resume all the way back down, and then return to the top of a loop because you could get another condition. So you'd end up writing something more like this:
do {
result = call(...);
if is_condition(result) {
// possibly handle it or re-yield
switch(result) {
log: ...
other_condition: ...
default: yield result;
}
}
} while(is_condition(result));
It should work, but it seems like it would be unpleasant to work with. With a condition system, you may implement it in a way that forces this kind of costly search through the call stack, or you could register the handlers and avoid this costly backtrack and resume operation.This isn’t terrible, 2 relatively popular languages, Rust and Go, do not have exceptions and do require the caller explicitly handle errors. Rust even has a question mark operator to make error handling this way a bit more ergonomic.
> Another issue is just in performance. In an error or try/catch system you unwind the stack along the way. In a condition system, you don't.
That makes sense. I’m curious if this is something that can be optimized statically by a compiler.
One thing that exceptions don't interact with very well is concurrency. For example, the Alice ML programming language[1] had a nice approach to concurrency using futures where you could just insert a "spawn" keyword in front of any expression to make it run in a background thread, returning a (transparent) future for the eventual result. This was pretty neat, but it fell apart somewhat around the use of exceptions. That is, given code like the following:
let x = some_computation()
handle DivByZero() => ...
you can't just simply put a "spawn" in front of some_computation() because now any exception raised will happen in the background thread, which doesn't have the exception handler on the stack. (At least, that's how I remember Alice ML working, and that's how it works in most languages with exceptions).My feeling at the time was that rather than exception handling walking up the stack to find a handler, the try/catch (or handle) statement should instead create a closure at the point it is executed that is then passed down as part of the dynamic environment to subsequent calls. The raise/throw statement would then find a matching closure from the dynamic environment and immediately call it at the point the exception is raised. This makes exception/condition handlers into normal functions, and also means that this dynamic environment can be copied to background threads when they are spawned.
In this approach, restarts can be handled via normal datatypes. That is, the code that was raising an error would do something like this (pseudocode):
type restart = Skip | Retry | Abort
if malformed(record):
let restart = signal MalformedRecord(record)
match (restart):
on Skip -> /* do nothing */
on Retry -> parse(record)
on Abort -> return
end
end
and callers would then do something like: try parse(stuff)
on MalformedRecord(record) -> Skip
end
The language I was designing at the time was agent-based and a message sent to an agent would start a new transaction. There was a special "fail" statement that could be used to completely abort the current transaction and signal a failure back to the caller, providing an escape hatch when you really did want to unwind the whole stack (and cancel any spawned background tasks).Anyway, this was fun to think about again. There's so much more design space to explore around error handling IMO.
Edit: I should add that this is pretty much how explicit promises work in most languages that use them, where error handlers are attached to a promise object that is passed down to asynchronous tasks. So in a sense my sketch here is just another form of syntactic sugar over that existing pattern.
[1]: https://en.wikipedia.org/wiki/Alice_(programming_language)