Last modified on 4 March 2011, at 19:22

Common Lisp/Advanced topics/Condition System

Common Lisp has an extremely advanced condition system. The condition system allows the program to deal with exceptional situations, or situations which are outside the normal operation of the program as defined by the programmer. A common example of an exceptional situation is an error, however the Common Lisp condition system encompasses much more than error handling.

The condition system can be broken into three parts, signalling or raising conditions, handling conditions, and providing recovery from conditions. Almost every modern programming language offers the first two protocols, but very few offer the last (or distinguish between the last two). This last protocol, providing restarts, or ways for the program to recover, is in some ways the most important aspect of Common Lisp condition handling.

RestartsEdit

A restart is a way to recover from an exceptional situation. Exceptional situations arise all the time, oftentimes as errors. If you have been playing along in a REPL with this book, undoubtedly you have landed in a debugger session at least once. The debugger is invoked when a serious condition is raised and the Lisp system has no recourse but to ask you what to do. It gives you a list of options of ways it can recover from this condition. These options are restarts. Often the only restarts the debugger offers are to return to the top level REPL, but sometimes you are allowed to continue, or retry a computation. In addition, other restarts can be defined by you.

For example, if you are attempting to read data from a file, there are quite a few things that can go wrong: the file might not exist, you might have insufficient permissions to read it, or it might be that the data inside the file is corrupted. Each of these happenings are usually thought of as an exceptional situation and each could be dealt with in several ways. In the case of the file not existing, you might want to specify a different file name; in the case of insufficient permissions, you may want to specify a different file name or alter the files permissions in order to make it readable; and in the case where the data is corrupt, you may want to specify a new file name, try to interpret the corrupt data in a meaningful way, or even try to repair the file and reread.

When you implement restarts, your job is to identify these possible recovery mechanisms and put them in place as restarts for the condition system. To fill out our example, let's say you are reading a file that contains several lines, each containing a list of (x,y) coordinates just listed as pairs of numbers delimited by spaces. As an example, the file might look like this:

0 0 100 150 50 30
30 20 65 65 10 20
0 100 150 50 30 0

As a first cut, we might write our function to read this file like this:

(defun read-points-file (filename)
  (iter (for line in-file filename using #'read-line)
        (collecting
         (iter (for val in-stream (make-string-input-stream line))
               (collect val) ))))

Now, we see what happens (in SBCL) if we call the function on a file that doesn't exist:

(read-points-file #p"does-not-exist.data")
 
error opening #P"does-not-exist.data":
  No such file or directory
   [Condition of type SB-INT:SIMPLE-FILE-ERROR]
 
Restarts:
 0: [RETRY] Retry SLIME REPL evaluation request.
 1: [ABORT] Return to SLIME's top level.
 2: [TERMINATE-THREAD] Terminate this thread (#<THREAD "repl-thread" RUNNING {BA80BC1}>)
 
Backtrace:
  0: (SB-IMPL::SIMPLE-FILE-PERROR "error opening ~S" #P"does-not-exist.data" 2)
  1: ((LABELS SB-IMPL::VANILLA-OPEN-ERROR))
  2: (OPEN #P"does-not-exist.data")[:EXTERNAL]
  3: (READ-POINTS-FILE #P"does-not-exist.data")
 
...etc...

We see that SBCL has raised an error, and since it didn't know what to do, it asks the user. It gives a list of restarts, RETRY, ABORT, and TERMINATE-THREAD. If you created a file called does-not-exist.data, then you might restart with RETRY. In the case where the file exists but cannot be read due to permissions, we get the nearly the same result, but the error message Permission denied instead. Again, you could fix the file and invoke the RETRY restart.

It seems prudent that if the file could not be opened for reading, the user may have entered an incorrect file name. In light of this, let's provide a restart to change the file name we are trying to open. We can do this using the form restart-case or the more general restart-bind.

(defun prompt-for-new-file ()
  (list (prompt "Input new file name: ")) )
 
(defun read-points-file (filename)
  (restart-case
      (iter (for line in-file filename using #'read-line)
            (collecting
             (iter (for val in-stream (make-string-input-stream line))
                   (collect val) )))
    (try-different-file (filename)
      :interactive prompt-for-new-file
      (read-points-file filename) )))

Now, if we enter the debugger because the file cannot be read, we get an extra option, TRY-DIFFERENT-FILE.

(read-points-file #p"does-not-exist.data")
 
error opening #P"does-not-exist.data":
  No such file or directory
   [Condition of type SB-INT:SIMPLE-FILE-ERROR]
 
Restarts:
 0: [TRY-DIFFERENT-FILE] TRY-DIFFERENT-FILE
 1: [RETRY] Retry SLIME REPL evaluation request.
 2: [ABORT] Return to SLIME's top level.
 3: [TERMINATE-THREAD] Terminate this thread (#<THREAD "repl-thread" RUNNING {BA80BC1}>)
 
Backtrace:
  0: (SB-IMPL::SIMPLE-FILE-PERROR "error opening ~S" #P"does-not-exist.data" 2)
  1: ((LABELS SB-IMPL::VANILLA-OPEN-ERROR))
  2: (OPEN #P"does-not-exist.data")[:EXTERNAL]
  3: (READ-POINTS-FILE #P"does-not-exist.data")
...etc...
 
Input new file name: #p"does-exist.data"
 
==> ((0 0 100 150 50 30) (30 20 65 65 10 20) (0 100 150 50 30 0))

The form restart-case runs its first argument in an environment where restarts are in effect. This means that the specified restart, TRY-DIFFERENT-FILE, can be invoked at any time during the execution of that first form. In our example, during the execution of the form the debugger was invoked, which means that restart is included in the list of actions it can take.

ConditionsEdit

Conditions are used to describe situations that the programmer doesn't want to handle in the main program flow. We haven't defined any conditions yet in our little example, however, a condition was signalled. This condition was an error that open raised when trying to open files that it cannot. The error is of type sb-int:simple-file-error which is built into the Lisp system.

You can define your own conditions via the define-condition macro which is very similar to the defclass macro.

HandlersEdit

A handler serves the simple purpose of tying a condition to a restart. This means that if that condition is raised, that restart will automatically be chosen, meaning that we don't get dumped into the debugger.

The forms that instantiate handlers are handler-case and the more general handler-bind.

Pedestrian Error HandlingEdit

Common Lisp also provides a set of error handling mechanisms built that mimic the behavior found in most other languages.