内容简介:I have been readingWriting 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
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)))
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))
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 inparse-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.
以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网
猜你喜欢:本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。