[comp.lang.scheme] REPOST : Exception handling - how to define it ?

bevan@cs.man.ac.uk (Stephen J Bevan) (04/05/91)

[I originally posted this on the 29th of March, but I've a feeling it
never made it off my local site.  If it did, then I appologise for
wasting time (and money) with a question nobody seems to be able to
answer]

What is the general style used to write exceptions and their handlers
in Scheme?  By exceptions I mean facilities similar to catch/throw in
Lisp.  The ways I can see of doing it are :-

1) Pass a success and fail continuation to every function.

2) Pass multiple continuations, one for each error to be handled.

3) Call a function that is assumed to be set by the user e.g.
 
     (define (foo x y z)
       ...
       (if (some-unexpected-error)
         (unexpected-error-exception args) ...))

   where `unexpected-end-of-file-exception' as a default just aborts.
   It would be up the user to re-define this as appropriate.  I guess
   the easiest way of doing this would be via fluid-let e.g. :-

      (fluid-let ((unexpected-error-exception
                    (lambda x (do-something-sensible x))))
        (foo an-arg another-arg yet-another-arg))

   However, as fluid-let isn't part of the standard (as far as I'm
   aware), this solution isn't portable.

So some questions :-

* Are there better mechanisms that those above?
* Any opinons as to which is the best?
* Am I totally off course trying to use exceptions in Scheme?

All answers greatefully received.

Stephen J. Bevan			bevan@cs.man.ac.uk

gyro@cymbal.reasoning.COM (Scott Layson Burson) (04/09/91)

   Date: 5 Apr 91 08:03:48 GMT
   From: Stephen J Bevan <bevan@cs.man.ac.uk>

   What is the general style used to write exceptions and their handlers
   in Scheme?  By exceptions I mean facilities similar to catch/throw in
   Lisp.  The ways I can see of doing it are :-

I don't know what most people do, but:

   1) Pass a success and fail continuation to every function.

   2) Pass multiple continuations, one for each error to be handled.

I have been curious for some time about the style that would result from
passing explicit exception continuations to every function (letting the
default continuation serve for the normal case).  One would think that
this would clutter the code unacceptably, but I wonder if there might
not be some way to mitigate or manage the clutter.  So I have had it in
the back of my mind for some time to attempt the experiment of writing a
program in this style and seeing how it worked out.

How might the clutter be dealt with?  Well, consider that 1) it would
consist of numerous additional arguments being passed to functions, and
2) Scheme has a very powerful mechanism, viz. lambda-abstraction, for
encapsulating argument passing.  So, for instance, imagine that CAR took
a second argument which would be invoked if the object passed to CAR
were not a cons.  Then one could do something like

(define (foo [...args...] fail)
  (let ((car (lambda (x) (car x fail))))
     ... (car something) ...))

(One might well prefer giving the two-argument CAR a different name,
e.g., CAR-GEN, where "gen" might mean "general" and/or "generator".)

Would this really work in practice, for a program of substantial size
and complexity?  I don't know.  But perhaps you can see why I'm curious
about the possibility.

But I'm not really recommending this, both because it's experimental and
because to do it with reasonable efficiency would probably require
access to the internals of one's Scheme implementation.

   3) Call a function that is assumed to be set by the user e.g.

	(define (foo x y z)
	  ...
	  (if (some-unexpected-error)
	    (unexpected-error-exception args) ...))

      where `unexpected-end-of-file-exception' as a default just aborts.
      It would be up the user to re-define this as appropriate.  I guess
      the easiest way of doing this would be via fluid-let e.g. :-

	 (fluid-let ((unexpected-error-exception
		       (lambda x (do-something-sensible x))))
	   (foo an-arg another-arg yet-another-arg))

      However, as fluid-let isn't part of the standard (as far as I'm
      aware), this solution isn't portable.

