Beefy Boxes and Bandwidth Generously Provided by pair Networks
go ahead... be a heretic

the try/catch example from "Programming Perl" analyzed

by pemungkah (Priest)
on Aug 18, 2004 at 16:54 UTC ( [id://384038] : perlmeditation . print w/replies, xml ) Need Help??

The try/catch example in the Prototype section of Programming Perl often causes confusion among programmers who are trying to understand prototypes.
sub try (&$) { my($try, $catch) = @_; eval { $try }; if ($@) { local $_ = $@; &$catch; } } sub catch (&) { $_[0] }
This is invoked like this:
try { die "phooey"; } catch { /phooey/ and print "unphhoey\n"; }
The usual questions at this point are "WHAT?" and "How in the heck does that even work?" So this is an explication in detail. Let's summarize how subroutine calls work first, and then tackle the prototype stuff after.
Remember that when Perl calls a subroutine, it evaluates any expressions supplied as arguments first and then passes the values of those expressions as the arguments to the subroutine. So in this case, try() wants two expressions as its arguments: one is required to evaluate to a reference to a subroutine, and the other is required to evaluate to a scalar. On the other hand, catch() requires only one: some thing that evaluates to a subroutine reference.

Remember that an anonymous sub is actually an expression; it has the side effect of generating code that can be executed later, but it's actually just an expression that returns a scalar value: the reference that points to the generated code.

So how do the try() and catch() subs work, and why does the syntax shown actually compile?

Let's look at catch() first. It's just an identity function:

sub identity { return $_[0]; }
This returns whatever the first argument evaluated to as the value of the subroutine. This is exactly what catch() does, though it states it slightly more compactly, and uses a (&) prototype to constrain its argument to be a subroutine reference. So why is this useful, and why does it use an & prototype?

The & prototype, when it comes first, does two things:

  1. It allows you to pass an anonymous sub without the sub keyword in front
  2. it allows you to leave off the comma following this first argument
So catch()'s prototype says "Give me a sub reference somehow. An anonymous sub without the sub keyword will be fine, and I require that you give me no other arguments." So a catch() call could be any of the following:
sub example { /foo/ and print "foo detected"; } catch \&example; catch sub { /foo/ and print "foo detected"; }; catch { /foo/ and print "foo detected"; }
This last one is the one we'll be using for our example.

Now remember that catch() just returns its first argument as its value. All of these argument forms shown above evaluate to the same kind of thing: a reference to a subroutine. A reference to a subroutine is always a scalar, so a call to catch will be acceptable to try() as its second parameter (the prototype being &$: a sub reference, and a scalar).

So now we know we've got something that works for the second argument of try(). Let's look at the first one. This is &, so it can accept the same kinds of things that catch() did as its first parameter: an actual reference to a sub (like \&example), an anonymous sub ( sub { ... }), or a "headless" anonymous sub (like { ... }).

Remember that the second option you have when using & as the first argument prototype lets you leave off the comma as well as the sub, so the total call looks like this:

try { # some code that dies } catch { # pattern match against $_ for die string }
Which is really, if you wrote it all out in the standard sub call syntax:
try( sub{ # code that dies }, catch(sub { # code that checks vs. $_ } ) );
So. To call try(), Perl will have to evaluate any expressions in its argument list and then pass them in.

The first argument, when evaluated, yields a reference to some code to be called. This is the first (headless ) anonymous sub, with no trailing comma.

The second argumentis a call to catch() that has to be evaluated. The catch() call, as mentioned before, simply returns its argument to the caller. Its argument is an anonymous sub, so the returned value is a reference to this (unevaluated) sub. This is a scalar, so it matches the $ in the &$ prototype.

Now we have the arguments to try(): the expected subroutine reference (the & in &$) from the code block right after the try() sub name, and the reference to the anonymous sub returned by catch() (the $ in &$). We're ready to call try() at this point.

Inside try(), we eval the first anonymous sub. Since it's inside an eval block, the code does whatever it does and (possibly) sets $@ if it dies without further disturbing the flow of control. If $@ is set after the eval, we localize $_ (this creates a new global variable that we'll reference via $_ during this block) and set it to $@. Then we just call the second anonymous sub, which expects to see the contents of $@ in $_, where we've conveniently just put them. Since this is inside the block that localized $_, we sill get the "new" $_. This code does whatever it wants to do, then returns. The block ends, and $_ is restored to whatever value it had previous to the if ($@) block, and try() exits.

So the concepts here that are important are:

  • subroutines evaluate their arguments before they are called.
  • sub { $_[0] } returns its first argument's value as its value when it is called.
  • sub {} is an expression, returning a reference to an nonymous sub while compiling the code at the point where the sub block is evaluated
  • local() defines a new copy of a variable, and blocks called by this one get the new copy when referring to the variable. When the block localizing the variable ends, the old variable is restored.
  • a leading & in a prototype lets you leave off the "sub" in an anonymous sub definition, and lets you leave off the comma following the block too.
Edited 8/04: errant "*" removed in try() prototype

Edited 8/09: Typos corrected

Replies are listed 'Best First'.
Re: the try/catch example from "Programming Perl" analyzed
by perrin (Chancellor) on Aug 18, 2004 at 17:09 UTC
    Be very careful with coderef prototypes. This code is subject to all of the same problems that have been documented with the Error module. I wrote about these here.
Re: the try/catch example from "Programming Perl" analyzed
by hardburn (Abbot) on Aug 18, 2004 at 17:59 UTC

    On a tangently related point, I'm rather unsatisfied with the idioms for OO exception handling in Perl.

    With Java exceptions, you are guarenteed that any caught exception is an object (one place where Java's nearly-pure OO environment comes in handy). In Perl, you also have to handle the condition of a runtime error which is not an object:

    eval { do_something_that_could_die() }; if($@) { # We know it died, but is it an object or a # printable message? if( ref $@ ) { # Assume it's an exception object. Not a # great way to do it, but it works. # # Now, what kind of exception is it? # if( $@->isa( 'IOException' ) ) { . . . } elsif( $@->isa( 'OtherException' ) ) { . . . } else { . . . } } else { # Assume it's an error string . . . } }

    If you're good aboug indenting your code, then this produces an extra level of indentation (and an extra case) that Java doesn't have. Java does have an extra level by nature of enclosing everything in a class declaration, but this is compensated by using 4-space indent, whereas most Perl code can reasonably sit in an 8-space indent. (Of course, I'm now sitting dangerously close to a coding-style flame war.)

    Thank God for folding editors, or this would be a huge blight to see in the middle of a subroutine.

    "There is no shame in being self-taught, only in not trying to learn in the first place." -- Atrus, Myst: The Book of D'ni.

      Don't use ref - use blessed. Now, you can be guaranteed it's an exception object, insofar as you can guarantee that all code you would want to call in an eval/$@ block would propagate exception objects if they propagate objects at all.

      We are the carpenters and bricklayers of the Information Age.

      Then there are Damian modules.... *sigh* ... that's not about being less-lazy -- that's about being on some really good drugs -- you know, there is no spoon. - flyingmoose

      I shouldn't have to say this, but any code, unless otherwise stated, is untested

        Well, that helps a bit with the implementation, but doesn't solve the fundamental problem of having to handle the non-blessed case. Also, you now have to pull in another module to check for blessedness.

        "There is no shame in being self-taught, only in not trying to learn in the first place." -- Atrus, Myst: The Book of D'ni.

      In Perl, you also have to handle the condition of a runtime error which is not an object

      I believe that the try block is where the memory leak issues usually show up, since that is where the bulk of your working code is. But why throw catch away too?

      sub catch (&) { if ($@) { my ($catch) = @_; my $e = $@; $e = My::Base::Exception->new($@) unless (ref($@) && UNIVERSAL::isa($@, "My::Base::Exception +")); $catch->($e); } } eval { die "test"; }; catch { my $e = shift; if ($e->isa('IOException')) { # ... } else { print $e; } };
      I know chromatic would be upset that I am using UNIVERSAL::isa, but it will avoid your problem with using Scalar::Util::blessed, and as long as you don't use any other classes that override isa you should be okay.

      In Perl, you also have to handle the condition of a runtime error which is not an object:

      You could always encapsulate the extra test in some code:

      sub exception (;$) { my $wanted_exception = shift; return unless $@; return 1 unless defined $wanted_exception; return ref($@) && $@->isa( $wanted_exception ) };

      allowing you to do:

      eval { $coderef->() }; if ( exception 'IOException' ) { ... handle IOException ... } elsif ( exception 'OtherException' ) { ... handle OtherException ... } elsif ( exception ) { ... handle all other exceptions ... } else { ... we lived ... };

      Sure, you have an extra ref test and subroutine call, but since exceptions should be... well... exceptional it shouldn't impact your runtime speed much.

      Not sure that the nested if is actually needed. This code works with string and object exceptions (on 5.8.3 at least):
      use strict; use warnings; package Exception; package IOException; @IOException::ISA = qw(Exception); package OtherException; package YetAnotherException; package main; *isa = \&UNIVERSAL::isa; my @grisly = ( sub { die "AARRGH!\n" }, sub { die bless [], 'Exception' }, sub { die bless [], 'IOException' }, sub { die bless [], 'OtherException' }, sub { die bless [], 'YetAnotherException' }, sub { print "I live\n"; } ); sub do_something_that_could_die { $grisly[0]->(); # or int(rand(@grisly)) } eval { do_something_that_could_die(); }; if(isa($@, 'Exception')) { print ref($@), ' caught'; } elsif(isa($@, 'OtherException')) { print ref($@), ' caught'; } elsif(ref $@) { # handle any other exception object print ref($@), ' caught'; } elsif($@) { # handle non-object exception print $@; }
      replaced $@->isa('package') calls with calls to UNIVERSAL::isa()

        As long as you are sure you are going to get an object. You may want to use the functional UNIVERSAL::isa() if you want to avoid your 'catch' crapping out when something does just a plain die $!


      I have yet to find a case in which I would want to throw an object. Besides all exception objects are supposed to be stringifiable AFAIK.

      Besides Java code doing the same thing is usualy several times longer than its Perl conterpart so I don't think these three or four additional lines matter. That is unless you program Java in perl.

      Always code as if the guy who ends up maintaining your code will be a violent psychopath who knows where you live.
         -- Rick Osborne

        I have yet to find a case in which I would want to throw an object.

        I have. Specifically, I want to do different things for different errors, and extract information from it. For instance, Exception::Class provides full information about the uid/gid, package, file, line number, stacktrace, pid, and so on. Sure, it'd be possible to encode all that into a single string, but the extraction process isn't as easy. Further, you might want to provide some information to the user, and send more details to the developer. It's much easier (and safer) to do that with an object than trying to munge a string.

        Besides Java code doing the same thing is usualy several times longer than its Perl conterpart so I don't think these three or four additional lines matter. That is unless you program Java in perl.

        It's not just the extra lines, but the extra level of indentation. Like I said, Java programmers typically operate with 4-space indents, because they know they're going to take a lot more space for indentation than other langs (and also because the identifiers tend to be much longer). However, most Perl programmers can easily get away with 8-space indents. As such, every extra level of indentation is a big deal.

        Other people have given solutions which get rid of that extra level. My prefered is to place the string into an exception object and then handle it like any other exception..

        "There is no shame in being self-taught, only in not trying to learn in the first place." -- Atrus, Myst: The Book of D'ni.

Re: the try/catch example from "Programming Perl" analyzed
by trammell (Priest) on Aug 18, 2004 at 21:21 UTC
    Code should read:
    sub try (&$) { ...
    In my version of Programming Perl (3rd Ed.) this is on pp. 227-8.
Re: the try/catch example from "Programming Perl" analyzed
by ambrus (Abbot) on Aug 23, 2004 at 10:09 UTC

    As trammell has noted, the prototype of try should be (&$), not (*&$), I guess that's only a typo but it has confused me very much when I've read it.

    Also, that code is also listed in perldoc perlsub (of perl 5.8.2), I'm not sure which one copied from the other.

    From perlsub:

    The interesting thing about "&" is that you can generate new syntax with it, pro- vided it's in the initial position:

    sub try (&@) { my($try,$catch) = @_; eval { &$try }; if ($@) { local $_ = $@; &$catch; } } sub catch (&) { $_[0] } try { die "phooey"; } catch { /phooey/ and print "unphooey\n"; };

    That prints "unphooey". (Yes, there are still unresolved issues having to do with visibility of @_. I'm ignoring that question for the moment.

      Thank you both; I've fixed that typo.