Here is some material explaining restarts in Dylan. It is
mostly taken from some posts I wrote in
comp.lang.dylan
and is presented in a somewhat
awkward order, since it is composed out of messages appearing in
different posts. If restarts are of interest to you, send me a
mail and I will update it to
something more understandable.
There is a description of the exception mechanism in the book by Feinberg et al. And here follows a part of one of my programs that uses restarts. The program is a PostScript interpreter written in Dylan which isn't finished yet. It is about 10000 lines at the moment and makes heavy use of restarts for error handling.
Most PostScript operators throw exceptions when an error occurs. I use Dylan conditions to implement the normal exception behaviour, but sometimes it is not useful to abort the erroneous function after an error has occured and it would be better for program execution to continue (e.g., to generate as much output on paper as possible, even if parts of it are wrong). Of course the real benefit of restarts would be to have a much more elaborate error handling than the simple scheme described below, which is something that I will probably implement in later versions of the interpreter (if they should remain to be written in Dylan).
There are various error classes, with a base class
<ps-error>
:
define class <ps-error> (<error>)
end class <ps-error>;
...
define class <ps-stack-underflow-error> (<ps-error>)
end class <ps-stack-underflow-error>;
...
The class <ps-ignore-restart>
is used to ignore the
error that caused the exception to be raised and continue the
computation:
define class <ps-ignore-restart> (<restart>)
end class <ps-ignore-restart>;
The following method is a simplified version of the one from the
Dylan Programming book (p. 351, it is called available-restart
there). It checks whether a restart that handles instances of
restart-class
is available. If this is the case
then it returns an instance of a restart, otherwise it returns
false.
define method restart-available?
(restart-class :: <class>)
=> (result :: false-or(<restart>));
block (return)
local method check-restart (type, test, function, initargs);
if (subtype?(type, restart-class))
let instance = apply(make,
type,
initargs | #());
if (test(instance))
return(instance);
end if;
end if;
end method check-restart;
do-handlers(check-restart);
#f;
end block;
end method restart-available?;
The method ignore-errors-if-possible invokes a
<ps-ignore-restart>
if it is available, otherwise
it declines to handle the error.
define method ignore-errors-if-possible
(condition :: <ps-error>, next-handler :: <function>);
let restart = restart-available?(<ps-ignore-restart>);
if (restart)
error(restart);
else
next-handler();
end if;
end method ignore-errors-if-possible;
Ok, after all this preparation let's look at the code that uses
restarts. The function ps-top
returns the topmost
element of a stack. If the stack is empty it raises an instance
of <ps-stack-underflow-error>
. If you choose to
"ignore" the error it returns an instance of
<ps-null>
which is a kind of no-op.
define function ps-top
(interpreter :: <postscript-interpreter>,
stack :: <function>)
=> (head :: <postscript-object>);
if (interpreter.stack.empty?)
block ()
error(make(<ps-stack-underflow-error>));
exception (restart :: <ps-ignore-restart>)
make(<ps-null>);
end block;
else
head(interpreter.stack);
end if;
end function ps-top;
The read-eval-print loop of the interpreter looks like this:
define function rep-loop () => ();
<<print welcome message>>
let handler (<ps-error>) =
method (condition, error)
say("Ignoring Error (%=)!\n",
object-class(condition));
ignore-errors-if-possible(condition, error);
end method;
let handler (<ps-undefined-error>) =
method (condition, error)
say("Ignoring undefined name %=\n", condition.undefined-name);
ignore-errors-if-possible(condition, error);
end method;
let interpreter = make(<postscript-interpreter>);
for (input = read-line(*standard-input*, on-end-of-stream: #f)
then read-line(*standard-input*, on-end-of-stream: #f),
until: input = #f)
execute-ps(input, interpreter);
if (interpreter.operand-stack.empty?)
say-line("<empty operand stack>");
else
say("Value: %=\n", interpreter.top-operand.ps-value);
end if;
say("> ");
end for;
end function rep-loop;
It chooses to print an error message when it catches an
exception and then signals a restart (via
ignore-errors-if-possible
). Let's have a look at
how this is supposed to work: If you try to look at the top
element of an empty stack the function ps-top
is
invoked by the interpreter. It will raise the error of type
<ps-stack-underflow-error>
. Since a handler for
<ps-error>
which is a superclass of the raised
error was established with let handler
, the
anonymous method declared within the handler is invoked in the
context of the rep-loop
(i.e., it is called as if
the call to error
was a call to this anonymous
method). Because a handler for <ps-ignore-restart>
is established by the exception
clause of the
block
construct in the ps-top
function
the method ignore-errors-if-possible
will print an
error message and then raise a <ps-ignore-restart>
.
This restart will be handled by creating a
<ps-null>
-object and exiting the
block
. Phew!
Here is a test run with the interpreter. The PostScript
operator dup
duplicates the topmost element on the
operand stack. It is implemented by calling ps-top
and pushing its return value on the operand stack. If we call
it with the empty operand stack, according to my description the
stack should contain a single object of type
<ps-null>
(which has ps-value
#"ps-null"
). So
> tc:~/prog/dylan/editor$ ./editor -i
The DopeScript interpreter
(c) Matthias Hoelzl 1997
> dup
Ignoring Error ({Class <ps-stack-underflow-error>})!
Value: #"ps-null"
> pop
<empty operand stack>
>
Must be my lucky day ;-)
Now, why isn't there a default handler? I think the main reason
is that this would buy you too much once you have the method
restart-available?
(granted that it is somewhat
tricky to write). And if you really want to you can install
your own handler for restarts with something like
let handler (<restart>)
= method (condition, error)
#f;
end method;
in the main method of your program. I suppose that the reasoning of the Dylan designers to have some restrictions on the signalling of restarts is that recovering from errors is a situation where you really should be knowing what you are doing and not just randomly trying to take some action.
Now it might be reasonable to assume that a restart
actually restarts the computation in the following sense:
"Can you tell me what time it is?" "Uh, it's already quite late." "That's not very helpful!" <looking out of the window> "Well, I guess it must be about 8." "Hey, I have to catch a train!" <goes to the next room, returns> "8:12:43."
In most cases I would tend to implement this with multiple return values (returning the first result and a precision) so that the caller can decide whether to restart the computation itself. But I can see some cases (e.g., when controlling real-world processes) where you cannot complete the computation and then start from scratch.
There is one more confusing aspect of restarts in Dylan.
Exceptions can have aborting semantics like exceptions in C++,
but Dylan supports calling semantics for exceptions as well. It
was not the best thing on my part to introduce the mixture of
let handler and exception clauses in my example. Here is an
example with calling semantics:
define method do-something
let handler (<my-restart>) = ...<including do-something>...
...
if (something bad happened)
signal(<some-condition>)
| block () <recovery-action> end block;
end if;
end method;
The "| block () ..."
part is there in case the
caller doesn't know or care about the exception you raise.
I have written a small program to illustrate how you could do
this and why you probably don't want to. The central method is
signal-and-provide-restart
. Its definition is
define function signal-and-provide-restart
(i :: <integer>)
=> (big-number :: <integer>);
let handler (<improve-restart>)
= method (condition, error)
say("Restarting with value %D instead of %D\n",
(i + 1) ^ 2, i);
signal-and-provide-restart((i + 1) ^ 2);
end method;
let handler (<done-restart>)
= method (condition, error)
say("Returning %D via <done-restart>\n", i);
i;
end method;
if (i < 30)
signal(make(<my-condition>, proposed-result: i))
| block ()
say("Signal failed, returning %D normally\n", i);
i;
end block;
else
say("Returning %D normally.\n", i);
i;
end if;
end function signal-and-provide-restart;
What does it do? It is supposed to generate numbers ≥ 30.
So if its input is ≥ 30 it just returns its input. But it
doesn't know how to solve the problem for inputs less than 30 so
it signals a condition and proposes to return the smaller input
to be returned as result. If the caller cannot accept this, it
can signal an <improve-restart>
; and obtain a
better result, or it can accept that result by signalling
<done-restart>
. If the caller doesn't
establish a handler for <my-condition>
the
function returns its input.
To handle conditions it establishes two handlers, where it calls
itself in the handler for <improve-restart>
and where it returns a result in the handler for
<done-restart>
. The method doesn't take
nonlocal exits unless the handler for
<my-condition>
does.
I'll append the rest of the program so that you can play with it
if you want to, and I'll make some comments on the test run
below.
// Some condition classes
//
define class <my-condition> (<condition>)
slot proposed-result,
required-init-keyword: #"proposed-result";
end class <my-condition>;
define class <improve-restart> (<restart>)
end class <improve-restart>;
define class <done-restart> (<restart>)
end class <done-restart>;
// This is our old friend RESTART-AVAILABLE?
//
define method restart-available?
(restart-class :: <class>)
=> (result :: false-or(<restart>));
block (return)
local method check-restart (type, test, function, initargs);
if (subtype?(type, restart-class))
let instance = apply(make,
type,
initargs | #());
if (test(instance))
return(instance);
end if;
end if;
end method check-restart;
do-handlers(check-restart);
#f;
end block;
end method restart-available?;
// To generate the methods used in the condition handlers:
//
define method make-improve-method (limit :: <integer>)
method (condition :: <my-condition>, next-handler :: <function>);
if (condition.proposed-result < limit)
let restart = restart-available?(<improve-restart>);
if (restart)
signal(restart);
else
next-handler();
end if;
else
let restart = restart-available?(<done-restart>);
if (restart)
signal(restart);
else
next-handler();
end if;
end if;
end method;
end method make-improve-method;
define constant improve-if-lt-10 = make-improve-method(10);
define constant improve-if-lt-50 = make-improve-method(50);
define function signal-invalid-restart () => ();
signal(make(<simple-restart>, format-string: "Restart"));
say("This should be printed.\n");
end function signal-invalid-restart;
// And this is a MAIN method to demonstrate the behaviour
// of the functions.
//
define method main
(name :: <byte-string>, #rest switches)
=> ();
say("\nNo handler installed.\n");
say("Calling function with value 40 (should return 40 normally).\n");
let result = signal-and-provide-restart(40);
say("The result is %D\n", result);
say("Calling function with value 10 (should return 10 ignoring exception).\n");
let result = signal-and-provide-restart(10);
say("The result is %D\n", result);
say("Calling function with value 1 (should retrun 1 ignoring exception).\n");
let result = signal-and-provide-restart(1);
say("The result is %D\n", result);
say("\nInstalling improve-if-lt-10 handler.\n");
let handler (<my-condition>) =
method (condition, error)
say("Handler: improve-if-lt-10.\n");
improve-if-lt-10(condition, error);
end method;
say("Calling function with value 40 (should return 40 normally).\n");
let result = signal-and-provide-restart(40);
say("The result is %D\n", result);
say("Calling function with value 10 (should signal 10).\n");
let result = signal-and-provide-restart(10);
say("The result is %D\n", result);
say("Calling function with value 1 (should signal 25).\n");
let result = signal-and-provide-restart(1);
say("The result is %D\n", result);
say("\nInstalling improve-if-lt-50 handler.\n");
let handler (<my-condition>) =
method (condition, error)
say("Handler: improve-if-lt-50.\n");
improve-if-lt-50(condition, error);
end method;
say("Calling function with value 40 (should return 40 normally).\n");
let result = signal-and-provide-restart(40);
say("The result is %D\n", result);
say("Calling function with value 10 (should signal but "
"return 121 normally).\n");
let result = signal-and-provide-restart(10);
say("The result is %D\n", result);
say("Calling function with value 1 (should signal but return 676 normally).\n");
let result = signal-and-provide-restart(1);
say("The result is %D\n", result);
say("\nInstalling <restart> handler.\n");
let handler (<restart>)
= method (condition, error)
say("Caught invalid restart!\n");
end method;
say("Calling Function that signals invalid restart.\n");
signal-invalid-restart();
say("End of the Program.\n\n");
exit();
end method main;
Well, the first result is not very surprising. If we have no
handler installed, signal-and-provide-restart
just
returns its input:
tc:~/prog/dylan/test$ ./test
No handler installed.
Calling function with value 40 (should return 40 normally).
Returning 40 normally.
The result is 40
Calling function with value 10 (should return 10 ignoring exception).
Signal failed, returning 10 normally
The result is 10
Calling function with value 1 (should retrun 1 ignoring exception).
Signal failed, returning 1 normally
The result is 1
If you install the improve-if-lt-10
handler and
call S-A-P-R with argument 40, 40 is returned normally; if you
call it with argument 10 the handler signals a
<done-restart>
immedeately and so
S-A-P-R
returns 10, called with argument 1 it goes
through a few iterations calling
<improve-restart>
and then returns:
Installing improve-if-lt-10 handler.
Calling function with value 40 (should return 40 normally).
Returning 40 normally.
The result is 40
Calling function with value 10 (should signal 10).
Handler: improve-if-lt-10.
Returning 10 via <done-restart>
The result is 10
Calling function with value 1 (should signal 25).
Handler: improve-if-lt-10.
Restarting with value 4 instead of 1
Handler: improve-if-lt-10.
Restarting with value 25 instead of 4
Handler: improve-if-lt-10.
Returning 25 via <done-restart>
The result is 25
The most interesting case is the last one. If you install the
improve-if-lt-50
handler and call
S-A-P-R
with argument 40, it returns 40 without
signalling. When you call it with argument 10 the
improve-if-lt-50
handler signals an
<improve-restart>
, the handler for
improve-restart
calls S-A-P-R
with
argument 121 and S-A-P-R
returns normally without
signalling any further condition.
Installing improve-if-lt-50 handler.
Calling function with value 40 (should return 40 normally).
Returning 40 normally.
The result is 40
Calling function with value 10 (should signal but return 121 normally).
Handler: improve-if-lt-50.
Restarting with value 121 instead of 10
Returning 121 normally.
The result is 121
Calling function with value 1 (should signal but return 676 normally).
Handler: improve-if-lt-50.
Restarting with value 4 instead of 1
Handler: improve-if-lt-50.
Restarting with value 25 instead of 4
Handler: improve-if-lt-50.
Restarting with value 676 instead of 25
Returning 676 normally.
The result is 676
Installing <restart> handler.
Calling Function that signals invalid restart.
Caught invalid restart!
This should be printed.
End of the Program.
tc:~/prog/dylan/test$
So you can do quite complicated things with exceptions. But I think that you agree with me that even in this simple example it is not very easy to follow the flow of control through the program (all right, it's not particularly hard either). But if your program gets larger and more complicated you'll want to avoid code like that if possible.
Matthias, bringing Spaghetti code to the Dylan community...