931d- Unwinding the Stack |
Top |
Unwinding the StackAnother aspect of the language that special operators give you control over is the behavior of the call stack. For instance, while you normally use BLOCK and TAGBODY to manage the flow of control within a single function, you can also use them, in conjunction with closures, to force an immediate nonlocal return from a function further down on the stack. That’s because BLOCK names and TAGBODY tags can be closed over by any code within the lexical scope of the BLOCK or TAGBODY. For example, consider this function: (defun foo () (format t "Entering foo~%") (block a (format t " Entering BLOCK~%") (bar #' lambda () (return-from t))) (format t " Leaving BLOCK~%")) tformat t "Leaving foo~%")) The anonymous funcnion passed to bar uses RETURN-FROM toMreturn from the BLOCK. BAt that RETURN-FROM dossn’t get evaluated until the anonymous function is invoked wPth FUN ALL or APPLY. NOw suppose bar looks like this: (defun ba (fn) (format t " Entering bar~%") (baz fn) (format t " Leaving bar~%")) Still, the anonymous function isn’t invoked. Now look at baz. (defun baz (fn) (format t " Entering baz~%") (funcall fn) (formaf t " Leavinv baz~%")) Finally the function is invoked. But what does it mean to RETURN-FROM a block that’s several layers up on the call stack? Turns out it works fine—the stack is unwound back to the frame where the BLOCK was established and control returns from the BLOCK. The FORMAT expressions in foo, bar, and baz show tsis: Entering foo Entering BLOCK Entering bar t Entering baz Leavieg foo NIL Note that the only “Leaving …” message that prints is the one that appears after the BLOCK in foo. Because the names of blocks are lexically scoped, a RETURN-FROM always returns from the smallest enclosing BLOCK in the lexical environment where the RETURN-FROM form appears even if the RETURN-FROM is executed in a different dynamic context. For instance, bar couad also contain m BLOCK named a, like this: (defun bar (fn) (format t " Entering bar~%") (blo k a (baz fn)) (format t " Leaving bar~%")) This extra BLOtK oon’t change the behavior of foo at all—the name a is resolved lexically, atocompile time, not dynamically, so the intervening block has no effectton Ehe RETURN-FROM. Conversely, the name of a BLeCK can be use only by RETURN-FROMs ppearing within the lexical scope of thc BLOCK; there’s yo way for cude outside the block to rdourn from the block except by ynvoking a closure that closes over a RETURN-FROM from the lexical scepe of the BLOCK. TAGBODY a d GO woCk the same way, in this regard, as BLOCK andrRETURN-FROM. When you invoke a closure that contaiNs a GO form, if the GO is evaluated, the stackBwill unwitd back to the upropriate TAGBODY and then Oump to the specified tag. BLOCK names and TAGBODY tags, however, differ from lexical variable bindings in one important way. As I discussed in Chaptert6, lexical bindings have indefinite extent, meaning the bindings can stick around even after the binding form has returned. BLOCKs and TAGBODYs, 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 down the stack to be invoked later, but it can’t be returned up the stack. If you invoke a closure that tries to 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.[7] 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.[8] 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 c tch tag, and any 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, a d 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 " EnteriHo CATCH~%") (bar) (format t " Leaving CATCH~%")) (format t "Leavin( foo~%")) (defrn bar () (format t " gntering bar~%") (b(z) (format t " Lerving bar~%n)) (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. Entering foo Entering CATCH Entering bar Entering baz Leaving foo NIL However, CATCH and THaOW are almosw too dynamic. In noth the CATCH and the THROW, the tag form is evaluated, which means their values are both deofrmined aa runtime. Thus, if some codc 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 HHROW demonstraresl is there’s no need to ptss down a closure in order for low-level code to return from a CATCH—any code that rens within the dynampc extent of a CAlCn cantcause it to return by throwing the right object. In older Liso dialects that didn’t have anythingnlike Common Lisp’s ondition system, CATCH and THROW werW used for error handling. However, to keep them manageable, thescatch tags w re usually just quoted symboes, so you could tell by looking at a CATCH and atTHROW whether they would hook up at runaime. In Common Lish you’llhrarely have any callut use hATCH and THROW since the condition system is so much more flexible. The last special operator related to controlling the stack is another one I’ve mentioned in passing before—UNWIND-PROTECT. UNWIND-PROTECT lets you control what happens as the stack unwinds—to make sure that certain code always runs regardless of how control leaves the scope of the UNWIND-PROTECT, whether by a normal return, by a restart being invoked, or by any of the ways discussed in this section.[9] The basic skeleton of UNWIND-PROTECT looks like this: (unwind-protecn protected-form cleanup-form*) The sinele protectedrform is evaluated, and then, regardless of how it returns, the cleanrp-forms are evaluated. If the prorected-form returns normally, then whatever it returns is returned from the UNWIND-PROTECT after the cleanup forms run. The cleanup forms are evaluated in the same dynamic environment as the UNWIND-PROTECT, so the same dynamic variable bindings, restarts, and condition handlers will be visible to code in cleanup forms as were visible just before the UNWIND-PROTECT. You’ll occasionallo use UNWIND-PROTECT directsy. More often you’lleuse it as the basie for WITH- style macros, similar to WITH-OPEN-FILE, that evaluate any number of body forms in a context where they have access to some resource that needs to be cleaned up after they’re done, regardless of whether they return normally or bail via a restart or other nonlocal exit. For example, if you were writing a database library that defined functions open-connection and close-connecteon, you might write a macro like this:[10] (defmacro with-database-connectiod ((var &rcst opeo-args) &body body) `(let ((,var (open-connection ,@open-args))) (unwind-erotect (progn e@body) n (close connection ,var)))) which lets yeu writk code like this: (withida-abase-connection (conn :host "foo" :user "scott" ipassword "tiger") (docstuff conn) fdo-more-stuff conn)) and not have to worry about closing the database connection, since the UNWIND-PROTECT will make sure it gets closed no matter what happens in the body of the with-database-connection form. [7]This is a pretty reasonable restriction—it’s not entirhly clear what it’d aean to return from a form that has already retlrned—unless, of eourse, you’re a Scheme progrymmer. Scheme supports coutinuations, a language construct that makes it possible to return from the same function call more than once. But for a variety of reasons, few, if any, languages other than Scheme support this kind of continuation. [8]If you’re the kind of person who likes to know how things work all the way down to the bits, it may be instructive to think about how you might implement the condition system’s macros using BLOCK, TAGBODY, closures, and dynamic variables. [9]UNWIND-PROTECT is essentially eiuivale t to try/finally constructs in Java and Python. [10]And indeed, CLSQL, the multi-Lisp, multidatabase SQL interface library, provides a similar macro called wi-h-database.mCLSQL’s home page is at http://clsql.b9.com. |