This approach is equivalent to the built-in exception systems of
Zetalisp and (New) Common Lisp.  If you're in a situation where you need
to write a Scheme program of substantial size and production quality and
are willing to limit yourself to Schemes that support FLUID-LET, I would
recommend you do it this way.

If you can't use FLUID-LET, however, I don't know what to suggest.

-- Scott

pavel@parc.xerox.COM (Pavel Curtis) (04/09/91)

I believe that there is no standard way to define and use exceptions in
portable Scheme programs, so you may as well use any of your proposed
mechanisms.  I don't think that you'll be particularly pleased with any of
those solutions in the long run, however, on the ground of either performance,
maintainability, or flexibility.

We have a mechanism in SchemeXerox that I will (at some appropriate point)
propose for inclusion in R5RS; it works as follows (this is quoted from the
current SchemeXerox reference manual):

6.15. Signalling and handling errors

An exceptional condition is a situation that technicaly falls within the
contract of a system, but is not considered part of the system's normal
functioning.  For example, the procedure READ normally returns a datum parsed
from a port, but also has defined behavior when the input is not syntactically
correct (according to the Revised^4 Report, it ``signals an error'').  This
proposal presents a means for systems to report the occurrence of such
exceptional conditions and to notice and recover from such reports.

A signal is a Scheme value representing a particular exceptional condition; its
precise semantics is a part of the contract of the signaller, the system
reporting the condition.  For modularity reasons, we expect that most signals
will be instances of distinct, application-specific, opaque types; this allows
handlers to recognize unambiguously the meaning of a particular signal, and
signallers to control the capability to raise that signal.

A handler is a procedure of one argument, a signal currently being raised.  In
broad terms, a handler has only two choices in dealing with each signal raised:

1.	The handler may accept the signal, taking full responsibility for
recovering from the exceptional condition.  This is usually accomplished by
invoking a continuation captured before the handler was enabled.

2.	The handler may decline the signal, refusing to take final
responsibility for recovering from the exceptional condition.  This is
accomplished simply by returning from the handler.

Every SchemeXerox thread maintains a list of currently-active handlers.  When a
signal is raised, each handler on the list is applied to it in turn,
most-recently-activated first, in the dynamic environment of the signaller.  If
all of the handlers decline the signal, then some unspecified action takes
place; the debugger might be entered (if one is available), the thread might be
frozen (awaiting a remote debugger), etc.  We expect this to be well-defined
for any particular set of circumstances, but unspecified in general.

6.15.1. Raising and handling signals

The procedure WITH-HANDLER is the fundamental means for making a handler active
over some dynamic extent.  The new syntax HANDLER-CASE captures a particularly
common idiom.  The procedure RAISE is the fundamental means for raising
signals.  The procedure ERROR captures another common idiom.

with-handler handler thunk                                       [Procedure]

The given handler is made active during the application of thunk to zero
arguments.  Whenever control leaves the thunk, either normally or via explicit
invocation of a continuation, the handler is deactivated.  Whenever control
subsequently returns to the thunk, via invocation of a continuation created
there, the handler is reactivated.

handler-case expression (predicate (var) body) ...               [Syntax]

The given expression is evaluated and its results returned.  If a signal is
raised during the evaluation, each predicate in turn is evaluated (in the
dynamic environment of the signaller) and applied to the signal.  If the
application returns true, then the corresponding body is evaluated in the
dynamic environment of the handler-case form, with the corresponding var bound
to the signal; in this case, the body's results are returned from the
handler-case form.

raise signal                                                     [Procedure]

The given signal is raised as described above.  This procedure does not return.

error format-string arg1 arg2 ...                                [Procedure]

A convenience for signalling conditions that are not expected to be handled.
Equivalent to (raise (format #f format-string arg1 arg2 ...))

6.15.2. Predefined signals

Several types of conditions arise frequently in practice, so it is useful to
standardize their corresponding signals.  The following sections specify the
procedures available for manipulating these predefined signals.

... several more sections detailing the signals raised by various standard
procedures ...

	Pavel Curtis