tags:

views:

137

answers:

4

If I have Perl module like

 package X;

and an object like

 my $x = X->new ();

Inside X.pm, I write an error handler for $x called handle_error, and I call it

 sub check_size
 {
     if ($x->{size} > 1000) {
         $x->handle_error ();
         return;
     }
 }

Is there any way to make handle_error force the return from its caller routine? In other words, in this example, can I make handle_error do return in check_size without actually writing return there?

+4  A: 

You can use goto &NAME, your return from the error handler will return to the point where check_size was called.

sub check_size { my $x = shift; # you don't say where the $x comes fro in X.pm.
# I assume it is the invocant.

if( $x->{size} > 1000 ) {
    my $sub = $x->can('handle_error');
    goto $sub;
}

}

This works because goto &NAME transfers control to the called function without creating a new stack frame.

I use can to get a reference to the handle_error for $x, so that the method will work properly with subclasses that override handle_error.

This design seems like a bad idea to me, though.

Perhaps this is a good place to use exceptions:

use Try::Tiny;
my $x = X->new();

try   {  $x->check_size }
catch {  $x->handle_error };
daotoad
Thanks for the answer. But I want to force this inside `handle_error` rather than in the caller. Is there any way to do that? Maybe not?
Kinopiko
+10  A: 

The only reasonable way to backtrack multiple levels up the call stack is by throwing an exception/dying.

That said, you really should reconsider what you want to do. A programmer (including yourself, six months from now) will expect that, when a function call completes, the statement after it will execute (unless an exception is thrown). Violating that expectation will cause bugs which are caused in handle_error, but appear to be with the code which called handle_error, making them extremely difficult to debug. This is Not A Good Thing.

You're also making the assumption that there is absolutely no situation in which continuing after an error is handled will be appropriate. Hardcoding an assumption like that is practically a sure guarantee that, as soon as you've had time to forget it, you'll come across a case where you need to continue after calling handle_error (and then waste huge amounts of time trying to figure out why the code after handle_error doesn't get run).

And then there's the assumption that you'll always want to skip back exactly two levels in the call stack. That's another assumption that will fail as soon as it's hardcoded. Not only will there be cases where the calling code should continue, there will also be cases where you need to go three levels up the call stack.

So just have handle_error exit by calling die instead of return and trap the exception at the appropriate level where execution should continue. You don't know every place where the sub will ever be called, so you can't predict how many levels back it will need to go.

In the code at hand, if the extra line to just say return is bothering you, you can use return $x->handle_error; You can even get rid of the enclosing scope and make it return $x->handle_error if $x->{size} > 1000; There - three lines removed rather than just one, plus a pair of braces and two pairs of parentheses as a free bonus.

Finally, I would also suggest changing the name of handle_error to better reflect what it actually does. (report_error, maybe?) "Handling an error" usually means cleaning things up to resolve the error so that execution to continue. If you want your handle_error to prevent the code which called it from continuing, then it seems very unlikely that it's cleaning things up to make continuation possible and, once again, it will cause nasty, hard-to-debug surprises for future programmers using this code.

Dave Sherohman
I would like to know if there is any way to do this, even an unreasonable way.
Kinopiko
@Kinopiko, you might be able to do it with some crazy XS code. But IMO, this design sounds like walking barefoot over broken glass to get to a gun so that you can shoot yourself in the foot. But what the heck? They're your feet.
daotoad
A: 

You could use Continuation::Escape. This would basically allow you to pass the "point-of-return" on to the error handler.

phaylon
+1  A: 

The answer to your question is extremely difficult, and I'm not even going to field it because its a poor solution to your real problem. What's the real problem? The problem is that you want an error inside a deeply nested subroutine call to bubble up the stack. This is what exceptions are for.

Here's your code rewritten to throw an exception using croak.

package X;

sub new {
    my $class = shift;
    my %args = @_;
    my $obj = bless \%args, $class;

    $obj->check_size;

    return $obj;
}

my $Max_Size = 1000;
sub check_size
{
    my $self = shift;

    if ($self->{size} > $Max_Size) {
        croak "size $self->{size} is too large, a maximum of $Max_Size is allowed";
   }
}

Then when the user creates an invalid object...

my $obj = X->new( size => 1234 );

check_size dies and throws its exception up the stack. If the user does nothing to stop it, they get an error message "size 1234 is too large, a maximum of 1000 is allowed at somefile line 234". croak makes sure the error message happens at the point where new is called, where the user made the error, not somewhere deep inside X.pm.

Or they can write the new() in an eval BLOCK to trap the error.

my $obj = eval { X->new( size => 1234 ) } or do {
    ...something if the object isn't created...
};

If you want to do something more when an error occurs, you can wrap croak in a method call.

sub error {
    my $self = shift;
    my $error = shift;

    # Leaving log_error unwritten.
    $self->log_error($error);
    croak $error;
}

my $Max_Size = 1000;
sub check_size
{
    my $self = shift;

    if ($self->{size} > $Max_Size) {
        $self->error("size $self->{size} is too large, a maximum of $Max_Size is allowed");
   }
}

The exception from croak will bubble up the stack through error, check_size and new.

As daotoad points out, Try::Tiny is a better exception handler than a straight eval BLOCK.

See http://stackoverflow.com/questions/1498042/should-a-perl-constructor-return-an-undef-or-a-invalid-object/2860066#2860066 for more reasons why exceptions are a good idea.

Schwern