gnu-arch-users
[Top][All Lists]
Advanced

[Date Prev][Date Next][Thread Prev][Thread Next][Date Index][Thread Index]

Re: [Gnu-arch-users] Nit


From: Tom Lord
Subject: Re: [Gnu-arch-users] Nit
Date: Tue, 21 Oct 2003 08:54:33 -0700 (PDT)



    > From: Robin Farine <address@hidden>
    > >>>>> "Tom" == Tom Lord <address@hidden> writes:

    > [I think you are trolling here but I cannot resist.]

FWIW, your post gave me two ideas for improvements to the error_t
mechanism and one for an improvement to Java.


    > The good thing with a library function raising an exception is that
    > while the call backtracks, intermediate callers that just ignore the
    > error won't hide it.

When you say "callers that just ignore the error" you mean callers
that don't put any code to check for the error, right?   You don't
mean callers that explicitly put in code to ignore the error?

If so, no, that's the _bad_ thing about a library function raising an
exception:

Consider that I have have a library, B, with a public function
specified like this:

        parsed_config
        B_fn (string user_name)

           Find the config file for the indicated user and
           return it in parsed form.

           May raise exceptions:

                no_such_user    The indicated user does not exist.
                ....


I dutifully write some code that uses the B library:


        try {

          cfg = B_fn (user);

        } except (no_such_user e) {

           user = prompt_for_correct_user_name ();
           retry;

        } ...


This works fine until, later, the implementation of B_fn is modified
to also call D_fn.    Perhaps D_fn is a logging function that tries to
send a message to the admin user.

In making the change to B_fn, the programmer makes a mistake.   He
doesn't notice that D_fn can throw a no_such_user exception (if the
admin account is missing) and just ignores that case.

The result is that my little retry loop receives and handles the
exception from D_fn, but it handles it quite incorrectly.

What _should_ have happened, of course, is that the system should
"somehow" know that B is ignoring that error from D_fn and
_abort_the_system_ (with clean-ups, if you like).   In a language like
Java, to get that effect, apparently every call will have to be
written (forgive my pseudo-java here):


        try {
            call ()
        } except (error_i_handle e) {
          handle
        } except (error_i_propogate e) {
          throw
        } except (any_error e) {
          abort
        }



    > My turn to troll a little bit. I find code that catch everything and
    > then re-throw (for cleanup or error logging) quite grotesque (observed
    > frequently in Java code, who knows why). 

But there's no getting around it (not for cleanups, necessarily --
just to turn unanticipated exceptions into aborts rather than
forwarding them to unsuspecting callers).

It seems very clear to me now that to write really robust code,
every call site has to be independently decorated with an explicit
enumeration of what's handled, what's forwarded, what (if anything) to
otherwise do before the inevitable abort.   The whole problem of
language or error-interface design is therefore to syntactically
optimize that enumeration.   It'd be nice to have short syntax for:

a) abort on _any_ error in this call
b) drop _all_ error codes from this call
c) forward _all_ error codes from this call

In Java, the "null" syntax -- what you get if you don't write any
explicit code -- gives you (c).

A better choice would have been (a) but perhaps extensions 
along the line of:

        foo ()
          exceptions are passed to a dynamically enclosing
          except clause (exactly as current code)

        foo ()!(default)
          all exceptions are caught and dropped, causing `default'
          to be evaluated and returned as the value of the 
          expression (case (b))

        foo:()
          all exceptions turn into aborts (case (a))

and then train programmers to use :() forms everywhere unless they are
certain they want to do otherwise.

Perhaps there should also be a "try:" construct which is like "try"
but automatically converts unhandled exceptions into aborts.

All of this _does_ suggest to me an improvement to the error_t
mechanism.  It should really be (though I'll change it further,
below):

        typedef char * error_t;

        error_t * ignore_errors;

With the rule that:

        foo (&err, ...)

            Invoke foo and store any error code in err.


        foo (0, ...)

            Invoke foo and abort on any error.


        foo (ignore_errors, ...)

            Invoke foo and return on errors, dropping the
            error code.

A really thorough library function doesn't come out any simpler than
in Java, of course:

        bar (error_t * caller_err, ...)
        {
          error_t err = 0;

          foo (&err, ...);

          if (is_error_i_handle (err))
            handle it;
          else if (is_error_i_forward)
            forward_error (caller_err, err);
          else
            invariant (!err);
        }

(With the understanding that `forward_err', if passed a NULL first 
argument, turns into an abort.)



The real differences between error codes like error_t and Java-like
exceptions are:

1) Exceptions perform non-local exits rather than normal returns.

   But I'll almost never want that in a language that lacks GC and,
   when I do, I can achieve the effect with setjmp/longjmp.
   I certainly don't want it to be the default in _any_ language.


2) Java exceptions exist in a class hierarchy and I can write a
   handler that will receive any instance of any subclass of a 
   given error type.

   But with error_t, I can achieve the same effect more flexibly,
   using functions.   It's not obvious to me either how often I'll
   really want to "subclass" error types or if, when I do want to,
   that I'll want a class hierarchy to express that (rather than some 
   other way to construct types).

3) Java exceptions can include arbitrary parameters.

   This, as far as I'm concerned, is the biggest thing missing from
   error_t mechanism, and I wonder if that can't be fixed?  (see
   below).




    > Some languages provide constructs, like C++ automatic objects,
    > that allow one to deal nicely with cleanup and object state
    > coherency in presence of exceptions. So yes, exception
    > mechanisms provide an elegant tool to deal with errors which
    > tend to be used as a hammer to fix flies on the wall.

I think that those cleanup mechanisms in C++ are just great
_when_you_intend_non_local_exits_.   But the example of B_fn, C_fn,
and D_fn illustrates that in the general case of error reporting, we
do not want non-local exits -- they violate abstraction barriers,
sometimes with drastic consequences.

If I'm writing a recursive tree-searching function, for example, then
sure -- non-local exits, clean-up mechanisms: great stuff.  As the
interface to errors from general-purpose library functions?  No way.

But about arbitrary parameters to exceptions:

Perhaps it should really be:

        struct error;
        typedef struct error * error_t;

        struct error_data_vtable
        {
          void (*free_data) (char * errname, void * data);
          void (*copy_data) (char * errname, void * data);
          /*
           * For sanity's sake, error data vtable functions
           * can't return errors.   They should simply abort
           * if they can not do their work.
           */
        };

        struct error_data
        {
          struct error_data_vtable * vtable;
          void * data;
        };

        struct error
        {
          char * errname;
          struct error_data;
        };

        extern error_t * ignore_errors;

with new errors defined by:

        error_t my_err_landshark = { "landshark attack!", };

Hmm.

-t




reply via email to

[Prev in Thread] Current Thread [Next in Thread]