20.2.2 Simple condition handling
A handler can potentially resolve an exceptional situation, although a handler can decline to resolve a particular exception. If an application provides no handlers, then the generic function default-handler is called on the condition. There is a method on <condition> that just returns false, and there is a method on <serious-condition> (a superclass of <error>) that causes some kind of implementation-specific response to be invoked. Most development environments provide a debugger that deals with any serious conditions not handled by the application. Typically, the debugger describes the serious condition being signaled, and might provide any number of options for recovery (or might provide no recovery options). In a sense, the debugger is the handler of final resort.
In the following example, we establish a handler for the condition that we want to resolve, before calling the code that might signal that condition. We redefine the correct-arrival-time and say-corrected-time methods to take advantage of the Dylan exception protocol.
define method correct-arrival-time
(arrival-time :: <time-of-day>, weather-delay :: <time-offset>,
traffic-delay :: <time-offset>)
=> (sum :: <time-of-day>)
traffic-delay + (weather-delay + arrival-time);
end method correct-arrival-time;
define method say-corrected-time
(arrival-time :: <time-of-day>,
#key weather-delay :: <time-offset> = $no-time,
traffic-delay :: <time-offset> = $no-time)
=> ()
block ()
say(correct-arrival-time(arrival-time, weather-delay, traffic-delay));
// We establish the handler in the following two lines
exception (condition :: <time-error>)
say(condition);
end block;
end method say-corrected-time;
The exception clause of block establishes a handler for a condition, and all that condition's subclasses, for any code in the block body, and for any code called by the block body. We say that the handler is established within the dynamic scope of the block body. When an exception is signaled, Dylan starts a search to find the nearest handler available that matches the condition signaled, and that accepts the exception. The nearest handler is the one that was most recently established in the dynamic scope of the signaler. The handler matches the condition if the class associated with the handler (the handler class) is the same as the condition, or if the handler class is a superclass of the condition. You can associate a test with the handler so that the handler can selectively accept the condition. By default, a matching handler always accepts. If a handler established by the exception clause of block matches and accepts, then a nonlocal exit from the signaler occurs, with execution continuing in the body of the exception clause, which is executed in the context of the very beginning of the block. All the locals defined by the block are gone, but the exit procedure (if there is one) is still available. If there is relevant local state, it may be captured in slots of the condition prior to signaling of the condition. The code within the exception clause body is executed, and the value of the last statement in that body is then returned as the value of the block.
In this example, the + method (called by correct-arrival-time) may signal a <time-boundary-error> condition using the error function during the execution of say-corrected-time. If this error is signaled, then the handler established by the block for <time-error> will match the <time-boundary-error> condition. This exception clause will always accept the condition, so a nonlocal exit will occur, and will terminate execution of the error function, the + method, and the correct-arrival-time method. Within the context of the beginning of the block, the variable condition is bound to the condition instance being signaled (the instance supplied to error); then, execution resumes with the code inside the body of the exception clause. The body calls the say generic function on the condition instance, which causes an appropriate error message (instead of the time) to be displayed to the user. Execution then continues normally after the end of the block; in this case, that results in the normal exit from the say-corrected-time method. Figure 20.1 shows the state of execution when error is called, and after the execution of the exception clause body for <time-error> begins. Figure 20.1 is a simplified diagram of the internal calling stack of a hypothetical Dylan implementation. It is similar to what a debugger might produce when asked to print a backtrace at these two points in the execution of the example. The error function called within the + method signals the <time-boundary-error> error, and the exception clause of block in the say-corrected-time method establishes the handler for that error. Once the handling of the exception is in progress, the handler selected is no longer established. If there is relevant local state, it may be captured in slots of the condition being signaled.
|
The advantages of this structured approach to signaling and handling conditions are significant:
The method focuses on the normal flow of control, and the exceptional flow of control appears only where necessary. For example, the
correct-arrival-timemethod does not need to be aware of the potential exceptions at all. The Dylan condition system makes it easier to reuse code that might not know about, or care to participate in, your application-specific exception recovery code.Because
correct-arrival-timedoes not need to participate in the exception-recovery protocol, it can also have a specific return value; thus, like the+method, it might allow better compiler optimizations and better compile-time error checking.We allow room for expansion in the code. For example, at some point,
correct-arrival-timemight do more sophisticated computations with time, which might signal other kinds of time errors. As long as these new time errors inherit from<time-error>, they can be resolved by the same handler established bysay-corrected-time. As the application evolves, we can build various families of error conditions, and can provide application-specific handlers that perform the correct recovery actions for those families.Because we are using the signaling and handling protocol defined by Dylan, casual readers of the code should be able to understand our intent.
Because the handler has access to the condition object, the handler can perform intelligent recovery actions based on the information captured in the condition object when the exception occurred. For example, the handler may examine various slots of the condition object, and perform different actions based on information stored in those slots.
Dylan supports two models of handler execution. The exception clause of block implements the exit model. When you establish handlers by the exception clause of block, you do not have the ability to restart a computation in the context of the signaler, or in a context closer to the signaler than the handler. In Section 20.2.3, we explore the calling model of handler execution, which allows you to recover from an exception without a nonlocal exit back to the point where the handler was established.





