Show HN: My book, The Common Lisp Condition System
amazon.com
amazon.com
The Common Lisp Condition System is my first book and it was previously discussed on Hacker News[0] as soon as the Apress page for the book was first posted.
The HN discussion was very fruitful and insightful and prompted me to add more content about the condition system in general. Due to time constraints and the flow of working on the book, it was impossible to add this new stuff to the actual body of the book, so me and Apress have decided to publish this content as an appendix named Discussing the Common Lisp Condition System and release it[1] on the Internet. The appendix is free to download and use in any way and I'd like to once again thank everyone who participated in the original thread.
The book is currently available for purchase on Apress[2] (with chapter samplers) and Amazon[3].
The full source code is available on GitHub[4] and should be buildable under any conforming C compilers and any conforming Common Lisp implementations. I'm available for answering any questions about it and merging any PRs that might arrive.
I hope that the book is helpful in general and wish everyone who decides to try it a good read.
AMA.
[0] https://news.ycombinator.com/item?id=23843525
[1] https://github.com/Apress/common-lisp-condition-system/blob/...
[2] https://www.apress.com/us/book/9781484261330
[3] https://www.amazon.com/Common-Lisp-Condition-System-Mechanis...
(I guess that I can be proud of myself though: the first edition of the book wasn't even out of production and I already had some stuff written for the second edition.)
> Another issue is preserving the state of the program stack, as it is destroyed in the process of dumping the Lisp image, and it cannot be fully restored when the image is thawed. However, portability libraries such as Dissect[0] allow for saving the stack state as normal Lisp data that can then be inspected using the system inspector.
[0] https://shinmera.github.io/dissect/
Still, you can fully preserve the full stack information as Lisp data along with the whole heap, dump the image, and then inspect the dumped core as if you inspected any other Lisp image. This is already a big quality improvement over digging in dead crash dumps or - worse - textual, printed stacktraces with no other information available.
Therefore, I think that the statement "you can't use saved images to debug errors in SBCL" is somewhat far-fetched, really.
Macros, for example, are a natural fit for Lisp because of the parentheses. It would be difficult to add Lisp-style macros to a language like Python because Python doesn't have Lisp parentheses. In contrast, there's nothing about multiple namespaces that is particularly tied to Lisp. Common Lisp and Emacs Lisp have multiple namespaces, but Scheme doesn't. Python doesn't have them, but it just as easily could.
So is the condition system more like macros or more like multiple namespaces?
It is possible to implement a condition system on top of any language that has dynamic variables, a `finally`-style construct and some mechanism for unwinding the stack. Since dynamic variables are implementable on top of lexical variables and `finally`, it's basically just about unwinding the stack and `finally`.
The main issue is how a condition system would fit with any existing system which likely works by immediately unwinding the stack rather than allowing to wind it further; that's the case e.g. in Java or C++.
In its nature, a condition system is simply a means of executing code that has been provided dynamically, including transfers of control. I think that a closer term would be algebraic effects, which seem to be an equivalent of a condition system in a strictly typed strictly functional world.
I found this old thread talking a bit about the lisp condition system [1] which also mentions implementing it in a typed manner.
I used Lisp for a while myself as my favourite language (the condition system was part of the reason) but the problem I ended up switching to Haskell because it has much of the power macros provide coupled with a very strong type system. That's why the potential occurred to me that CPS can probably implement something effectively equivalent condition system.
To expand on their comment: if `throw` in Java or C++ is similar to `error` in CL (or `throw` in some special cases), a `catch` clause in Java or C++ is equivalent to a label, where the `try` binds a handler that exits to it immediately. There's no equivalent of putting code in the handler other than a single unwind-and-jump, and there's no equivalent of restarts.
In CL, a condition handler gets called on top of the existing stack and can inspect what's going on before choosing where to exit to. Other functions in the call stack can provide alternative exits (restarts), like “continue processing anyway” or “substitute a placeholder value”; these are dynamically named, rather than lexically bound like `catch` . So there's a lot more possible decoupling, at least in theory. The equivalent of `finally`/destructors is `unwind-protect`, which has to interoperate with the condition mechanism but doesn't deal with conditions itself.
In C++ or Java, you could implement the restarts with a (thread-local) stack of restart descriptions plus try/finally or constructor/destructor, and the same for handlers, and then do your nonlocal exits with specialized throwables. I did something similar in Lua, in fact, while trying to extend it into a fancier language. But a “normal” `throw` will bypass all of that. That's not dangerous if you do the unwind-protects properly, but none of your existing libraries will be built for it, and the results will be kind of anemic.
In the Java-style objects+throw/catch world, similar things can be achieved by toggling “what to do if X happens” state or plumbing callback pointers through the object graph beforehand, which is similar but more ad-hoc, and possibly harder to add to existing systems. That said, the CL style proper is very tied to the call stack, which can also make things tricky.
> The equivalent of `finally`/destructors is `unwind-protect`, which has to interoperate with the condition mechanism but doesn't deal with conditions itself.
It actually doesn't need to interoperate with the condition system; it has to interoperate with the stack-unwinding primitives - `go`/`tagbody`, `return-from`/`block`, and `throw`/`catch` - that are one of the foundations of the condition system.
If it works with those, then it works with a condition system, since all control flow that happens inside the condition system is a derivative of those primitive operators.
Ouch. Kudos for the pun, that was truly awful. :D
To top it off, you can provide a "restart" class that this new __cxa_throw treats like ordinary C++ exceptions, and throw an instance of it to perform the "exit".
I have no idea if this hack would comply with the standard, but it works with GCC.
You'd be missing COMPUTE-RESTARTS, however, so there'd be no asking the user where to jump.
Also, if we can have dynamically established handlers, then we could also have dynamically established restarts (even if by means of a dynamic variable implemented via a lexical variable + a destructor), and therefore we could have a COMPUTE-RESTARTS of our own and then be able to invoke it arbitrarily as well as invoke individual restarts.
> A two-phase exception-handling model is not strictly necessary to implement C++ language semantics, but it does provide some benefits. For example, the first phase allows an exception-handling mechanism to dismiss an exception before stack unwinding begins, which allows resumptive exception handling (correcting the exceptional condition and resuming execution at the point where it was raised). While C++ does not support resumptive exception handling, other languages do, and the two-phase model allows C++ to coexist with those languages on the stack.
What would C++ be coexisting with on non-mainframe hardware?
> It is also noteworthy that this aspect of the condition system is fully independent from Lisp’s homoiconicity; rather, it is a consequence of the way other programming languages are designed. For instance, when one divides by zero in a Java program, then there is nothing carved in stone which would prevent the language from winding the stack further and executing some code that will analyze the dynamic environment in which the error happened, calling all error handlers found in the dynamic environment, and then—if no handler transferred control outside the error site—either entering an interactive debugger of some sort or giving up and crashing. However, Java goes a different way: it immediately destroys the stack by throwing an exception. That is a design choice which was made by the Java creators; Lisp’s homoiconicity has nothing to do with this fact, as can be demonstrated by the multiple independent implementations of condition systems in non-homoiconic languages that we have mentioned earlier.
(Thanks for the assist!)
I hope that the book is useful for you; in case of any questions, please feel free to ask me and/or make issues on the GitHub repository of the book. In the worst case, I'll create or expand the book's errata... or write yet another appendix.
From the Amazon blurb:
>In part 1 of The Common Lisp Condition System, the author introduces the condition system using a bottom-up approach, constructing it piece by piece. He uses a storytelling approach to convey the foundation of the condition system, dynamically providing code to alter the behavior of an existing program. Later, in part 2, you’ll implement a full and complete ANSI-conformant condition system while examining and testing each piece of code that you write.
Implementing a condition system in a Common Lisp implementation is actually a mostly solved problem, since one can use e.g. https://github.com/phoe/portable-condition-system or borrow some code from an already working Common Lisp implementation.
Apparently it's analogous to "exception handling" in other languages, but with some extra features.
Props to the author for getting out his first book!
Take a peek, it doesn't unwind the stack unless you ask it to, which gives it much more power than exception handling in languages such as Java.
I like Scheme but having seen what the condition system could do on Lisp Machines decades ago makes me feel like we are currently living in a computing dark ages.
There are implementations of condition systems as libraries, too. Copying links from my book:
* Python: https://github.com/svetlyak40wt/python-cl-conditions
* Clojure: https://github.com/clojureman/special
* Ruby: https://github.com/michaeljbishop/mulligan
* Perl: https://metacpan.org/release/Worlogog-Incident, https://metacpan.org/release/Worlogog-Restart
I'll give some examples:
You cannot appreciate the extreme simplicity of Pascal or other Wirthian languages if you never wrote your own compiler. For the average developer, it is just a procedural language with weird excessive syntax, annoying separation between interface and implementation and way too picky about where you declare your variables.
You cannot appreciate Forth if you don't know how the machine works and see the beauty of threaded code. Forth then just becomes this reverse Polish notation thing that some crazy people love. They really must be nuts, as I'm solving my problems with C and JavaScript just fine. Why do they like RPN so much and why can't they just shut up about it?
So the same goes for Lisp and Smalltalk. These are not regular languages, they have operating system aspirations. What a small group of people were doing with Lisp Machines when everyone else were using line editors is completely unknown for the average developer, they just see the parenthesis and run away.
As you mention it is also the reason why I'm partial to Common Lisp over something like Scheme and Clojure. I mean, I like those a lot as well, but they don't offer me much over plain JavaScript and I'd rather use APL :)
You don't need to have written a compiler. Just have wanted real modularity. You can get it with some popular and mainstream OO languages (Java, C#), but it's horribly broken in many other mainstream languages (C++).
Separating interface from implementation shines when dealing with larger teams, and can really be nice for compile times which improves feedback loops.
Regarding '"interface vs implementation" nonsense', that's kind of nonsense to call it nonsense. Most of us don't (professionally) get to work in Lisp or other non-batch compiled languages. In those cases, the separation (in sane language implementations) can lead to much faster compilation time. But they also help to work with teams at scale, and make it easier to substitute implementations without having to recompile the entire thing (though you'll still usually have to relink it).
I don't have much experience with web applications in Lisp, but there's some substantial tooling there and I know of people who have done that before, both client- and server-wise.
I have no experience whatsoever about Common Lisp on mobile.
The situation with desktop apps is pretty good. You have two commercial solutions and the usual open source bindings to popular libraries to choose from. There's also McCLIM if you fancy doing some archeology. https://github.com/CodyReichert/awesome-cl#gui
Concerning mobile, there is a LispWorks runtime, a subset of Common Lisp called mocl and there's also the option to use ECL. These need a bit of tinkering, as is usual for non platform languages.
As for demonstrating it, two of the recent Online Lisp Meeting[1] videos utilize McCLIM: one of them describes Clouseau, an inspector application written in McCLIM[2] (running on the Mezzano OS as well[3], since McCLIM runs there too!) and the other utilizes McCLIM to visualize the functioning of Eclector, a portable and conforming implementation of the Common Lisp reader[4].
[0] https://github.com/McCLIM/McCLIM/graphs/commit-activity
[1] https://www.youtube.com/playlist?list=PLgq_B39Y_kKD9_sdCeE5S...
[2] https://www.youtube.com/watch?v=-1LzFxTbU9E&list=PLgq_B39Y_k...
[3] https://www.youtube.com/watch?v=cooTl4-9bhg&list=PLgq_B39Y_k...
[4] https://www.youtube.com/watch?v=3Yvv2XVBi58&list=PLgq_B39Y_k...
This allows for some extra flexibility with regard to how to handle errors, and with regard to how conditions may be used without any unwinding whatsoever.
(Thanks for the props!)
(My Common Lisp is getting rusty, sorry if I've slightly jubmled the condition/handler/restart lingo)
Still, I've actually wanted to write it there since I considered it to be important, and because that argument came into existence during the first Hacker News discussion about the book (and therefore is a product of more than just my mind).
It may come down to how we actually use exceptions in those languages, however, that makes the difference. I worked on a system written in PHP + Bash which had been built to have some restartability. It was a horrible kludge, and to improve the system you had to pore over the logs to try to divine what had happened, but there was an attempt.
Again, that's just control flow, and that's because throwing exceptions is the only way of performing non-local exits in those languages. Actual exception handling starts when the existing control flow cannot handle some situation that has occurred, and someone - or something - needs to be able to create new control flow dynamically in order to continue program execution and therefore the process that was ongoing there.
Or, you know, we can do the industry standard: crash the program, and restart the thread or daemon or virtual machine or server cluster in question in hope that turning it off and on again helps. /s
My copy arrived yesterday!