How can a Perl force its caller to return? [duplic

2019-08-05 19:41发布

问题:

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.

回答1:

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. :)



回答2:

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.



回答3:

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



回答4:

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