[r6rs-discuss] [campbell_at_mumble.net: Re: [Formal] Recursive exception handling considered harmful]

From: John Cowan <cowan>
Date: Sat Mar 17 19:40:51 2007

----- Forwarded message from Taylor R Campbell <campbell_at_mumble.net> -----

To: r6rs-discuss_at_r6rs.org
Cc: sperber_at_informatik.uni-tuebingen.de, cowan_at_ccil.org
Subject: Re: [Formal] Recursive exception handling considered harmful
Date: Fri, 16 Mar 2007 04:52:33 +0000
From: Taylor R Campbell <campbell_at_mumble.net>

   Date: Thu, 15 Mar 2007 12:17:25 -0400
   From: Michael Sperber <sperber_at_informatik.uni-tuebingen.de>

   Then how is punting on an exception distinguished from returning a
   value?

The design that I had in mind has no provision at all for a handler
simply to return a value to the signaller. This encroaches on the
space of a principled recovery protocol, without offering the most
important part of one: choice. If there is a unique path to recovery,
then why did the signaller ask for another part of the program to
handle the condition to begin with?

What I wanted was some way for the signaller or the condition to
provide some default handler for the rare case in which all of the
dynamic handlers declined to handle the condition. This default
handler is a last resort, not a preferable path. For example, when
signalling a warning, the default handler might be a procedure that
prints the warning's message to a standard error port. This allows
information to be reported even if there are no useful dynamic
handlers, such as in a thread whose dynamic environment is extremely
minimal or even perhaps corrupted.

I also want a principled recovery protocol, or what Common Lisp and
Dylan call `restart systems', but I don't want a meagre simulacrum of
a recovery protocol in the form of what R6RS currently calls
`continuable exceptions'. A principled recovery protocol allows the
signaller, or anything in the signaller's dynamic environment, to
present to the condition handlers an array of recovery options.
Possibly the most common kind of condition handler is a debugger, or,
transitively, a human programmer, for whom the choice of recovery
options is invaluable, especially in a very complex system, possibly
compiled with heavy compiler optimization flags, where the low-level
debugger operation of finding the desired stack frame at which to
restart computation is not an option.

A principled recovery protocol is unlikely to find its way into R6RS,
but I'd rather not provide a simulacrum of a recovery protocol that
prevents a principled one from being developed later because its
purpose was already perceived to have been filled.

This, then, is the condition signalling mechanism that I wanted to
see:

1. If a program encounters a condition beyond which it has no obvious
   choice to proceed, it may defer the choice of further operation to
   a dynamically enclosing handler by /signalling/ a condition. It
   may supply a default handler, which will be invoked if all dynamic
   handlers decline to handle the condition. The default handler will
   be passed the condition that the outermost dynamic handler returned
   (see below on declining).

   The default value of the default handler is unspecified, except
   that serious conditions must be handled specially (e.g., as fatal
   errors, which cause termination of the thread or the Scheme
   program, and perhaps trigger core dumps).

2. When a condition is signalled, the nearest dynamically enclosing
   condition handler is called in a dynamic environment in which it
   has been popped off of the stack of condition handlers; that is,
   the nearest dynamically enclosing condition handler inside a
   condition handler is whatever was nearest when that handler was
   established.

     ;;; This will signal an unbound variable error in the handler
     ;;; that was established in the enclosing dynamic scope; it will
     ;;; not loop.

     (with-condition-handler
         (lambda (condition)
           (if (foo-condition? condiition) ;Typo!
               (handle-foos condition)
               condition))
       (lambda () ...))

3. A condition handler has three general classes of options of actions
   to take when it is passed a condition:

   . A handler may /decline/ to handle the condition by returning a
     condition, which will propagate to the next dynamically enclosing
     condition handler.

       ;;; Decline to handle the condition, but add on an annotation
       ;;; to any condition that passes through.

       (with-condition-handler
           (lambda (condition)
             (make-annotated-condition condition annotation))
         (lambda () ...))

   . A handler may /abort/ by performing an exiting control transfer
     outside the dynamic scope of its establishment, discarding all of
     the program state of the signaller.

       (define (file-exists? pathname)
         (call-with-current-continuation
           (lambda (abort)
             (with-condition-handler
                 (lambda (condition)
                   (if (file-unreachable-error? condition)
                       (abort #f)
                       ;; There may be other serious conditions, such
                       ;; as heap exhaustion, which are unrelated to
                       ;; whether the file exists, so decline to
                       ;; handle anything that is not directly
                       ;; related to our task at hand.
                       condition))
               (lambda ()
                 (close-input-port (open-input-file pathname))
                 #t)))))

   . A handler may /recover/ by the use of some recovery protocol,
     akin to Common Lisp or Dylan's restart systems. This entails a
     non-local control transfer, and may imply returning a value from
     the condition signalling procedure (SIGNAL-CONDITION, or what
     R6RS calls RAISE), but the condition handler should not know how
     the recovery works; a condition handler ought not to be able to
     distinguish two recovery paths identified by the same name that
     are implemented with different approaches.

This design has a number of important implications:

- The signaller can ensure that, even if all handlers decline to
  handle the condition, some action will be taken as a last resort;
  see, for instance, the example above of signalling warnings, which
  we want to report somewhere even if all handlers decline to handle
  the warning.

    (define (warning message . irritants)
      (signal-condition (make-simple-warning message irritants)
        (lambda (condition) ;Default handler
          condition ;ignore
          (report-warning (standard-error-port) message irritants))))

  An example of a handler that does not decline to handle a warning is
  a graphical interaction system, where the warning could be presented
  as a pop-up window. In this case, the handler may also wish to
  continue using a recovery protocol, so that the warning is not
  reported redundantly.

    (define (with-warnings-to-window body)
      (with-condition-handler
          (lambda (condition)
            (if (warning? condition)
                (pop-up-warning-window (warning-message condition)
                                       (warning-irritants condition))
                condition))
        body))

- The condition signaller always has the power to decide whether
  control may return to it; no condition handler may disagree, except
  by semantics-violating features in a debugger. The default is that
  control may not return to the signaller, which is what most programs
  expect. Returning control to the signaller is the exception, not
  the rule, and since we're discussing the fundamental mechanism of
  error reporting, we don't want to permit errors to creep into the
  system caused by returning control to the signaller when it least
  expected.

- Condition handlers are safe from the kind of recursive lossage that
  the original formal comment warned against.

- Only a constant amount of stack space separates the handler from the
  signaller. This helps to avoid potential stack space shortage
  issues, and it also helps to remove clutter from stack traces, which
  are almost always computed from the continuation of a condition
  handler (a debugger, again, being the most common example of a
  condition handler).

- Returning a new condition allows handlers to wrap more information
  around conditions; for instance, if we are implementing, say, a
  networked file system, and the network layer signals a NETWORK-I/O
  condition, the file layer may wrap it in a FILE-I/O condition for
  the next condition handler to see. (This is not different from
  R6RS, although it is different from Scheme48's old condition
  system.)

- This does not preclude a recovery protocol, and does not pretend to
  be one either.

(I'm not subscribed to the list, so please cc me in any further
replies.)

----- End forwarded message -----

-- 
They tried to pierce your heart                 John Cowan
with a Morgul-knife that remains in the         http://www.ccil.org/~cowan
wound.  If they had succeeded, you would
become a wraith under the domination of the Dark Lord.         --Gandalf
Received on Sat Mar 17 2007 - 19:40:41 UTC

This archive was generated by hypermail 2.3.0 : Wed Oct 23 2024 - 09:15:01 UTC