Using a scalar as a condition in perl

2019-06-22 13:06发布

问题:

First timer...so let me know if there is anything that I have not paid attention to whilst posing a question.

The question is how to use a scalar as a condition, as the code below does not work.

my @parameter=('hub');

my %condition;
$condition{'hub'}{'1'}='$degree>=5';

foreach (@parameter) {
       if ($condition{$_}{'1'}) {..}
}

I thought that is because the condition is not interpreted correctly, so I also tried the following, which also did not work.

if ("$condition{$parameter}{'1'}") { ..}

Would really appreciate any help. :)

回答1:

You either want string eval, which evaluates a string as Perl code

if (eval $condition{$_}{'1'}) { ...

or perhaps a more secure approach would be using code references

$condition{'hub'}{'1'} = sub { return $degree>=5 };

if ($condition{$_}{'1'}->()) { ...

In the second example, you are attaching a piece of code to a variable. The $var->() syntax executes the code and evaluates to the return value of the code.



回答2:

What you are trying to do is to evaluate '$degree>=5' as real code. Rather than trying to evaluate the string as code (which can be done with eval), it's usually safer and often more robust to instead pass a code-reference. You can use a generator subroutine to generate conditional subs on demand, like this:

sub generate_condition {
    my ( $test, $bound ) = @_;
    return sub { return $test >= $bound; };
}

my %condition;
$condition{'hub'}{'1'} = generate_condition( $degree, 5 );

if( $condition{$parameter}{1}->() ) { ... }

It gets a little more tricky if you want the >= (ie, the relationship itself) to be dynamically created as well. Then you have a couple of choices. One takes you back to stringy eval, with all of its risks (especially if you start letting your user specify the string). The another would be a lookup table within your generate_condition() sub.

generate_condition() returns a subroutine reference that when invoked, will evaluate the condition that was bound in at creation time.

Here's a generalized solution that will accept any of Perl's conditionals and wrap them along with the arguments being tested into a subroutine. The subref can then be invoked to evaluate the conditional:

use strict;
use warnings;
use feature qw/state/;

sub generate_condition {
    my ( $test, $relation, $bound ) = @_;
    die "Bad relationship\n" 
        if ! $relation =~ m/^(?:<=?|>=?|==|l[te]|g[te]|cmp)$/;
    state $relationships = {
        '<'     => sub { return $test <   $bound },
        '<='    => sub { return $test <=  $bound },
        '=='    => sub { return $test ==  $bound },
        '>='    => sub { return $test >=  $bound },
        '>'     => sub { return $test >   $bound },
        '<=>'   => sub { return $test <=> $bound },
        'lt'    => sub { return $test lt  $bound },
        'le'    => sub { return $test le  $bound },
        'eq'    => sub { return $test eq  $bound },
        'ge'    => sub { return $test ge  $bound },
        'gt'    => sub { return $test gt  $bound },
        'cmp'   => sub { return $test cmp $bound },
    };
    return $relationships->{$relation};
}


my $true_condition  = generate_condition( 10, '>', 5 );
my $false_condition = generate_condition( 'flower', 'eq', 'stamp' );

print '10 is greater than 5: ', 
      $true_condition->()  ? "true\n" : "false\n";
print '"flower" is equal to "stamp": ', 
      $false_condition->() ? "true\n" : "false\n";

Often when you construct these sorts of things one is interested in leaving one parameter open to bind at call-time rather than at subroutine manufacture-time. Let's say you only want to bind the "$bound" and "$relation" parameters, but leave "$test" open for specification at subroutine call time. You would modify your sub generation like this:

sub generate_condition {
    my ( $relation, $bound ) = @_;
    die "Bad relationship\n" 
        if ! $relation =~ m/^(?:<=?|>=?|==|l[te]|g[te]|cmp)$/;
    state $relationships = {
        '<'     => sub { return $_[0]  <   $bound },
        # ......

And then invoke it like this:

my $condition = generate_condition( '<', 5 );
if( $condition->(2) ) {
    print "Yes, 2 is less than 5\n";
}

If the goal is to provide late binding of both the lefthand and righthand side in the relational evaluation, this will work:

sub generate_condition {
    my $relation = shift;
    die "Bad relationship\n" 
        if ! $relation =~ m/^(?:<=?|>=?|==|l[te]|g[te]|cmp)$/;
    state $relationships = {
        '<'     => sub { return $_[0]  <   $_[1] },
        '<='    => sub { return $_[0]  <=  $_[1] },
        # ...... and so on .....
    return $relationship->($relation);
}

my $condition = generate_condition( '<' );
if( $condition->(2,10) ) { print "True.\n"; }

This sort of tool falls into the category of functional programming, and is covered in beautiful detail in Mark Jason Dominus's book Higher Order Perl



回答3:

What are you expecting? String values are interpreted as true when they are nonempty.

themel@kallisti: ~ $ perl -e 'print "oops\n" if "false" ; '
oops
themel@kallisti: ~ $ perl -e 'print "oops\n" if "" ; '
themel@kallisti: ~ $ perl -e 'print "oops\n" if "\$degree < 5" ;'
oops

If you want to dynamically evaluate code in your conditions, you have to investigate eval. Example:

my @conds=('$foo>42', '$foo>23');
my $foo = 33;

foreach my $cond(@conds) { 
    print "$cond itself was true\n" if $cond;
    print "$cond evaluated to true\n" if eval($cond);
}

prints

$foo>42 itself was true
$foo>23 itself was true
$foo>23 evaluated to true