Debugging Lisp Part 4: Restarts
malisper.me
malisper.me
A few lengthy discussions with some lispers who were more well-versed in CL's error handling than what would be the equivalent of try/catch/throw completely changed my view on lisp's abilities. The reasoning behind using handler-bind instead of handler-case just blew my mind once I understood it. It was a hard concept to grasp, but I'm so glad those guys pounded it in.
I reprogrammed the error handling in cl-async/blackbird/wookie/etc to use handler-bind/restarts and now everything is so much clearer. You get full stacktraces when in debug mode, but also have the option to pass errors around as objects if in production.
CL's error handling definitely was a "wow" moment for me once I got it.
Restarts are a very nice way of handling known error conditions and I wish more languages had stolen this feature from Lisp before macros or garbage collectors and things.
> I wish more languages had stolen this feature from Lisp
The terminology and some of the semantics of CL's condition may have been derived derived from concepts in IBM's PL/I programming language!
Similarities:
* PL/I's xceptions are called "conditions" in PL/I.
* Conditions are "signaled" (by a construct called SIGNAL).
* The "on unit" construct (ON statement) resembles condition handling and restarts (functionality of handler-bind and restart-bind rolled into one). On units are dynamically scoped, and when a condition is raised they are searched without discarding the stack ("the whole stack is there", in your words). On units can return to the source of the signal, or execute a GO to the surrounding procedure, which then triggers unwinding: exactly like a (go ..) in Lisp jumping out of a restart lambda.
* Conditions in PL/I are used not only for errors but for giving advice to low level code on what to do. For instance the printing system signals an "endpage" condition, and the higher level code can provide an on unit to do what it wants at the end of a page. This on unit returns to the lower level code.
I've never written code in PL/I, but I studied the condition system.
Greenberg was involved in the design of the "New Error System" at Symbolics, which in turn influenced the condition system adopted by ANSI Common Lisp.
And per the referenced citation (http://www.nhplace.com/kent/CL/Revision-18.txt):
Many aspects of the NES were inspired by the (PL/1) condition system used by the Honeywell Multics operating system.
This isn't the first time this has been pointed out to me. Someone had pointed me to Kent Pitmans'[0] paper on Common Lisp's condition handling system. It is quite a good read.
As I wasn't even born when Multics and PL/1 were released and was never old enough when they were around to have used them I do lack that context.
To me, having started using CL around 2008, it is an essential "feature" of the Lisp experience. One that no other language I'm aware of implements and few, if any, operating systems implement today.
Unix signals perhaps could emulate it?
So... my apologies if my ignorance characterizes Lisp incorrectly but I still wish this style of error handling was more common. It is just so damn useful.
[0] http://www.nhplace.com/kent/Papers/Condition-Handling-2001.h...
The thing is, though, is that restarts are relatively easy to add to any language that has well-supported structures for non-local control flow; they're just a protocol, after all. That's really what Common Lisp has which most other languages lack. Common Lisp (very intelligently, IMHO) did not conflate error handling with non-local control flow; they are different concepts and deserve to be treated independently.
Specifically, the lexcal scoping of BLOCK and TAGBODY labels and the fact that RETURN-FROM and GO will unwind the stack if necessary to get to the target label make creating something like HANDLER-BIND/RESTART-BIND relatively easy. (There is also of course THROW and CATCH [which don't really have anything to do with error handling in CL, they're just structures for dynamically-scoped stack unwinding], but I've generally found the need to create explicit labels for those less elegant than using lexical scoping. You can always use a closure if you need to store the lexicaly-bound state to be used somewhere else, and the reverse is not as easy...)
You obviously also need UNWIND-PROTECT to keep things safe when the stack unwinds, but most languages successfully manage to get that with something like try-finally (or RAII in C++'s case, even if I find it clumsy sometimes).
What I was attempting to convey is that the reason I think the CL condition system works so well is that at its core it doesn't attempt to manage control flow directly; i.e. neither HANDLER-BIND nor RESTART-BIND unwind the stack on their own, and even ERROR only gets its behavior by falling back to INVOKE-DEBUGGER if unhandled. Because CL has good tools for doing non-local control flow that aren't bound up in its condition/exception system it winds up strictly more expressive (IMHO) than languages that conflate these.
What I've noticed from languages that have traditional try/catch exception handling (especially when that's the only non-local control flow available) is that the mechanism of error handling is seen as DEEP MAGIC by their users. If you separate it out into non-local control flow and a protocol for communicating exception handlers as CL does, the "magical" properties go away since it can be seen how the condition system could be implemented.
Or at least that's how it worked for me.