RETURN-FROM
is executed in a different dynamic context. For instance, bar
could also contain a BLOCK
named a
, like this:
(defun bar (fn)
(format t ' Entering bar~%')
(block a (baz fn))
(format t ' Leaving bar~%'))
This extra BLOCK
won't change the behavior of foo
at all—the name a
is resolved lexically, at compile time, not dynamically, so the intervening block has no effect on the RETURN-FROM
. Conversely, the name of a BLOCK
can be used only by RETURN- FROM
s appearing within the lexical scope of the BLOCK
; there's no way for code outside the block to return from the block except by invoking a closure that closes over a RETURN-FROM
from the lexical scope of the BLOCK
.
TAGBODY
and GO
work the same way, in this regard, as BLOCK
and RETURN- FROM
. When you invoke a closure that contains a GO
form, if the GO
is evaluated, the stack will unwind back to the appropriate TAGBODY
and then jump to the specified tag.
BLOCK
names and TAGBODY
tags, however, differ from lexical variable bindings in one important way. As I discussed in Chapter 6, lexical bindings have indefinite extent, meaning the bindings can stick around even after the binding form has returned. BLOCK
s and TAGBODY
s, on the other hand, have dynamic extent—you can RETURN-FROM
a BLOCK
or GO
to a TAGBODY
tag only while the BLOCK
or TAGBODY
is on the call stack. In other words, a closure that captures a block name or TAGBODY
tag can be passed RETURN-FROM
a BLOCK
, after the BLOCK
itself has returned, you'll get an error. Likewise, trying to GO
to a TAGBODY
that no longer exists will cause an error.[212]
It's unlikely you'll need to use BLOCK
and TAGBODY
yourself for this kind of stack unwinding. But you'll likely be using them indirectly whenever you use the condition system, so understanding how they work should help you understand better what exactly, for instance, invoking a restart is doing.[213]
CATCH
and THROW
are another pair of special operators that can force the stack to unwind. You'll use these operators even less often than the others mentioned so far—they're holdovers from earlier Lisp dialects that didn't have Common Lisp's condition system. They definitely shouldn't be confused with try
/catch
and try
/except
constructs from languages such as Java and Python.
CATCH
and THROW
are the dynamic counterparts of BLOCK
and RETURN- FROM
. That is, you wrap CATCH
around a body of code and then use THROW
to cause the CATCH
form to return immediately with a specified value. The difference is that the association between a CATCH
and THROW
is established dynamically—instead of a lexically scoped name, the label for a CATCH
is an object, called a THROW
evaluated within the dynamic extent of the CATCH
that throws that object will unwind the stack back to the CATCH
form and cause it to return immediately. Thus, you can write a version of the foo
, bar
, and baz
functions from before using CATCH
and THROW
instead of BLOCK
and RETURN-FROM
like this:
(defparameter *obj* (cons nil nil)) ; i.e. some arbitrary object
(defun foo ()
(format t 'Entering foo~%')
(catch *obj*
(format t ' Entering CATCH~%')
(bar)
(format t ' Leaving CATCH~%'))
(format t 'Leaving foo~%'))
(defun bar ()
(format t ' Entering bar~%')
(baz)
(format t ' Leaving bar~%'))
(defun baz ()
(format t ' Entering baz~%')
(throw *obj* nil)
(format t ' Leaving baz~%'))
Notice how it isn't necessary to pass a closure down the stack—baz
can call THROW
directly. The result is quite similar to the earlier version.
CL-USER> (foo)
Entering foo
Entering CATCH
Entering bar
Entering baz
Leaving foo
NIL
However, CATCH
and THROW
are almost CATCH
and the THROW
, the tag form is evaluated, which means their values are both determined at runtime. Thus, if some code in bar
reassigned or rebound *obj*
, the THROW
in baz
wouldn't throw to the same CATCH
. This makes CATCH
and THROW
much harder to reason about than BLOCK
and RETURN-FROM
. The only advantage, which the version of foo
, bar
, and baz
that use CATCH
and THROW
demonstrates, is there's no need to pass down a closure in order for low-level code to return from a CATCH
—any code that runs within the dynamic extent of a CATCH
can cause it to return by throwing the right object.