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.
Since restarts must be explicitly invoked to have any effect, you can define multiple restarts, each providing a different recovery strategy. As I mentioned earlier, not all log-parsing applications will necessarily want to skip malformed entries. Some applications might want parse-log-file
to include a special kind of object representing malformed entries in the list of log-entry
objects; other applications may have some way to repair a malformed entry and may want a way to pass the fixed entry back to parse-log- entry
.
To allow more complex recovery protocols, restarts can take arbitrary arguments, which are passed in the call to INVOKE-RESTART
. You can provide support for both the recovery strategies I just mentioned by adding two restarts to parse-log-entry
, each of which takes a single argument. One simply returns the value it's passed as the return value of parse-log-entry
, while the other tries to parse its argument in the place of the original log entry.
(defun parse-log-entry (text)
(if (well-formed-log-entry-p text)
(make-instance 'log-entry ...)
(restart-case (error 'malformed-log-entry-error :text text)
(use-value (value) value)
(reparse-entry (fixed-text) (parse-log-entry fixed-text)))))
The name USE-VALUE
is a standard name for this kind of restart. Common Lisp defines a restart function for USE-VALUE
similar to the skip-log-entry
function you just defined. So, if you wanted to change the policy on malformed entries to one that created an instance of malformed-log-entry
, you could change log- analyzer
to this (assuming the existence of a malformed-log-entry
class with a :text
initarg):
(defun log-analyzer ()
(handler-bind ((malformed-log-entry-error
#'(lambda (c)
(use-value
(make-instance 'malformed-log-entry :text (text c))))))
(dolist (log (find-all-logs))
(analyze-log log))))
You could also have put these new restarts into parse-log-file
instead of parse-log- entry
. However, you generally want to put restarts in the lowest-level code possible. It wouldn't, though, be appropriate to move the skip-log-entry
restart into parse-log-entry
since that would cause parse-log-entry
to sometimes return normally with NIL
, the very thing you started out trying to avoid. And it'd be an equally bad idea to remove the skip-log-entry
restart on the theory that the condition handler could get the same effect by invoking the use-value
restart with NIL
as the argument; that would require the condition handler to have intimate knowledge of how the parse-log- file
works. As it stands, the skip-log-entry
is a properly abstracted part of the log-parsing API.
While conditions are mainly used for error handling, they can be used for other purposes—you can use conditions, condition handlers, and restarts to build a variety of protocols between low- and high-level code. The key to understanding the potential of conditions is to understand that merely signaling a condition has no effect on the flow of control.
The primitive signaling function SIGNAL
implements the mechanism of searching for an applicable condition handler and invoking its handler function. The reason a handler can decline to handle a condition by returning normally is because the call to the handler function is just a regular function call —when the handler returns, control passes back to SIGNAL
, which then looks for another, less recently bound handler that can handle the condition. If SIGNAL
runs out of condition handlers before the condition is handled, it also returns normally.
The ERROR
function you've been using calls SIGNAL
. If the error is handled by a condition handler that transfers control via HANDLER-CASE
or by invoking a restart, then the call to SIGNAL
never returns. But if SIGNAL
returns, ERROR
invokes the debugger by calling the function stored in *DEBUGGER-HOOK*
. Thus, a call to ERROR
can never return normally; the condition must be handled either by a condition handler or in the debugger.
Another condition signaling function, WARN
, provides an example of a different kind of protocol built on the condition system. Like ERROR
, WARN
calls SIGNAL
to signal a condition. But if SIGNAL
returns, WARN
doesn't invoke the debugger—it prints the condition to *ERROR-OUTPUT*
and returns NIL
, allowing its caller to proceed. WARN
also establishes a restart, MUFFLE-WARNING
, around the call to SIGNAL
that can be used by a condition handler to make WARN
return without printing anything. The restart function MUFFLE-WARNING
finds and invokes its eponymous restart, signaling a CONTROL-ERROR
if no such restart is available. Of course, a condition signaled with WARN
could also be handled in some other way—a condition handler could 'promote' a warning to an error by handling it as if it were an error.
For instance, in the log-parsing application, if there were ways a log entry could be slightly malformed but still