20

Restarts in Common Lisp

 4 years ago
source link: https://sulami.github.io/posts/common-lisp-restarts/index.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

I have been reading Practical Common Lisp by Peter Seibel over the weekend, which is an excellent introduction to Common Lisp, showcasing its power by writing real programs. If you are interested in Lisp or programming languages at all, I recommend at least skimming it, it is free to read online.

Writing a Lisp-descended language professionally, and also living inside Emacs, I had dabbled in Common Lisp before, but I still found something I was not aware of, restarts. I do not think that this is a particularly well known feature outside the Lisp world, so I would like to spread awareness, as I think it is a particularly interesting take on error handling.

The book explains restarts using a mocked parser, which I will slightly modify for my example. Imagine you are writing an interpreter/compiler for a language. On the lowest level you are parsing lines to some internal representation:

(define-condition invalid-line-error (error)
  ((line :initarg :line :reader line)))

(defun parse-line (line)
  (if (valid-line-p line)
      (to-ir line)
    (error 'invalid-line-error :line line)))
We define a condition,

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.

which is very similar to an exception object with metadata in other languages, and a function which attempts to parse a single line.

This is assuming of course that a line always represents a complete parsable entity, but this is only an example after all.

If it turns out that the line is invalid, it throws an error up the stack. We attach the line encountered, in case we want to use it for error reporting.

Now imagine your parser is used in two situations: there is a compiler, and a REPL. For the compiler, you would like to abort at the first invalid line you encounter, which is what we are currently set up to do. But for the REPL, you would like to ignore the line and just continue with the next line. I’m not saying that is necessarily a good idea, but it is something some REPLs do, for example some Clojure REPLs.

To ignore a line, we would have to either do it on a low-level, return nil instead of throwing and filter out nil values up the stack. Catching the error will not help us a lot, because at that point we have lost our position in the file already, or have we?

The next layer up is parsing a collection of lines:

(defun parse-lines (lines)
  (loop for line in lines
        for entry = (restart-case
                     (parse-line line)
                     (skip-line () nil))
        when entry collect it))
This is where the magic begins. The loop construct just loops over the lines, applies parse-line to every element of the list, and returns a list containing all results which are not nil . The feature I am showcasing in this post is restart-case . Think of it this way: it does not catch an error, but when the stack starts unwinding

Technically not unwinding yet, at least not in Common Lisp.

because we threw an error in parse-line , it registers a possible restart-position. If the error is caught at some point,

If it isn’t caught, you will get dropped into the debugger, which also gives you the option to restart.

the error handler can choose to restart at any restart-point that has been registered down the stack.

Now let us have a look at the callers:

(defun parse-compile (lines)
  (handler-case
      (parse-lines lines)
    (invalid-line-error (e)
                        (print-error e))))

(defun parse-repl (lines)
  (handler-bind ((invalid-line-error
                  #'(lambda (e)
                      (invoke-restart 'skip-line))))
    (parse-lines lines)))

There is a lot to unpack here. The compiler code is using handler-case , which is comparable to catch in other languages. It unwinds the stack to the current point and runs the error handling code, in this case print-error .

Because we do not actually want to unwind the stack all the way, but resume execution inside the loop in parse-lines , we use a different construct, handler-bind , which automatically catches invalid-line-error and invokes the skip-line restart. If you scroll up to parse-lines now, you will see that the restart clause says, if we restart here, just return nil , and nil will be filtered on the very next line by when entry .

The elegance here is the split of error handling code, and decisions about which error handling approach to take. You can register a lot of different restart-case statements throughout the stack, and let the caller decide if some errors are okay to ignore, without the caller having to have intricate knowledge of the lower-level code. It does need to know about the registered restart-case statements though, at least by name.

If you want to learn more about this, make sure to have a look at the book, it goes into much more detail than I did here.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK