Next: Multiple Restarts, Previous: Condition Handler, Up: Common Lisp Conditions [Contents][Index]
The condition system lets you do this by splitting the error handling code into two parts.
You can place restart code in mid- or low-level functions, such as
parse-log-file
or parse-log-entry
, while moving the condition handlers
into the upper levels of the application.
To change parse-log-file
so it establishes a restart instead of a condition
handler, you can change the HANDLER-CASE
to a RESTART-CASE
.
The form of RESTART-CASE
is quite similar to a HANDLER-CASE
except the
names of restarts are just names, not necessarily the names of condition types.
In general, a restart name should:
In parse-log-file
, you can call the restart skip-log-entry
since that’s
what it does. The new version will look like this:
(defun parse-log-file (file) (with-open-file (in file :direction :input) (loop for text = (read-line in nil nil) while text for entry = (restart-case (parse-log-entry text) (skip-log-entry () nil)) when entry collect it)))
If you invoke this version of parse-log-file
on a log file containing
corrupted entries, it won’t handle the error directly; you’ll end up in the
debugger. However, there among the various restarts presented by the debugger
will be one called skip-log-entry
, which, if you choose it, will cause
parse-log-file
to continue on its way as before. To avoid ending up in the
debugger, you can establish a condition handler that invokes the
skip-log-entry
restart automatically.
The advantage of establishing a restart rather than having parse-log-file
handle the error directly is it makes parse-log-file
usable in more
situations. The higher-level code that invokes parse-log-file
doesn’t have to
invoke the skip-log-entry
restart. It can choose to handle the error at a
higher level. Or, as I’ll show in the next section, you can add restarts to
parse-log-entry
to provide other recovery strategies, and then condition
handlers can choose which strategy they want to use.
Here is how to set up a condition handler that will invoke the
skip-log-entry
restart. You can set up the handler anywhere in the chain of
calls leading to parse-log-file
. This may be quite high up in your
application, not necessarily in parse-log-file
’s direct caller.
For instance, suppose the main entry point to your application is a function,
log-analyzer
, that finds a bunch of logs and analyzes them with the function
analyze-log
, which eventually leads to a call to parse-log-file
. Without
any error handling, it might look like this:
(defun log-analyzer () (dolist (log (find-all-logs)) (analyze-log log)))
The job of analyze-log
is to call, directly or indirectly, parse-log-file
and then do something with the list of log entries returned. An extremely
simple version might look like this:
(defun analyze-log (log) (dolist (entry (parse-log-file log)) (analyze-entry entry)))
where the function analyze-entry
is presumably responsible for extracting
whatever information you care about from each log entry and stashing it away
somewhere.
Thus, the path from the top-level function, log-analyzer
, to
parse-log-entry
, which actually signals an error, is as follows:
file:/usr/local/dev/programming/Lisp/LispLang/resources/images/restart-call-stack.png
Assuming you always want to skip malformed log entries, you could change this
function to establish a condition handler that invokes the skip-log-entry
restart for you. However, you can’t use HANDLER-CASE
to establish the
condition handler because then the stack would be unwound to the function where
the HANDLER-CASE
appears.
Instead, you need to use the lower-level macro HANDLER-BIND
. The basic form
of HANDLER-BIND
is as follows:
(handler-bind (binding*) form*)
where each binding is a list of a condition type and a handler function of one
argument. After the handler bindings, the body of the HANDLER-BIND
can
contain any number of forms.
HANDLER-BIND
and HANDLER-CASE
is that
the handler function bound by HANDLER-BIND
will be run without unwinding
the stack–the flow of control will still be in the call to parse-log-entry
when this function is called.
The call to INVOKE-RESTART
will find and invoke the most recently bound
restart with the given name. So you can add a handler to log-analyzer
that
will invoke the skip-log-entry
restart established in parse-log-file
like
this:
(defun log-analyzer () (handler-bind ((malformed-log-entry-error #'(lambda (c) (invoke-restart 'skip-log-entry)))) (dolist (log (find-all-logs)) (analyze-log log))))
In this HANDLER-BIND
, the handler function is an anonymous function that
invokes the restart skip-log-entry
.
You could also define a named function that does the same thing and bind it instead. In fact, a common practice when defining a restart is to define a function, with the same name and taking a single argument, the condition, that invokes the eponymous restart. Such functions are called restart functions.
You could define a restart function for skip-log-entry like this:
(defun skip-log-entry (c) (invoke-restart 'skip-log-entry))
Then you could change the definition of log-analyzer
to this:
(defun log-analyzer () (handler-bind ((malformed-log-entry-error #'skip-log-entry)) (dolist (log (find-all-logs)) (analyze-log log))))
As written, the skip-log-entry
restart function assumes that a
skip-log-entry
restart has been established. If a !malformed-log-entry-error~
is ever signaled by code called from log-analyzer
without a skip-log-entry
having been established, the call to INVOKE-RESTART
will signal a
‘CONTROL-ERROR’ when it fails to find the skip-log-entry
restart.
If you want to allow for the possibility that a ‘malformed-log-entry-error’
might be signaled from code that doesn’t have a skip-log-entry
restart
established, you could change the skip-log-entry
function to this:
(defun skip-log-entry (c) (let ((restart (find-restart 'skip-log-entry))) (when restart (invoke-restart restart))))
FIND-RESTART
looks for a restart with a given name and returns an object
representing the restart if the restart is found and NIL if not. You can invoke
the restart by passing the restart object to INVOKE-RESTART
. Thus, when
skip-log-entry
is bound with HANDLER-BIND
, it will handle the condition by
invoking the skip-log-entry
restart if one is available and otherwise will
return normally, giving other condition handlers, bound higher on the stack, a
chance to handle the condition.
Next: Multiple Restarts, Previous: Condition Handler, Up: Common Lisp Conditions [Contents][Index]