Tony Olekshy writes:
: In Apocalypse 4, Larry Wall wrote:
: |
: |   In fact, a C<CATCH> of the form:
: |
: |       CATCH {
: |           when xxx { ... }          # 1st case
: |           when yyy { ... }          # 2nd case
: |           ...                       # other cases, maybe a default
: |       }
: |
: |    means something vaguely like:
: |
: |       BEGIN {
: |           %MY.catcher = {
: |               given current_exception() -> $! {
: |
: |                   when xxx { ... }  # 1st case from above
: |                   when yyy { ... }  # 2nd case from above
: |                   ...               # other cases, maybe a default
: |
: |                   die;            # rethrow $! as implicit default
: |               }
: |               $!.markclean;       # handled cleanly, in theory
: |           }
: |       }
: 
: Beautiful. The synthesis of CATCH, BEGIN blocks, %MY, given, when,
: break, dwim =~, die, $!, $!.clean, and $!.stack is awe-inspiring.
: The way proto-exceptions, fail, and use fatal work together is also
: brilliant.

Thanks.  I'm the proof that nearly anyone can be brilliant if they
don't have to do it in real-time.  :-)

: I particularly enjoyed this one:
: 
:        CATCH { when @$! =~ Foo { ... } }

It was actually in trying to translate that very example that I
realized that most of the arrays in the =~ table had an implied any()
around them (just as all the lists have an implied any() around them).
It hadn't occurred to me before then that Damian's special "array
intersection" dwim was the same as any(@foo) =~ any(@bar).  From
there it was easy to generalize that

    @foo =~ Class

really means

    any(@foo) =~ Class

So, thanks!

: I do have a few questions.
: 
:    1. Does this example:
: 
:       {
:           my $p = P.new;   LAST { $p and $p.Done; }
:           foo();
:           my $q = Q.new;   LAST { $q and $q.Done; }
:           ...
:       }
: 
:       effectively get compiled into something like:
: 
:       {
:           my $p;  my $q;
:           $p = P.new;   LAST { $p and $p.Done; }
:           foo();
:           $q = Q.new;   LAST { $q and $q.Done; }
:           ...
:       }
: 
:       If not, how can we evaluate $q in the LAST block if foo() dies?
:       Or are LASTs not handled by a magic BEGIN mechanism? Or are the
:       LASTs converted into a BEGIN plus some run-time state variable
:       that is only set when the LAST is encountered during execution?
:       Or am I missing the point entirely ;-?

All the LASTs run always.  The compiler has to take responsibility one
way or another to make sure unelaborated variables still evaluate to
undefined.  It can, for instance, hoist the allocation to the front,
"something like" your example.  It's not something that's possible to
write an example of in Perl, since you can't separate the allocation
from the name introduction in Perl.  Maybe with two pseudo-ops indicating
the two aspects of "my":

      {
          myalloc $p;  myalloc $q;
          myintro $p = P.new;   LAST { $p and $p.Done; }
          foo();
          myintro $q = Q.new;   LAST { $q and $q.Done; }
          ...
      }

Alternately, the compiler can keep track of how far the elaboration
has proceeded, and compile down to something like:

      {
          my $p = P.new;   LAST { maybe("$p") and maybe("$p").Done; }
          foo();
          my $q = Q.new;   LAST { maybe("$q") and maybe("$q").Done; }
          ...
      }

where maybe() is smart enough to know whether the variable in question
has been allocated.  As yet another way, the code searching for the LAST
to execute could make sure any unallocated variables get allocated before
calling the LAST block, much like

    {
        goto X;
        my $p = 2;
        X: print $p;
    }

would need to make sure that $p gets created "en passant" if it were not
already created before the goto.

I expect the first way is the easiest, though perhaps it's not the most
efficient way for non-exceptional code.

:    2. Consider the following example:
: 
:       for my $file ( @files ) {

That's obsolete syntax, by the way...

:           my $f is last { close } = open $file or next;
:           foo($f);
:           CATCH { default { print "foo($f) failed\n" } }
:           }
: 
:       The last and CATCH blocks must be invoked at the end of each
:       time around the for block, no? Or should I be writing:
: 
:       for my $file ( @files ) {
:           try {
:               my $f is last { close } = open $file or next;
:               foo($f);
:               CATCH { default { print "foo($f) failed\n" } }
:               }
:           }

