Restarts in Dylan

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...


Mathias Hölzl (tc)
Last modified: Sat Dec 6 23:09:02 CET 1997