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.
Lisp's 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 LET
binds
RESOURCE
and STREAM
to NIL
--
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 PROGN
that
calls ALLOCATE-RESOURCE
and OPEN-FILE
to
set our local variables, then PROCESS
uses these
resources. SETQ
assigns values sequentially to our
local variables: (ALLOCATE-RESOURCE)
must succeed
before a value can be assigned to RESOURCE
, then
OPEN-FILE
must succeed before its value can be assigned
to STREAM
. A failure (i.e. an interrupt or error) at
any point in this sequence will transfer control out of the protected
form.
If the initializations succeed and PROCESS
returns
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
by 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.
The BLOCK
and 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 BLOCK-DEMO
with T
, the
IF
statement's consequent -- (return-from outer
7)
-- immediately returns the value 7 from the (BLOCK
OUTER ...
form. Calling BLOCK-DEMO
with
NIL
executes the alternate branch of the
IF
-- (return-from inner 3)
-- passing the
value 3 to the PRINT
form wrapped around the
(BLOCK INNER ...
form.
Block names have lexical scope: RETURN-FROM
transfers control to the innermost
BLOCK
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 LOOP
and
DOTIMES
, establish a block named NIL
around their body forms. You can return from a NIL
block using (RETURN-FROM NIL ...)
, or just
(RETURN ...)
.
? (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
So BLOCK
and 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!
Enter CATCH
and 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 UNWIND-PROTECT
can
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 OPEN
and
CLOSE
form around the code you provide, and makes sure
that the 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.