That would be better if you want any exception to go to the next file.
Alternately, and perhaps more clearly, you could write:

    for @files -> $file {
        my $f = open $file or die;
        foo($f);
        CATCH {
          default { print "foo($f) failed\n"; next }
        }
        NEXT { $f.close }
    }

or equivalently

    for @files -> $file {
        my $f is next { .close } = open $file or die;
        foo($f);
        CATCH {
          default { print "foo($f) failed\n"; next; }
        }
    }

:    3. Would the following execute the C<die>? When do I have to worry
:       about "accidentally" catching control exceptions?
: 
:       sub ...
:       {
:           return if 1;
:           fragile();
:           CATCH { default { die "Couldn't fragile." } }
:           }

Yes, that would execute C<die> in the current scheme of things.  Apo 4
says that you should write

    CATCH { when X::Kaos { die "Couldn't fragile." } }

if that's what you mean, but that's kind of a joke, in at least two senses.

I can see several ways out of it, most of which are messy.  Perhaps the
cleanest solution is to say that CATCH {} only catches non-control
exceptions, and CONTROL {} catches control exceptions.  Or perhaps
CONTROL catches everything, and CATCH has an implied

    when X::Control { die }

at the top.

:    4. The test for "block exited successfully" is C< !$! || $!.clean >,
:       for the purposes of the block-end handing code, correct?  So
: 
:           KEEP is like LAST { if ( !$! || $!.clean ) { ... } }
:       and
:           UNDO is like LAST { unless ( !$! || $!.clean ) { ... } }
: 
:       in which case CATCH is actually like UNDO with an implied given,
:       die, and $!.markclean, except it's handled in a different end-
:       block order, yes?

Yes, something like that.  I don't know the exact success formula yet.

:    5. What is the order of processing all these special blocks at the
:       end of their containing block? Is it:
: 
:           1. CONTINUE

You mean NEXT.

:           2. CATCH
:           3. KEEP
:           4. UNDO
:           5. LAST
:           6. POST
: 
:       or some other fixed order, or is there some sort of order-of-
:       encounter interleaving of some of the kinds of blocks?

At the moment, I see this:

    -2. PRE                     in order, inherited, no side effects
    -1. FIRST                   in order
     0. inline code             normal flow
     1. CATCH, CONTROL          singular
     2. NEXT, LAST, KEEP, UNDO  in reverse order
     3. POST                    in reverse order, inherited, no side effects

NEXT has to follow CATCH because the CATCH might invoke C<next>, as in
the earlier examples.  I don't see an reason to split out all the #2
blocks to separate levels, since they're all "clean up" variants.

We do need to figure out whether KEEP and UNDO are simply LAST variants or
if they also fire off when NEXT fires off.  I can argue it both ways.

:    6. What is the value of
: 
:           my $x = try { "1" CATCH { default { "2" } } LAST { "3" } };
: 
:       What happens for each permutation of replacing "n" by die "n"?

$x must get "1".  There can be an explicit way for CATCH or LAST to set
the return value, but I think we can't have the final, usually
accidental, value of a CATCH or LAST magically becoming the return
value.  And I don't think the explicit way is with C<return>, since
we're talking about the final value of a block that perhaps doesn't
handle (or has perhaps already handled!) a C<return>.  Might be a good
spot for C<yield>, which even in its projected use an an iterator
"return" implies that we're providing the return value without wanting
to influence the block's native control flow.  So maybe that would
work.  But a real C<yield> may have extra hidden control-flow garbage
underneath that would preclude this.  Could always use "result" or some
such instead.

If you replace them all with dies, the die "1" fires off, is caught by
the CATCH, which fires off die "2".  The die "3" is always fired off on
the way out of the block because that's what LAST does.  None of the
information is thrown away, because die "2" bypasses the C<markclean>.
So we end up with a single unclean exception propagating outward whose
@$! stack contains all three exceptions.

:    7. Is there any particular reason why multiple CATCH blocks can't
:       simply be queued in some fashion like multiple LAST blocks?

We could certainly allow more than one if it were just syntax, but in
terms of semantics CATCH blocks are not like LAST blocks.  In general,
most LAST blocks should not have much interaction with each other.  But
multiple CATCH blocks would definitely have a lot of interaction where
some cases hide other cases, in some non-obvious fashion.  If you're
going to scatter pieces of a switch statement all over, it's going to
be non-obvious, even if the rules are straightforward.  It seems better
to me to force all the cases into a single CATCH (or CONTROL?), where
the hiding of latter cases by earlier ones is at least visually obvious.

Larry

Reply via email to