http://www.perlmonks.org?node_id=480102

Tanalis has asked for the wisdom of the Perl Monks concerning the following question:

I'm currently working on a script to performs auto-categorisation of data elements based on rules generated from their characteristics.

These categorisation rules change periodically, and hence I've created a rules file to perform the categorisation and return a suitable category and reason to the calling sub.

My problem: I'm struggling to have eval return anything. According to the docs:

In both forms, the value returned is the value of the last expression evaluated inside the mini-program; a return statement may be also used, just as with subroutines.

.. but that doesn't seem to work for me.

A typical rule might look like the following, where $abs_diff and $source are defined within the sub performing the eval:

return( "A", "Cat A" ) if $abs_diff < 75_000 and $source eq "ICE";

I've tried the following:

# process analysis rules foreach( @$analysis_rules ) { eval $_; }
and
# process analysis rules foreach( @$analysis_rules ) { my( $cat, $comment ) = eval $_; return( $cat, $comment ) if $cat and $comment; }
among a bunch of variations without luck.

Can anyone see where I'm going wrong?

Thanks in advance ...

Update: Fixed a typo in the rule - thanks to holli for pointing that out :)

Replies are listed 'Best First'.
Re: Returning data from an eval
by Zaxo (Archbishop) on Aug 02, 2005 at 07:47 UTC

    One problem is that your trials don't do what you intend with the program flow. Something like my @results = map { eval $_ } @$analysis_rules; is probably more like what you want. The example with return is particularly likely to be unexpected at you.

    With your rules containing return as well, it's not clear what you expect to happen when they are evaluated. Should they be compiled as subroutines?

    After Compline,
    Zaxo

Re: Returning data from an eval
by holli (Abbot) on Aug 02, 2005 at 09:15 UTC
    First, in your rule you are using "=" to do a string comparison. Don't do that. use eq.

    I have a similar program which reads rules from a xml-file and applies them to a bunch of data. I do compile those rules into anonymous subroutines and call them later on. That is far faster then evaling the same code over and over again. Like so:
    # @rules read from file or somewhere else my @rules = ( 'return( "A", "Cat A" ) if $_[0] < 75000 and $_[1] eq "ICE";', #... ); # convert rules to anon subs @rules = map { eval "\$_ = sub { $_ }"; } @rules; for ( @rules ) { print join ",", &$_(20000, "ICE"); #A, Cat A }


    holli, /regexed monk/
      Meh .. dumb typo in the comparison there. Thanks.

      I'd not thought about using anonymous subs - thanks for the tip. It certainly seems to speed things up - I'm doing a fairly large number of iterations over the code.

Re: Returning data from an eval
by anonymized user 468275 (Curate) on Aug 02, 2005 at 10:26 UTC
    The first example is simply throwing the result away; this modification simply prints it out instead:
    # process analysis rules foreach( @$analysis_rules ) { print eval( $_ ) . "\n"; }
    The second example has two errors. Firstly the number of declarations do not match the number of assignments and secondly the argument to return, while syntactically acceptable, doesn't DWIM, because the list will be coerced twice, first into an array and from that to a single scalar. Moreover it will force the return during the first iteration of the loop. The 'map' function provides a rather quick alternative.
    # process analysis rules my @results = map( eval, @analysis_rules );

    One world, one people

Re: Returning data from an eval
by tlm (Prior) on Aug 02, 2005 at 13:51 UTC

    I'm surprised your second version does not work for you; it works for me.

    If I were you, I'd code the rules without return, and so that the last expression in them is a clear, unambiguous value; e.g.

    q( $abs_diff < 75_000 and $source eq "ICE" ? ( "A", "Cat A" ) : () )
    Then you can write:
    for my $rule ( @rules ) { my @ret = eval $rule; return @ret if @ret; }
    The way you have it, if the test is true, the eval will return a list, otherwise it returns undef, so simply testing for @ret wouldn't work (it will always have a non-zero length).

    the lowliest monk