Hacker Newsnew | past | comments | ask | show | jobs | submitlogin
Restarts in Common Lisp (sulami.github.io)
139 points by sulami on April 1, 2020 | hide | past | favorite | 75 comments


Nitpick:

> While Common Lisp calls them “conditions”, I will stick with “error” in this post, because I don’t think “condition” is very self-explanatory if you’re not familiar with the language already. Please forgive my inaccuracy.

This is misleading. It is possible to signal conditions that are not errors, and therefore do not require stack unwinding, like errors do. The condition facility allows one to execute arbitrary code with arbitrary arguments at the SIGNAL site.

`SIGNAL` walks the list of condition handlers and executes the type-matching ones in order of their binding. If a handler returns normally, then next ones are executed; if all matching handlers are exhausted, `SIGNAL` returns, and code execution continues.

    CL-USER> (handler-bind ((condition (lambda (c) (format t ";; Foo: ~A~%" c)))
                            (condition (lambda (c) (format t ";; Bar: ~A~%" c)))
                            (condition (lambda (c) (format t ";; Baz: ~A~%" c))))
               (signal 'condition)
               42)
    ;; Foo: Condition CONDITION was signalled.
    ;; Bar: Condition CONDITION was signalled.
    ;; Baz: Condition CONDITION was signalled.
    42
Of course, a handler can perform a non-local transfer of control elsewhere, e.g. by using `GO` or `THROW`. In such a case, control is transferred out of `SIGNAL`, so next handlers are - naturally - not invoked.

While I understand that this post is trying to appeal to language newcomers, simplifying the whole condition system to an "error try-catch" is a drastic misrepresentation of the condition system that will leave the newbies with the impression that that's all that is possible with that feature. Since it seems that the author is a Lisp newcomer as well (which I wholeheartedly admire! :), I'd advise him to use the original notation of "condition", instead for the one of "error".


This is a central point in this context, far from mere nitpicking.


Update: some of my fixes have made it into that article. Thanks, sulami, for the IRC discussion!


Correct me if I'm wrong but can't all of the cases covered by conditions be handled in an OO language by providing an object that one can register conditions with and call signals upon? I'm trying to imagine library support for languages that don't support conditions directly.


You can implement a condition system in any programming language that has dynamically scoped variables. (That was how the original CL condition system was implemented on Lisp that did not have a condition system - only dynamic variables.)

You can implement dynamically scoped variables with a stack of environments, onto which you push entries atop when entering new dynamic scope and from which you pop entries when leaving a dynamic scope. (That is how the original dynamically scoped systems, including Emacs Lisp, were implemented.)

Example: https://github.com/svetlyak40wt/python-cl-conditions


Aside from restarts, the core difference between the Common Lisp condition system and exceptions in most other languages is that the exception handler can run before the stack is unwound.

Having the call-stack available at the time the handler runs can be very useful; many modern dynamic languages will save some of that information in the exception object. It's still surprising to me that so very few languages have copied this feature (or even independently discovered it).


> the exception handler can run before the stack is unwound.

Old basic had an error handling model that is exceptionally elegant in some cases: "on error goto ...", with an an indication (errline, I think?) of where the exception happened, and then you could "resume next" to continue (perhaps after fixing the error condition, if the error handler is somewhere else) or directly restart at an earlier point if that makes sense.

It's simple, a little goto-ish, and has abuse potential - but if actually used, tended to end with robust code where the "usual path" was the simplified "that's what we usually do", and all the "in case something was unexpected" are listed independently. C code using goto for error handling (e.g. in the linux kernel) has similar properties.

try/catch, in all languages it exists, does not let you restart in the middle unless all the error handling code is in-line (which is comparatively disgusting IMO); lisp conditions do, and more - but they only exist in Lisp.


I wouldn't call it robust. It was way too easy to do it wrong by forgetting some statement that could fail, and then your RESUME ended up resuming something you didn't anticipate, with a potentially invalid program state.


> only exist in Lisp.

Windows's SEH is essentially simplified CL condition system.


Not really, though it does share the property that it executes user code before unwinding the stack (the exception filters), and that the user code can choose to mark the exception as handled and allow the program to continue without unwinding the stack.

However, there is no Restart system, allowing the code that raises an exception to also declare how it can be handled. And there are no alternative exception handling strategies than searching the stack and unwinding in case a suitable handler is not found. Unwinding is not a property of Conditions in general CL, it is just one of the strategies that can be used when signalling a condition.


But in Window's SEH, you can fix up a page fault and re-start the offending instruction. You have access to the detailed machine state, like all the registers and the bad address where an invalid access took place and such.


Lisp conditions seem to take quite a lot from PL/I conditions.


I think you meant to say that the "condition handler can run before the stack is unwound." (whereas exception handlers in Java, C++, Python unwind the stack while searching for the handler)

(I agree it's a nitpick, but since your comment is likely directed to people who do not know the condition system, it's better to be precise.)


Oh, that's why they need to unwind the stack!? These things are what some take for granted and others don't even know how to ask. Thanks.


Unwinding the stack isn't necessary to find the exception handler. The language runtime code that finds the correct exception handler could just as easily walk the return pointers stored on the stack without modifying the stack pointer, frame pointer, or current return pointer. The reasons for unwinding the stack are (1) avoiding running out of stack space, particularly if the exception handler might itself throw (2) simplifying/optimizing the very common case of wanting the exception handler to unwind the stack.

It's vastly simpler in languages where you have resources that must be freed in the correct order (C++ destructors, Java monitors being released, etc.) to ensure correctness. It's difficult to correctly reason about mutating sate to recover from an error while other pieces of code might actually be holding the mutexes/monitors keeping that data safe.

It's an optimization, because you already have to do much of the work of stack unwinding (in the absence of destructors, monitor releasing, and the like) in order to find the correct exception handler.


In .NET, at least, the stack is not unwound when searching for the exception handler. It's walked to determine the handler, and then unwound before running that handler.

(This is particularly visible when debugging, because in the debugger, you still see the original stack at which the exception occurred when it's reported as unhandled - but it had to walk the stack and observe that there's no matching handler to determine that it's unhandled!)

This has an interesting side effect - since .NET catch-blocks can have filters, which are just predicates. Those predicates have to be executed while searching for the handler. Thus, they can observe the original stack before unwinding.

Win32 SEH is similar, wrt filter expressions in __except blocks.

I'm pretty sure that most C++ implementations are similar, except that they don't have filters, and so the two-phase process is not observable from within the application itself (but is observable under debugger).

Python, on the other hand, really does unwind the frames to find the handler - every function only looks at its own handlers, and if it can't find one, then it re-raises the exception to the caller. Thus, you can't readily tell if an exception is handled or unhandled in Python at the point where it occurs.


For .NET, this behavior is actually visible in VB.NET if I remember correctly, it exposes user-specified exception filters instead of only relying on types like C#.

Not sure why that was considered useful in Visual Basic, of all languages :).


It's visible in both languages these days, since both have exception filters. The difference is that VB had them from the very beginning, and C# only got them 5 years ago in version 6.


Oh, I haven't followed C# for quite some time, didn't know they had gotten them.


> It's still surprising to me that so very few languages have copied this feature (or even independently discovered it).

I have often wondered why this is. It seems like Lisps did so many things right and here we are in These Modern Times waiting for people to rediscover "the secret is to bang the rocks together, guys".

Are these features just hard to implement in some platforms/languages? Why? Have they been de-prioritized generally until the zeitgeist decides otherwise?


I think this particular one is fairly straightforward:

1. It's a non-obvious solution (Lisp had (and in some cases, still has) other methods of doing what other languages do with exceptions before the current system

2. It was hard to implement in popular languages; any language without closures is unlikely to have this.

3. More dynamic languages that could implement these fairly easily inherited their exception system from less dynamic languages, with some other features tacked-on, or got the dynamic features after the exception system was already created.

4. Situations in which you aren't going to restart are already (mostly?) solved by the "shove the call-stack into the exception object" which significantly reduces pain compared to e.g. C++ exceptions.


Re 2: the condition system is easy to implement, try/catch implementation includes all needed building blocks.


Gabriel's "Worse is Better" is still relevant and explains a lot (if not everything).

Look at Rust removing CL-style conditions for a good example and Rust is by no means a popular language or a language that makes technical sacrifices in the interest of popular appeal (unlike Python and Javascript).


Expanding a bit on what you're saying, and repeating a bit from another of my comments in this thread, it's vastly easier to reason about exception/condition handling when all of the resources that need to be cleaned up (especially releasing mutexes/monitors) between the exception handler and the exception thrower have been cleaned up.

Making stack undwinding the decision of the exception/condition handler is strictly more powerful, but it greatly increases the number of corner cases that need to be considered when writing correct handlers. Just as a small subset of the issues, consider some data structure for which a mutex must be held in order to correctly recover. Assuming the mutex isn't held at the time the handler is installed, you need to consider 3 cases: (1) the mutex isn't currently held (so the handler must acquire it and remember to release the mutex before resuming normal execution), (2) the mutex is held by some active call frame sitting between the thrower and the handler (in most cases, it would then be safe to modify the data structure, but the handler absolutely must not release the mutex), and (3) the mutex is held by another thread (it's probably not possible to recover in this case).

Standard exception handling is non-local control flow, and that can make some situations difficult to reason about. Exception/condition handling where stack unwinding is optional is non-local control flow on steroids, engaged in 3D Chess Boxing.


That is true but I'd rather have the strategy in my toolbox and the choice to deploy it when needed rather than the language implementor making that decision for me.

That's a fundamental difference between Common Lisp and other more popular languages. CL tries to give you all the tools and trusts you to use them responsibly. Other, more opinionated languages, limit the problem solving approaches they offer in the interest of (popularity|performance|implementation simplicity|personal philosophy).

The CL philosophy makes a lot of sense if you examine its background and the culture that birthed it: a language designed to solve hard problems that did not have well-defined solutions.


Lua has this with `xpcall`.


Many years ago I wrote about restarts in CL using the example of a CSV parser: https://lisper.in/restarts

Fun fact: the restarts based CSV parser was based on some real world work me and a colleague had done for an online travel portal. I wrote about it here: https://www.reddit.com/r/lisp/comments/7k85sf/a_tutorial_on_...


I find that Practical Common Lisp has not aged well. Maybe it's the "practical" part with the specific examples the author used -that are now terribly dated- that is mainly responsible, but the end result is that I no longer recommend it.

My goto recommendation for newcomers is Norvig's PAIP which is full of insightful moments and unbelievably good code and for people with more experience "Let over Lambda" by Doug Hoyte, which is a mind-blowing book mainly because of the time-transcendent nature of the examples Doug chose.


Having read both PAIP and Practical CL, I do not see them as comparable: PAIP teaches classical AI algorithms, and introduces as much CL as nacessary to carry out the task; Practical CL instead has a goal of teaching Common Lisp, and uses coding examples to showcase features the language.

I agree that the MP3 streaming server example in PCL looks outdated, but so are many topics in PAIP as Norvig himself points out at the end of his retrospective [1]. (And I still think that the MP3 parser in PCL is a really good example for showing Lisp I/O, binrary processing abilities, and subtleties of CLOS, despite not being something that anyone would code as a side project nowadays.)

[1]: http://norvig.com/Lisp-retro.html


I don't agree. PAIP goes well beyond "introduces as much CL as necessary to carry out the task" in its Common Lisp presentation. It could easily serve as a first book for someone to learn Common Lisp, even if that person has no interest in AI whatsoever.

Some of the examples in PAIP may be outdated in a historical but not practical sense. I've joked in the past that the code is so well-written than it can be excised and framed on a wall. With code this good, the lessons one can learn are numerous and the code stands the test of time.

Unfortunately, the same can not be said about the PCL examples since - to name two - there are far better ways to do binary parsing and pathname handling today than binary-types and cl-fad. The PCL examples are a product of the time that they were written but they are tied to a particular verbose, subjective style and were not "optimal" or even particularly good, back then.

Ultimately, PCL introduces Common Lisp to the audience as does PAIP. The difference is that reading PCL in 2020 will have minimum, let's say thought-provoking, impact. Reading PAIP (or Let over Lambda, SICP, On Lisp, Lisp in Small Pieces) remains -and will remain- a paradigm-shifting experience.


I found "Lisp In Small Pieces" an extremely difficult book to digest. Non for the contents per se but rather for the prose. I never managed to get past the second chapter. Does anybody share the feeling?


Assuming that you're referring to the English version (it was originally written in French), I do agree that it's not an easy read. The translator did not do a good job in my view.

It does come together however, with some additional effort and extra passes.


Do you have pointers for how to better do binary and path management?


Use UIOP [1] for all path/filesystem (and more) operations. It's part of ASDF which means it's bundled with every Common Lisp implementation that counts. Generally, if something is covered by UIOP, you should use it instead of depending on a different library.

Binary parsing depends on the application constraints.

You can't go wrong by studying Frode's binary-types (not the same as PCL binary-types) though [2].

[1] https://common-lisp.net/project/asdf/uiop.html

[2] https://github.com/frodef/binary-types/


For those not in the know, PAIP is Paradigms of Artificial Intelligence Programming, with a fantastic repo here: https://github.com/norvig/paip-lisp


Conversely, it has aged well in that all of the code still works. My Scala and Python books? Grrr


PAIP is a great book. It both teachers Lisp programming and "classical" AI programming.

Knowing classical AI IMHO should be considered as part of programmers toolbox. What was once bleeding edge AI is now high level heuristic problem solving.


Are we really at a point that MP3 parsing is so old-fashioned that it's not worth recommending a book which uses it for an example?

Guys, this is software not Milan Fashion Week. It's okay: those are just examples. Software still has to parse things, even in 2020!


And besides, this part of the book has gone a long way on how I've been programming for and setting up my Pirate Radio (Raspberry Pi radio). There are probably easier ways to do it, including just using the software they recommend in the first place, but then I don't get anywhere near the level of enjoyable education from it either.


I'm planning to read PAIP over the upcoming weekend as a follow-up, I've already got it sitting here.

I know next to nothing about AI, beyond the basics of RNNs, so I'm hoping to learn a lot.


The material handled in PAIP mostly focuses on classical symbolic AI, as opposed to the modern ML-based AI that you refer to in RNNs.

(Not to discourage you from reading the book, of course!)


It's AI in the rule-based GOFAI paradigm rather than modern neural-network-based ML.

Treat it as a review of some of the historical classics of computer science and you can't go wrong.


You may need more than a weekend!



I respectfully disagree. I think PCL still does an excellent job in exposing CL features with coding examples.


I have read both and I also like PAIP better. I find PCL good only for somebody that don't have much programming experience. I believe that somebody that is a seasoned programmer and that wants to gets some understanding of CL will appreciate PAIP better. This is because PAIP begins with an introduction of the language that just touches all the basic stuff upfront very clearly and only afterwards starts to build on examples. So if one wants only to get an introduction to CL he can just read the first chapters and forget the rest. PCL does the opposite introducing just a few features at a time and providing a lot of examples side by side. This might force the reader to pay attention to stuff he is not really interested in.

P.S: I found the examples/practice part quite boring in both cases but for sure PAIP is more interesting as there is a chance to learn something new/unusual.


In TXR Lisp, I unified condidtions and restarts into one thing: exceptions. By programmer convention only, restarts are exceptions derived from restart.

The documentation has dialect notes about this with a comparative example:

https://www.nongnu.org/txr/txr-manpage.html#N-00F77525


Can somebody please also elaborate what is the difference between condition system in CL and "new thing" algebraic effect handlers? On the surface I see no difference, other that the latter is being a more formalized version of conditions? But we have same properties: 1. signal a condition to some handler(s), possibly with value. 2. reify the stack into a (delimited up to level with handler) continuation and let handler decide what to do with it: save, restart, resume with some value. Am I missing something?


AFAICT Algebraic Effects are a way of encoding conditions into a static type system. The idea of signaling a condition and passing a continuation is not the new part (as you've noted), it's the _algebra_ part, where a type system can check at compile time that these conditions that a piece of code calls are implemented by the context it is called in.


thank you!


The CL condition system is completely orthogonal to any concept of continuations or stack unwinding. Both condition handlers and restarts are plain functions and any kind of stack unwinding has to be implemented inside these functions (by means of TAGBODY/GO, BLOCK/RETURN-FROM or THROW/CATCH).


To add to that, as an example, here's how you could implement something similar in Clojure [1]. It just relies on dynamic scope to run the handler without unwinding the stack.

[1] https://gist.github.com/msgodf/6f4e43c112b8e89eee3d


what's the difference from what you said (handlers and restarts are functions) vs. a continuation, which could ostensibly just be a function that has closed over the state of the runtime before the condition was triggered?



A long time ago I wrote a conditions system for python: https://code.google.com/archive/p/pyconditions/

I wouldn't recommend using it, but it is an interesting error/control model, and I always wondered why it hasn't seen wider implementation - I presume because it is closer to the "dangerous" end of the power spectrum.


Hey Sulami, this is off topic, but how did you get those nice sidebar footnotes? What theme are you using? Or how did you edit the css?


This is the tufte-css, with some modifications.

Raw css is here: https://github.com/edwardtufte/tufte-css

My additions on top (and all other source) here: https://github.com/sulami/sulami.github.io/tree/develop/css



The condition system in Common Lisp is really amazing piece of work. Error and exception handling is just subclass of larger set of possible uses called conditions.


Interesting. This looks like an implementation of the abstract computer science notion of continuations (https://en.wikipedia.org/wiki/Continuation); neat to see them used this way.


The Common Lisp condition system is actually just a dynamically-scoped hook system.

Handlers are the actual hooks; conditions are the object that may trigger some of the hooks when they are signaled. Restarts are an addition on top of the hooks that allows for easier non-local transfer of control; they provide little that handlers do not provide unless you count the Lisp debugger - by design, the only way to leave the debugger is by using a restart.

In other words: no continuations here.


Interesting. I'm not sure I understand; what do the dynamically-scoped hooks lack that continuations would give you?


In Common Lisp, you can only transfer control up the stack. With continuations, you get multiple stacks that you can transfer control between.


To be frank, I do not know. I am not a continuation specialist or even an adept, so I cannot say myself; all I can say for sure is that a condition system can be implemented (and is actually implemented in practice) without any continuations, and therefore doesn't require the underlying language to support continuations. (Common Lisp, for example, purposefully doesn't have them in standard - see http://www.nhplace.com/kent/PFAQ/unwind-protect-vs-continuat...)


It's really orthogonal to continuations.

Scheme has them (continuations) explicitly. CL chose not to, I think for performance reasons if I recall correctly.


With a quick DuckDuckGo-ing, I found this intro to be really understandable compared to most links I read:

https://z0ltan.wordpress.com/2016/08/06/conditions-and-resta...


R, continuing its theme of being a weird Lisp with C syntax and really bad function names, has a condition system: https://adv-r.hadley.nz/conditions.html


Keep meaning to look at this, Clojure has some libraries for this I could use:

https://github.com/clojureman/special#alternative-libraries


The author doesn't seem to understand that not all exceptional situations are errors, which suggests he is a complete newbie to exception handling, which explains why he didn't propose the word "exception" instead of "condition".


I’m really awestruck that’s somebody took the time to write an article about Common Lisp, made the approximation of conditions as exceptions, and has many commenters like you and others nitpicking and claiming newbiness.

If every time somebody writes something about Lisp and it doesn’t tell 100% of the story, and somebody else chimes in “well ackchewelly you must not be very experienced”, nobody is going to write Lisp.

I think the author did a phenomenal job referencing the book they’re reading, and discussing what they learned.

Besides, when’s the last time you or anybody else in this thread actually defined and used in a serious manner a non-error condition? It happens, but beyond WARN, it’s so rare and you probably want to use other dynamic constructs anyway.

The author could improve simply by saying “Conditions are most often used for error handling” and change little else.


If there are lots of comments every time someone writes about Lisp, that's a good health indication.


Not all conditions signify exceptional situations. The author conflated error with condition but you're doing the opposite of conflating condition with exception.

The big plus of the Common Lisp condition system is that it gives you a programmable substrate that you can use to implement a wide variety of signaling and control flow systems. It does give you error and exception handling -on top of the condition system- but that shouldn't shoehorn your thinking into just these problem domains.


As an example of non-exceptional condition signaling:

http://jacek.zlydach.pl/blog/2019-07-24-algebraic-effects-yo...

Progress can be signaled, and then dealt with if needed. In headless mode it won't be handled. In a console it could be a progress bar or statement. In a GUI, update a variable that changes the status display.

And it ends up as little more than a no-op if there are no handlers for it. Nothing exceptional about it, but creates a great deal of flexibility in managing a system.


There is also nothing conditional about reporting progress, except in the sense that the progress is an indication of state.

According to a Honeywell PL/I reference manual, "a condition is a state of the executing program".

The progress of some activity being 50% through is a state of the executing program.

(All the built-in PL/I conditions are exceptional situations though. This is really the same use of the word "condition" that we find in "condition status register", a machine word that tells us via flags that exceptional situations have happened like numeric overflow.)


Not all exceptions signify exceptional situations, either.




Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: