Condition Handling in the Lisp Language Family (2001)
nhplace.com
nhplace.com
I'm surprised to read about this bit about Dylan for the first time; I missed that in past readings.
I also coalesced handling and restarting in TXR Lisp. There are no restarts, only exceptions. An exception can be handled (which is the terminology for intercepted without unwinding the stack) or caught (which is the terminology for intercepting and unwinding). The symbol restart is provided as the root of the hierarchy for restarts; the reference manual encourages the practice of deriving restart-like exceptions from this symbol.
Sounds very cool, and the article seems to edify wrt functionality and abstract benefit, but how does it really help? See: the more-or-less complete abandonment of resumable conditions in other languages.
The practical differences between conditions and c++-style exceptions seem to be:
1. The stack is followed back, looking for a handler, rather than being destructively unwound- 'restarts' don't necessarily 'try again', they can continue from where the condition was thrown.
2. While unhandled (ERROR) 'fatal conditions' land in the debugger, nonfatal (SIGNAL)s imply that a sensible default has already been provided, and, relying on point 1, continue otherwise unfettered.
I remain curious about the kinds of practical architectural impact this functionality provides- how often does everyday lisp code signal nonfatal conditions? The hyperspec mentions that *break-on-signals* can be used for visibility there...
It's through these condition handler objects that the caller can bidirectionally communicate with callees before they return.
In other words, it's like the visitor pattern, but rather than passing the visitor directly you do so through a thread-local variable, and also these visitors are designed to be composable.
A more modern version of this, straight from the ivory tower, are effects handlers, which I expect we'll see in more languages as time goes on. The cool thing about effects handlers is that they make it easier to reason about all these nonlocal effects due to some well-thought-out restrictions. They also have some good compositionality properties, and they appear to be able to be compiled down to code that doesn't need special runtime support (beyond what functional programming languages already provide). The example I'm somewhat familiar with is Koka: https://koka-lang.github.io/koka/doc/book.html#sec-handlers
From my time programming on Lisp machines (long time ago), I fondly remember wrapper functions (i.e. some macro wrapped around your own code) like "with-open-file" which would open a file and set up proper condition handling to close it on exit, regardless of what happened inside... you could always be sure that the file was closed when control left the wrapper, either normally or abnormally. And if you where creating a file and some exception occurred, the file would be removed and the file system left unaltered (as much as possible).
Thus instead of (with-open-file (mystream …) …) You do { mystream { class_whose_destructor_closes_the_file (…) }; … }
Which is uglier but in the end largely equivalent, as C++ implements unwind-protect in every execution scope.
Some web request went badly. We're concerned about leaking internal details to the caller. So, you reply with a generic error code, log something about it for operator investigation, and make sure that any other state changes were rolled back along the way.
Condition systems invert that relationship. Its not just a failure to open a file, but a failure to open one specific file for a specific use case in the pipline out of several that are needed in total. In a condition system, the caller needs to set up a handler for each one of those use cases, and possibly bind some additional discriminating information to the arguments to disambiguate them on the way back up the stack.
If a reasonable fallback policy is available, I'd much rather partition the pipeline up such that the fallbacks are applied on a case-by-case basis in the control flow of the caller. Idiomatic Python code does this all the time with exceptions. Idiomatic code with Optional types does so, too. Both methods are well-suited to the task, and I have never found myself wishing for a condition system when using them.
https://phoe.github.io/slides/control-flow.pdf
enjoy
The problem is the management of such conditions is quite local. The power of the common-lisp approach is that the caller can set policies this way which makes the abstractions easier to write.
Not everybody thinks seeing things this way is a good idea. After all Lisp is a systems programming language that consists of a large number of ropes any one of which you can use to hang yourself. IMHO it remains the most powerful, programmer-time efficient and, frankly fun language I’ve ever had the pleasure of being paid to use.
C++, which I mostly use these days, similarly has a bunch of ropes-to-hang-yourself. Fewer, of course, as it isn’t as powerful, but even it seems to intimidate and enrage people.