Show HN: My book, The Common Lisp Condition System

(amazon.com)

177 points | by phoe-krk 1280 days ago

8 comments

  • phoe-krk 1280 days ago
    Author here.

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

    [4] https://github.com/Apress/common-lisp-condition-system

    • dreamcompiler 1280 days ago
      Just bought it. Thanks for writing this, and thanks for your continuing participation in discussions of CCL issues on github.
      • phoe-krk 1280 days ago
        Thank you; I hope that I will find and allocate enough time in the nearby future to help with maintaining CCL some more.
    • db48x 1280 days ago
      That's a really impressive way of both continuing the conversation and sneaking in extra improvements to your book!
      • phoe-krk 1280 days ago
        Thanks. The only thing that is sad to me is that this content did not make it into the book itself.

        (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.)

    • oalae5niMiel7qu 1280 days ago
      There's an issue with 4.6.8: You can't use saved images to debug errors in SBCL, because SBCL's SAVE-LISP-AND-DIE function throws the stack away.
      • phoe-krk 1280 days ago
        Yes, this last fact is explicitly mentioned in the last two sentences of this section.

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

  • nickdrozd 1280 days ago
    To what extent is the CL condition system inherently tied to Lisp(s)? Is there anything about it that makes it a natural fit for Lisp but not for other languages?

    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?

    • nickdrozd 1280 days ago
      From Appendix E:

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

      • phoe-krk 1280 days ago
        Oh golly. To think that people would quote my own words to answer Hacker News questions...

        (Thanks for the assist!)

      • 7thaccount 1279 days ago
        Why did Java's creators decide to have it do such a thing? Performance? Or just an immediately fail and let the author know philosophy?
        • phoe-krk 1279 days ago
          I actually don't know this. Java creators were well aware of Common Lisp when they were working on Java (the famous quote from Guy Steele, "We were after the C++ programmers. We managed to drag a lot of them about halfway to Lisp."), but for whatever reason they've adopted the stack-destroying approach instead of the Lisp one.
          • lispm 1279 days ago
            The original language was designed to run on small devices and I don't think the designers thought of it as a useful feature for their kind of application domain. Steele wasn't even there at that time. Gosling also had only implemented a weak variant of Lisp (Mocklisp) before.
        • agumonkey 1279 days ago
          I'm not very knowledgeable but Java was designed as a compromise. But for instance anonymous inner classes, AFAICR, were put as a false lambdas but never really emphasized. Not surprised they left out a condition system.. the exceptions were already a step up ?
    • dasyatidprime 1280 days ago
      What phoe-krk mentions is indeed the main thing that blocks “real” integration into existing languages with exceptions: they always unwind before executing the handler. (JWZ even complained about this while writing about Java.)

      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.

      • oalae5niMiel7qu 1280 days ago
        In C++, you can override the __cxa_throw() function to implement a fully-fledged condition system that calls handlers on top of the stack instead of unwinding first. Call the real __cxa_throw if there's no dynamic handler.

        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.

        • louai 1279 days ago
          The Itanium C++ ABI mentions something similar

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

          http://itanium-cxx-abi.github.io/cxx-abi/abi-eh.html

          • oalae5niMiel7qu 1269 days ago
            I am aware that there have historically been other languages besides Lisp that allow resumptive exception handling (PL/I is a historical example), but I'm unaware of any modern language besides Lisp that does it.

            What would C++ be coexisting with on non-mainframe hardware?

        • phoe-krk 1280 days ago
          Could you link me to any sources for that GCC behavior?

          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.

      • phoe-krk 1280 days ago
        Thanks for the assist! I have one comment:

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

        • dasyatidprime 1279 days ago
          Ah, yes! I was treating those as effectively “part of” the condition mechanism for the comparative explanation, but you're right that that's misleading in CL. Thanks for the correction, or, shall I say, good catch. :-)
          • phoe-krk 1279 days ago
            > good catch

            Ouch. Kudos for the pun, that was truly awful. :D

    • phoe-krk 1280 days ago
      It is the latter.

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

      • jbjohns 1280 days ago
        Isn't it possible to do in any system with CPS as well? I've not tried it but I would expect to be able to have a "conditions system" using the CPS monad in Haskell, for example. And Haskell doesn't even have variables the way most programmers think of them.
        • phoe-krk 1280 days ago
          I haven't explored the CPS topic deeply, so I cannot really answer. As far as I understand, CPS preserves the state of the program or stack information by storing it in closures that are ready to be called at any time, whereas a condition system simply does not unwind the stack by simply not unwinding it and executing on top of the already existing stack, ready to both return control to the signaling code and to transfer it somewhere up the stack.

          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.

          • jbjohns 1280 days ago
            CPS can be kind of thought of like a normal programming language with the modification that every function takes an additional parameter "the rest of the program" and calls this instead of "return". Of course once this mechanism is in your language you might have multiple "rest of the program" parameters which the function can pick between.

            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.

            [1] http://lambda-the-ultimate.org/node/1544

            • phoe-krk 1280 days ago
              I see. I know that it is possible to transform primitive CL control flow operators into CPS, as it is shown on https://gitlab.common-lisp.net/cl-cont/cl-cont/-/blob/master..., so I assume that it is also possible a condition system into CPS as a derivative of those operators and then possibly optimize it further to take CPS-specific code traits into account.
    • nradov 1280 days ago
      In principle most other compiled languages could have macros that manipulate the abstract syntax tree. Lisp makes it easy since there is little separation between the source code and AST, but Scala now has macros despite using totally different syntax.
    • blackbear_ 1279 days ago
      Macros for python have been recently proposed [1], and oh boy aren't they ugly!

      [1] https://www.python.org/dev/peps/pep-0638/

      • phoe-krk 1279 days ago
        I'm glad they've finally been proposed, as ugly as they need to be in Python. It's not even about full homoiconicity; a language which is capable of easily understanding its own symbolic representation along with quote/unquote, as opposed to the direct AST representation, is capable of having somewhat bearable macros. See Elixir for a good example, e.g. at https://elixir-lang.org/getting-started/meta/macros.html.
  • hoytech 1280 days ago
    Congrats! The condition system isn't very well described elsewhere, especially relative to how cool it is. I just ordered your book, looking forward to reading it.
    • phoe-krk 1280 days ago
      Thank you for the congrats, and the fact that it wasn't described anywhere is the reason for my original frustration, which in turn is the reason why this book exists.

      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.

  • flubert 1280 days ago
    Is this targeted more at users of the condition system, or implementers who are writing their own common lisp compilers?

    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.

    • phoe-krk 1280 days ago
      Both. The book contains an explanation of how to derive the condition system from the primitive operators of Common Lisp, which should be useful both for users that try to gain some intuition or broaden their understanding of how the condition system works under the hood, and for people who want to implement a condition system in a programming language without one - be it Common Lisp or a different language.

      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.

  • javert 1280 days ago
    If anyone else is wondering what the Lisp "condition system" is...

    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!

    • ahefner 1280 days ago
      It's hard to appreciate the value of restarts if you haven't seen it fully integrated into the environment like on an old lisp machine. Given any exceptional condition deep down in a program, assuming appropriate restarts are bound, it can pop the debugger and let you choose how to proceed, but handlers can also be bound to handle conditions / invoke restarts programmatically. Restarts may be bound at different layers of granularity so if you had a command operating on a set of files, one one of them fails to open, the avaiable restarts might be "Retry command", "Skip this file", "Retry open", "Provide alternate filename", etc. It's sort of a missing link between interactive and batch/scriptable environments that current operating systems are missing.

      (My Common Lisp is getting rusty, sorry if I've slightly jubmled the condition/handler/restart lingo)

    • phoe-krk 1280 days ago
      Not exactly. The Common Lisp condition system is usable for handling error situations, but it's usable for much more as well; the act of signaling a condition does not immediately unwind the stack, like throwing an exception does, and the restart system allows for actively choosing which pieces of dynamically provided code should be run instead of just blindly executing them in order.

      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!)

    • minerjoe 1280 days ago
      It's not that "analogous".

      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.

      • outworlder 1280 days ago
        That's the problem with Lisp features - you don't get to appreciate how powerful they are until you are way deep in the woods, at which point you are now another 'convert' trying to convince others to follow.

        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.

        • TurboHaskal 1280 days ago
          This is exactly the issue. Growing appreciation for this kind of stuff often requires a level of understanding that a lot of the people in the industry lack. You can't miss what you don't know about.

          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 :)

          • Jtsummers 1280 days ago
            > 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 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.

            • oalae5niMiel7qu 1280 days ago
              You haven't seen a fast feedback loop until you've programmed in Lisp. And it has none of that "interface vs implementation" nonsense.
              • Jtsummers 1280 days ago
                I agree that Lisp offers a fast feedback loop, and I have programmed in it (though not professionally) for about 14 years now.

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

          • lenkite 1279 days ago
            Languages first need to be practical. Allow me to make productive desktop, mobile and web-apps with Common Lisp with minimal setup and headache and I'll become a Common lisp fanatic the next day.
        • phoe-krk 1280 days ago
          I sincerely hope that my book aids this problem that you are speaking of - both by allowing programmers and implementers of other languages use the techniques that originate in Lisp, and to try and bash at the Lisp-as-a-meme prejudice that I've seen in many places by showing what the condition system is actually useful for.
  • db48x 1280 days ago
    I think section 4.11 is the most important of the new material.
    • phoe-krk 1280 days ago
      Thank you. I am taking a big risk by making the statement in 4.11 (in the online Appendix E), since it directly implies that none of C++, Java, Python et al actually do any exception handling at all. It's all control flow, always has been.

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

      • db48x 1280 days ago
        You are kind of implying that :)

        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.

        • phoe-krk 1280 days ago
          Yes, I'm implying that. For instance, a lot of Python's use of "throw" is not really because we want to signal an exception in some Python code, we just want to unwind the stack while passing some values and we can't do it in any other way in the language.

          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

  • gibsonf1 1280 days ago
    Just bought it - Thanks!!!
    • phoe-krk 1280 days ago
      Thanks, hope that it serves you well.
  • rlonstein 1279 days ago
    Congratulations!

    My copy arrived yesterday!

    • phoe-krk 1279 days ago
      Thank you! I hope that it serves you well.