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]