David Grove wrote:
>
> This is what's scaring me about all this talk about exceptions...
> it can [...] make Perl into a "complainer language" belching up
> uncaught (don't care) exceptions forcing try/except blocks around
> every piece of IO or DB handling.

I respect David's concerns.  What's scaring me about all this talk is
that one person's "don't care" is another person's "be careful".  If
you want to ignore information about "unexpected behavior", you have
to decide what to ignore.  This applies whether return codes or
exceptions are being used to signal unexpected behavior, and whether
or not you want to ignore everything.

It is true that if you want to ignore everything (that is, you
really "don't care" ;-) then not checking return codes involves less
typing.  Seriously though, there are algorithmic contexts (such as a
group of functions passing back some data structure that can itself
signal failure, where failure is often expected) in which the use
of return codes is in fact more appropriate than the use of stack
unwinding flow control.  No one is denying that.

Peter Scott wrote, in respose to David Grove:
>
> Now steady on. No one is proposing getting rid of the normal way
> of doing it. We're just talking about beefing up another WTDI.
> There are situations in programs that have dozens or hundreds of
> lines of code which benefit from the exception model just as much
> as your example.

Yup.  What we're trying to work toward here is a consensus on the
appropriate syntax and semantics for Perl 6's support for the case
where one is in fact using stack unwinding to control flow, whether
or not Perl 6 itself uses stack unwinding for unexpected behavior.
It's a simple matter of, "What should C<eval> and C<die> look and
feel like in Perl 6?"

Independent of those issues, the idea is that you can get either
mechanism, using these dualities...


    Using Return Codes       |        Using Exceptions
-----------------------------+-------------------------------
                             |
                             |  use exceptions; use try;
                             | 
sub Alpha                    |  sub Alpha
{                            |  {
    my $rc = Beta();         |      try { Beta(); }
                             | 
    is_catch_error($rc)      |      catch is_catch_error
    and                      |
        handle_error($rc);   |         => { handle_error }
    }                        |      }
                             | 
sub Beta                     |  sub Beta
{                            |  {
    my $f = open $file;      |      return foo( open $file );
                             |      }
    is_error($f)             * 
    and                      * 
        return my_error($f); * 
                             * 
    return foo($f);          | 
    }                        | 

If you look at those examples closely, you'll notice that the non-
trivial syntactic differences are that the $rc you have to manage
manually (with ifs and returns per the *'d lines) becomes the $@
exception object that is managed automatically (via stack unwinding).
Also, since $@ is a priori out-of-band, you don't need my_error to
map found errors into out-of-band values for the result of Beta.

The semantic effect, though, is more subtle.  With return codes,
if you forget an is_error test after any call (i.e., the *'d
lines), you will continue incorrectly; with exceptions you will
fail correctly unless you explicitly add a catch clause.  With
C<use exceptions> and C<throw> (instead of C<return $error>)
automatic unwinding is on by default; without it, unwinding must
be manually propagated, and if you forget to do so, you drop an
unexpected behavior on the floor without any chance of being told
about it.  Some of us find that the latter approach gives us
indigestion; your heartburn may vary ;-)

Each style of programming has different benefits in different
applications.  It turns out that in many transaction-based
applications (such as those requiring concurrent consistency,
including http GET/POST via CGI to DB), when you can't satisfy a
precondition, you want to abort the transaction and tell the user
why, via some outer-level exception handler.  And, you want this
to work relatively safely across 1e6 lines of application code
running against 1e5 lines of product-line application framework.

In such a case, being able to say C<$precondition or throw $why>
beats the stuffing out of manually generating and propagating error
return codes throughout your application, and it plays well with
various database notions of an aborted/rollback transaction (via a
framework-based outer catch clause that invokes an abort/rollback
if unwinding).  Sometimes, considering only simplified canonical
examples does not in fact address the needs at hand.  Consider
this code instead (based on part of an application-level UI-layer
workflow form class):

    my $scene = DB::Scene->GetById($id);
    $scene->Can("Resolve") # Precondition.
        or throw "PSMC.21103: Can't resolve scene.", [ $scene ];
    my $title = $I->Markup("\@DB_Edit{D_$id,24}");
    $I->Widget("D_$id")->Value($scene ** "Title");
    my $resolve = $I->Parser->ParseChunk("\@EF_Resolver{R_$id}");
    $I->Widget("R_$id")->Options(Value => $scene ** "ResolveBy",
        Links => $scene->FieldParam(ResolveBy => "Links"));
    $resolve = $I->Parser->Interpret($resolve);

Sure, this is "method-farming" code, but I've got hundreds of printed
pages of this kind of stuff, involving hundreds of methods, coded
by multiple developers (not just me), and it actually works well.
I really don't want to have to check the result of every one of
those method invocations (including the **s and the intermediates
in -> chains, ~16 checks in those 9 lines) and return some sort of
error.  I want to get out of there if anything I call thinks anything
has gone wrong, unless I explicitly say otherwise.  I don't want to
tell the user that the engineering worksheet they see in their
client agent is correct, unless nothing went wrong generating the
worksheet.  I don't want to ignore the unexpected.  It is not the
case that I "don't care".

It may be that the style shown above is only appropriate if the
method invocations almost always succeed (or a precondition has
exposed a bug); we know it is not appropriate for all contexts.
But in the former context, when an unexpected behaviour (one I
haven't arranged to catch) happens, I want to be confident that
my application will tell me something is seriously awry, until
I decide otherwise and tell the source code how to stop telling
me about it.

If a transaction blows up for any reason that I don't explicitly
handle, then I want to be presented with a message like this:

     APP.1234: Can't resolve worksheet scenario.
     UIM.2345: Can't update Worksheet relationship.
     DBM.3456: Trouble processing SQL UPDATE clause.
     DBM.4567: Unable to write to Calculation table.
     IOM.5678: Can't open file ".../calculation.ndx".
     IOM.6789: File ".../calculation.ndx" not found.

rather than a message like this:

     Can't use an undefined value as a HASH reference at line 66.

or worse: no message at all: the incorrect appearance of success.

With structured exception handling that's relatively easy to obtain;
it's all managed automatically and is available via the @@ stack.
(And you can send throw-time stack tracebacks to the log files at
catch time, but not to the end user, if that's what you want to do.)

This is about Perl 6, TMTOWTDI, DWIM, one liners, simple scripts,
and (yes) large applications (for some value of large, such as some
of the largest web sites in existence today).  I just want to be
able to "use exceptions;" and "use try;" when I think that style
works better for the needs at hand, and I'd like the syntax and
semantics effected by those pragmas/modules to play well with the
kind of large applications that typically benefit from this approach.

All that "use exceptions" means is that core functions and operators
behave like this:

        if ($ok) { return $result } else { die $error }

instead of like this:

        if ($ok) { return $result } else { return $error }

All that "use try" means is that in addition to C<eval> and C<die>
you now have C<try> and C<throw>, and (bonus points) C<try> has
C<catch> and C<finally> clauses that look after many of the details
of structured exception handling that are otherwise exposed by the
lower-level C<eval> mechanism (such as correctly propagating
exceptions raised while handling exceptions in catch clauses,
while under the influence of a finally clause, as shown by test
/finally-5/ at http://www.avrasoft.com/perl6/try-tests.htm ).

Finally, the return code and the stack unwinding programming styles
can certainly be intermixed, often easily, by using the dualities
described above (and in the CONVERSION section of RFC 88).  RFC 88
also contains notes on how module authors can support both styles at
once, depending on the dynamic value of the use exceptions pragma,
should they want to.

We've gone to a lot of trouble in RFC 88 to not step on anyone's
toes.  Check it out, and you may find that RFC 88 is a friend,
not an adversary.  As http://www.avrasoft.com/perl6/rfc88.htm
says right up front:

- Nothing in this RFC impacts the tradition of simple Perl scripts.

- eval {die "Can't foo."}; print $@; continues to work as before.

- There is no need to use try, throw, catch, or finally at all, if
  one doesn't want to.

- This RFC does not require core Perl functions to use exceptions
  for signalling errors.

The one thing we don't want on this front in the design of Perl 6
is some half-baked concept of exception handling that (1) doesn't
work well in production, and (2) prevents the development of a
module-based mechanism that does work well.  "All this talk about
exceptions" is just work toward nailing down the structural details
of the -language layer, to provide a reasonable working model of
the community perspective to the good folks over at -internals.

Yours, &c, Tony Olekshy

Reply via email to