tags:

views:

186

answers:

4

Possible Duplicate:
Is it possible for a Perl subroutine to force its caller to return?

I want to write a subroutine which causes the caller to return under certain conditions. This is meant to be used as a shortcut for validating input to a function. What I have so far is:

sub needs($$) {
    my ($condition, $message) = @_;

    if (not $condition) {
        print "$message\n";
        # would like to return from the *parent* here
    }

    return $condition;
}

sub run_find {
    my $arg = shift @_;
    needs $arg, "arg required" or return;
    needs exists $lang{$arg}, "No such language: $arg" or return;

    # etc.
}

The advantage of returning from the caller in needs would then be to avoid having to write the repetitive or return inside run_find and similar functions.

+4  A: 

Sounds like you are re-inventing exception handling.

The needs function should not magically deduce its parent and interrupt the parent's control flow - that's bad manners. What if you add additional functions to the call chain, and you need to go back two or even three functions back? How can you determine this programmatically? Will the caller be expecting his or her function to return early? You should follow the principle of least surprise if you want to avoid bugs - and that means using exceptions to indicate that there is a problem, and having the caller decide how to deal with it:

use Carp;
use Try::Tiny;

sub run_find {
    my $arg = shift;
    defined $arg or croak "arg required";
    exists $lang{$arg} or croak "no such language: $arg"; 

    ...
}

sub parent {
    try { run_find('foo') }
    catch { print $@; }
} 

Any code inside of the try block is special: if something dies, the exception is caught and stored in $@. In this case, the catch block is executed, which prints the error to STDOUT and control flow continues as normal.

Disclaimer: exception handling in Perl is a pain. I recommend Try::Tiny, which protects against many common gotchas (and provides familiar try/catch semantics) and Exception::Class to quickly make exception objects so you can distinguish between Perl's errors and your own.

For validation of arguments, you might find it easier to use a CPAN module such as Params::Validate.

rjh
I get only error-messages if I write "catch { print $_; }" instead of "catch { print $@; }"
sid_com
The problem is that the caller of `run_find` is a module outside my control (Term::Shell, if you care), and that caller doesn't wrap my call in `eval`. Since I don't want to bring down the whole process, I have to exit normally from `run_find` and handle errors some other way.
JSBangs
+1  A: 

You may want to look at a similar recent question by kinopiko: Is it possible for a Perl subroutine to force its caller to return?

The executive summary for that is: best solution is to use exceptions (die/eval, Try::Tiny, etc...). You van also use GOTO and possibly Continuation::Escape

DVK
A: 

It doesn't make sense to do things this way; ironically, ya doesn't needs needs.

Here's why.

  • run_find is poorly written. If your first condition is true, you'll never test the second one since you'll have returned already.
  • The warn and die functions will provide you printing and/or exiting behavior anyway.

Here's how I would write your run_find sub if you wanted to terminate execution if your argument fails (renamed it to well_defined):

sub well_defined {

    my $arg = shift;
    $arg or die "arg required";
    exists $lang{$arg} or die "no such language: $arg";
    return 1;
}

There should be a way to return 0 and warn at the same time, but I'll need to play around with it a little more.

run_find can also be written to return 0 and the appropriate warn message if conditions are not met, and return 1 if they are (renamed to well_defined).

sub well_defined {

    my $arg = shift;
    $arg or warn "arg required" and return 0;
    exists $lang{$arg} or warn "no such language: $arg" and return 0;
    return 1;
}

This enables Boolean-esque behavior, as demonstrated below:

perform_calculation $arg if well_defined $arg; # executes only if well-defined
Zaid
How about `warn "no such language", return 0;` ?
Ether
@Ether : Tried your suggestion in a Perl one-liner, it doesn't `warn` properly: `perl -e "sub rf { $arg = shift; $arg or warn qq(No arg), return 0; return 1; } print qq(Yes) if rf ;"`. Replacing the `,` with `and` does work though.
Zaid
ah yes, I forgot that `warn` takes a list, so it will gobble up everything, unless we add in some parens to show where its args finish: `warn("no such language"), return 0;`
Ether
+4  A: 

I think you're focussing on the wrong thing here. I do this sort of thing with Data::Constraint, Brick, etc. and talk about this in Mastering Perl. With a little cleverness and thought about the structure of your program and the dynamic features that Perl has, you don't need such a regimented, procedural approach.

However, the first thing you need to figure out is what you really want to know in that calling subroutine. If you just want to know yes or no, it's pretty easy.

The problem with your needs is that you're thinking about calling it once for every condition, which forces you to use needs to control program flow. That's the wrong way to go. needs is only there to give you an answer. It's job is not to change program state. It becomes much less useful if you misuse it because some other calling subroutine might want to continue even if needs returns false. Call it once and let it return once. The calling subroutine uses the return value to decide what it should do.

The basic structure involves a table that you pass to needs. This is your validation profile.

sub run_find {
    my $arg = shift @_;
    return unless needs [
        [ sub { $arg }, "arg required" ],
        [ sub { exists $lang{$arg} }, "No such language: $arg" ],
        ];
    }
    ...
    }

You construct your table for whatever your requirements are. In needs you just process the table:

sub needs($$) {
    my ($table) = @_;

    foreach $test ( @$table ) {
        my( $sub, $message ) = @$test;
        unless( $sub->(...) ) {
            print $message;
            return
            }
        }

    return 1;
    }

Now, the really cool thing with this approach is that you don't have to know the table ahead of time. You can pull that from configuration or some other method. That also means that you can change the table dynamically. Now your code shrinks quite a bit:

sub run_find {
    my $arg = shift @_;
    return unless needs( $validators{run_find} );
    ...
    }

You cna keep going with this. In Mastering Perl I show a couple of solutions that completely remove that from the code and moves it into a configuration file. That is, you can change the business rules without changing the code.

Remember, almost any time that you are typing the same sequence of characters, you're probably doing it wrong. :)

brian d foy
+1 Gotta say that the table idea is just fantastic.
Zaid
Yeah, the table idea meets my needs. It doesn't require any wacky messing with scopes, but it still cuts down on the verbosity.
JSBangs