Other Uses for Conditions
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 parsable, you could write parse-log-entry
to go ahead and parse the slightly defective entries but to signal a condition with **WARN**
when it did. Then the larger application could choose to let the warning print, to muffle the warning, or to treat the warning like an error, recovering the same way it would from a malformed-log-entry-error
.
A third error-signaling function, **CERROR**
, provides yet another protocol. Like **ERROR**
, **CERROR**
will drop you into the debugger if the condition it signals isn’t handled. But like **WARN**
, it establishes a restart before it signals the condition. The restart, **CONTINUE**
, causes **CERROR**
to return normally—if the restart is invoked by a condition handler, it will keep you out of the debugger altogether. Otherwise, you can use the restart once you’re in the debugger to resume the computation immediately after the call to **CERROR**
. The function **CONTINUE**
finds and invokes the **CONTINUE**
restart if it’s available and returns **NIL**
otherwise.
You can also build your own protocols on **SIGNAL**
--whenever low-level code needs to communicate information back up the call stack to higher-level code, the condition mechanism is a reasonable mechanism to use. But for most purposes, one of the standard error or warning protocols should suffice.
You’ll use the condition system in future practical chapters, both for regular error handling and, in Chapter 25, to help in handling a tricky corner case of parsing ID3 files. Unfortunately, it’s the fate of error handling to always get short shrift in programming texts—proper error handling, or lack thereof, is often the biggest difference between illustrative code and hardened, production-quality code. The trick to writing the latter has more to do with adopting a particularly rigorous way of thinking about software than with the details of any particular programming language constructs. That said, if your goal is to write that kind of software, you’ll find the Common Lisp condition system is an excellent tool for writing robust code and one that fits quite nicely into Common Lisp’s incremental development style.
Writing Robust Software
For information on writing robust software, you could do worse than to start by finding a copy of Software Reliability (John Wiley & Sons, 1976) by Glenford J. Meyers. Bertrand Meyer’s writings on Design By Contract also provide a useful way of thinking about software correctness. For instance, see Chapters 11 and 12 of his Object-Oriented Software Construction (Prentice Hall, 1997). Keep in mind, however, that Bertrand Meyer is the inventor of Eiffel, a statically typed bondage and discipline language in the Algol/Ada school. While he has a lot of smart things to say about object orientation and software reliability, there’s a fairly wide gap between his view of programming and The Lisp Way. Finally, for an excellent overview of the larger issues surrounding building fault-tolerant systems, see Chapter 3 of the classic Transaction Processing: Concepts and Techniques (Morgan Kaufmann, 1993) by Jim Gray and Andreas Reuter.
In the next chapter I’ll give a quick overview of some of the 25 special operators you haven’t had a chance to use yet, at least not directly.
1Throws or raises an exception in Java/Python terms
2Catches the exception in Java/Python terms
3In this respect, a condition is a lot like an exception in Java or Python except not all conditions represent an error or exceptional situation.
4In some Common Lisp implementations, conditions are defined as subclasses of **STANDARD-OBJECT**
, in which case **SLOT-VALUE**
, **MAKE-INSTANCE**
, and **INITIALIZE-INSTANCE**
will work, but it’s not portable to rely on it.
5The compiler may complain if the parameter is never used. You can silence that warning by adding a declaration (declare (ignore c))
as the first expression in the **LAMBDA**
body.