In this chapter we'll see some of the specialized control flow forms provided by Common Lisp.
One of the challenges of writing robust programs is to make sure that important parts of your code always run, even in the presence of errors. Usually, this is most important when you're allocating, using and releasing resources such as files and memory, like this:
; Setup Allocate some resources Open some files ; Process Process using files and storage (may fail) ; Cleanup Close the files Release the resources
If the processing step might fail (or be interrupted by the user) you should make sure that every possible exit path still goes through the cleanup section to close the files and release the storage. Better still, your program should be prepared to handle errors that occur during the setup phase as you allocate storage and open files, since any of these operations might also fail; any partially completed setup should still be undone in the cleanup section.
UNWIND-PROTECT form makes this especially
easy to do.
(let (resource stream) (unwind-protect (progn (setq resource (allocate-resource) stream (open-file)) (process stream resource)) (when stream (close stream)) (when resource (deallocate resource))))
Here's what happens. The
we'll use the
NIL value to mean that there has been no
resource allocated or file opened. The first form in the
UNWIND-PROTECT is a "protected" form; if control leaves
the protected form via any means, then the rest of the
forms -- the "cleanup" forms -- are guaranteed to be executed.
In our example, the protected form is a
set our local variables, then
PROCESS uses these
SETQ assigns values sequentially to our
(ALLOCATE-RESOURCE) must succeed
before a value can be assigned to
OPEN-FILE must succeed before its value can be assigned
STREAM. A failure (i.e. an interrupt or error) at
any point in this sequence will transfer control out of the protected
If the initializations succeed and
normally, control continues into the cleanup forms.
If anything causes the protected form to exit -- for example, an
error or an interrupt from the keyboard -- control is transferred
immediately to the first cleanup form. The cleanup forms are guarded
WHEN clauses so we won't try to close the stream or
deallocate the resource if an error caused them to never be created
in the first place.
RETURN-FROM forms give
you a structured lexical exit from any nested computation. The
BLOCK form has a name followed a body composed of zero
or more forms. The
RETURN-FROM form expects a block
name and an optional (the default is
NIL) return value.
? (defun block-demo (flag) (print 'before-outer) (block outer (print 'before-inner) (print (block inner (if flag (return-from outer 7) (return-from inner 3)) (print 'never-print-this))) (print 'after-inner) t)) BLOCK-DEMO ? (block-demo t) BEFORE-OUTER BEFORE-INNER 7 ? (block-demo nil) BEFORE-OUTER BEFORE-INNER 3 AFTER-INNER T
When we call
IF statement's consequent --
7) -- immediately returns the value 7 from the
OUTER ... form. Calling
NIL executes the alternate branch of the
(return-from inner 3) -- passing the
value 3 to the
(BLOCK INNER ... form.
Block names have lexical scope:
transfers control to the
with a matching name.
Some forms implicitly create a block around their body forms.
When a name is associated with the form, such as with
DEFUN, the block takes the same name.
? (defun block-demo-2 (flag) (when flag (return-from block-demo-2 nil)) t) BLOCK-DEMO-2 ? (block-demo-2 t) NIL ? (block-demo-2 nil) T
Other forms, such as the simple
DOTIMES, establish a block named
around their body forms. You can return from a
(RETURN-FROM NIL ...), or just
? (let ((i 0)) (loop (when (> i 5) (return)) (print i) (incf i))) 0 1 2 3 4 5 NIL ? (dotimes (i 10) (when (> i 3) (return t)) (print i)) 0 1 2 3 T
RETURN-FROM are handy for
tranferring control out of nested forms, but they're only useful
when the exit points (i.e. block names) are lexically visible. But
what do you do if you want to break out of a chain of function calls?
; WARNING! This won't work! (defun bad-fn-a () (bad-fn-b)) (defun bad-fn-b () (bad-fn-c)) (defun bad-fn-c () (return-from bad-fn-a)) ; There is no block BAD-FN-A visible here!
THROW, which let you
establish control transfers using dynamic scope. Recall that dynamic
scope follows the chain of active forms, rather than the textual
enclosure of one form within another of lexical scope.
? (defun fn-a () (catch 'fn-a (print 'before-fn-b-call) (fn-b) (print 'after-fn-b-call))) FN-A ? (defun fn-b () (print 'before-fn-c-call) (fn-c) (print 'after-fn-c-call)) FN-B ?(defun fn-c () (print 'before-throw) (throw 'fn-a 'done) (print 'after-throw)) FN-C ? (fn-a) BEFORE-FN-B-CALL BEFORE-FN-C-CALL BEFORE-THROW DONE
Opening a file just long enough to process its data is a very
common operation. We saw above that
be used to ensure that the file gets properly closed. As you might
expect, such a common operation has its own form in Lisp.
(with-open-file (stream "file.ext" :direction :input) (do-something-with-stream stream))
WITH-OPEN-FILE wraps an
CLOSE form around the code you provide, and makes sure
CLOSE gets called at the right time. All of
the options available to
OPEN may be used in
WITH-OPEN-FILE -- I've shown the options you'd use to
open a file for input.