Perl ternary errantly enters "else" clause? - perl

I have the following code:
# List of tests
my $tests = [("system_test_builtins_sin", "system_test_builtins_cos", "system_test_builtins_tan")];
# Provide overrides for certain variables that may be needed because of special cases
# For example, cos must be executed 100 times and sin only 5 times.
my %testOverrides = (
system_test_builtins_sin => {
reps => 5,
},
system_test_builtins_cos => {
reps => 100,
},
);
my %testDefaults = (
system_test_reps => 10,
);
# Execute a system tests
foreach my $testName (#$tests)
{
print "Executing $testName\n";
my $reps;
if (exists $testOverrides{$testName}{reps})
{ $reps = $testOverrides{$testName}{reps}; }
else
{ $reps = $testDefaults{system_test_reps}; }
print "After long if: $reps\n";
exists $testOverrides{$testName}{reps} ? $reps = $testOverrides{$testName}{reps} : $reps = $testDefaults{system_test_reps};
print "After first ternary: $reps\n";
exists $testOverrides{$testName}{reps} ? $reps = $testOverrides{$testName}{reps} : print "Override not found.\n";
print "After second ternary: $reps\n";
}
This gives the following output:
Executing system_test_builtins_sin
After long if: 5
After first ternary: 10
After second ternary: 5
Executing system_test_builtins_cos
After long if: 100
After first ternary: 10
After second ternary: 100
Executing system_test_builtins_tan
After long if: 10
After first ternary: 10
Override not found.
After second ternary: 10
This output is most unexpected! I don't understand why the first ternary seems to always be executing the "if false" clause. It is always assigning a value of 10. I also tried changing the "false" clause to $reps = 6, and I saw that it always got the value of 6. Why does the ternary's logic depend on the content of the third (if false) clause?

Here's a simpler script that illustrates the problem:
#!/usr/bin/perl
use strict;
use warnings;
my $x;
1 ? $x = 1 : $x = 0;
print "Without parentheses, \$x = $x\n";
1 ? ($x = 1) : ($x = 0);
print "With parentheses, \$x = $x\n";
It produces this output:
Without parentheses, $x = 0
With parentheses, $x = 1
I'm not sure that the relationship between assignment and ?: can be complete explained by operator precedence. (For example, I believe C and C++ can behave differently in some cases.)
Run perldoc perlop and search for "Conditional Operator", or look here; it covers this exact issue (more concisely than I did here).
In any case, I think that using an if/else statement would be clearer than using the ?: operator. Or, since both the "true" and "false" branches assign to the same variable, a better use of ?: would be to change this:
exists $testOverrides{$testName}{reps}
? $reps = $testOverrides{$testName}{reps}
: $reps = $testDefaults{system_test_reps};
to this:
$reps = ( exists $testOverrides{$testName}{reps}
? testOverrides{$testName}{reps}
: $testDefaults{system_test_reps} );
But again, the fact that I had to wrap the line to avoid scrolling is a good indication that an if/else would be clearer.
You might also consider using the // operator, unless you're stuck with an ancient version of Perl that doesn't support it. (It was introduced by Perl 5.10.) It's also known as the "defined-or" operator. This:
$x // $y
is equivalent to
defined($x) ? $x : $y
So you could write:
$reps = $testOverrides{$testName}{reps} // $testDefaults{system_test_reps};
This doesn't have exactly the same semantics, since it tests the expression using defined rather than exists; it will behave differently if $testOverrides{$testName}{reps} exists but has the value undef.

The -p option to B::Deparse is illuminative for problems like this:
$ perl -MO=Deparse,-p -e '$condition ? $x = $value1 : $x = $value2'
(($condition ? ($x = $value1) : $x) = $value2);
As Keith Thompson points out, it is all about the precedence. If the condition is false, the ultimate assignment is $x = $value2. If the condition is true, then the assignment is ($x = $value1) = $value2 -- either way the outcome is to assign $value2 to $x.

I would do it this way (I dont mind using ternary operators)
$reps = exists($testOverrides{$testName}{reps}) ?
$testOverrides{$testName}{reps} :
$testDefaults{system_test_reps};
HTH

Thanks for giving us a sample of your code. Now we can tear it to pieces.
Don't use the ternary operator. It's an infection left over to originally make C programmers feel comfortable. In C, the ternary operator was used because it was originally more efficient than an if/else statement. However, compilers are pretty good about optimizing code, so that's no longer true and now it's discouraged in C and C++ programming. Programming in C is hard enough as it is without ternary operators mucking about.
The Perl compiler is also extremely efficient at optimizing your code, so you should always write for maximum clarity, so others who aren't as good as programming and get stuck maintaining your code can muddle through their job.
The problem you're having is one of operator precedence. You're assuming this:
(exists $testOverrides{$testName}{reps})
? ($reps = $testOverrides{$testName}{reps})
: ($reps = $testDefaults{system_test_reps});
I would too. After all, that's what I pretty much mean. However, the assignment operator has lower precedence than the ternary operator. What's really happening is this:
(exists $testOverrides{$testName}{reps})
? ($reps = $testOverrides{$testName}{reps}) : ($reps))
= $testDefaults{system_test_reps});
so, the final assignment is always happening to $reps.
It's much better if you use if/else:
if (exists $testOverrides{$testName}{reps}) {
$reps = = $testOverrides{$testName}{reps};
}
else {
$reps = $testDefaults{system_test_reps};
}
No precedence issues, easier to read, and just as efficient.

Related

Perl “do { … } if …” as expression

my $x = do { 3; } if 1; say $x # works
my $x = (do { 3; } if 1); say $x # syntax error
How come? If the do block is an expression, why can't it be parenthesised? If it's not, how does the first one parse?
A compound statement used for flow control (if BLOCK), as well as one with the statement modifier (used here, the postfix if), cannot appear inside parenthesis.
This restriction makes sense since such a statement may or may not return a value
if executes the statement once if and only if the condition is true.
(original emphasis)
A side note. The first example runs without warnings but it has undefined behavior, what must be avoided. From the end of the section Statement Modifiers in perlsyn
NOTE: The behaviour of a my, state, or our modified with a statement modifier conditional or loop construct (for example, my $x if ...) is undefined. The value of the my variable may be undef, any previously assigned value, or possibly anything else. Don't rely on it. Future versions of perl might do something different from the version of perl you try it out on. Here be dragons.
(original emphasis)
Any instances of this should be rewritten, and Perl::Critic has a policy for it, making it easier to find them
It's not that the do that's the problem, it's the postfix if. That postfix can't appear inside the parens:
$ perl -E 'my $x = ( 1 if 1); say $x'
syntax error at -e line 1, near "1 if"
Execution of -e aborted due to compilation errors.
Instead, you can use the conditional operator ?: with a do in one of the branches:
$ perl -E 'my $x = ( time % 2 ? do { 1 } : () ); say $x'
my $x = do { 3; } if 1;
is actually equivalent to
( my $x = do { 3; } ) if 1;
Note that you shouldn't execute my conditionally. (More precisely, you shouldn't use a my variable that hasn't been executed. Your code is technically ok since the my is always executed before $x is used.)
An expression (my $x = do { ... }) modified by a statement modifier (if 1) is a statement.
The inside of parens must be an expression, not a statement.
You can't do
( $x = 3; )
( sub f { } )
( if (f()) { g() } )
( g() if f(); )
( g() if f() )
You get the idea.
if 1 is a statement modifier. my $x = do { 3; } is a statement; do { 3; } is an expression.

Ternary operator doesn't allow iterative operator in it, but if-else does?

I noticed that if I replace the if-else statement I'm using with a ternary operator I end getting a compilation error when I try and run my code. I believe the culprit is the foreach() loop I have inside my if-else. Do you know why the ternary operator isn't behaving the same as the if-else construct in this instance?
My code looks like this
#!/program/perl_v5.6.1/bin/perl5.6.1
use strict;
use warnings;
my $fruits_array_ref = &get_fruits();
if($fruits_array_ref != 0) {
print("$_ is a fruit.\n") foreach(#$fruits_array_ref);
}
else {
print("Maybe you like vegetables?\n");
}
sub get_fruits() {
my #fruit_list;
my $shopping_list = "/home/lr625/grocery_items";
open(my $shopping_list_h, "<", $shopping_list) or die("Couldn't open.\n");
while(my $line = <$shopping_list_h>) {
next if $line =~ /^\#/;
chomp($line);
push(#fruit_list, $line);
}
close($shopping_list_h) or die("Couldn't close.\n");
scalar(#fruit_list) > 0 ? return(\#fruit_list) : return(0);
}
My data in the grocery list looks like
# this is a header
# to my grocery list
apple
banana
grape
orange
I'm replacing the if-else with a ?: operator to look like this now in the main function.
my $fruits_array_ref = &get_fruits();
$fruits_array_ref != 0 ? print("$_ is a fruit.\n") foreach(#$fruits_array_ref) : print("Maybe you like vegetables?\n");
Also, for reference my error says.
syntax error at test.pl line 8, near ") foreach"
Execution of test.pl aborted due to compilation errors.
if-else is a flow control structure, ?-: is an operator that takes expressions as operands. foreach is a flow control structure, not an expression.
You can turn any block of code into an expression by using do:
$fruits_array_ref != 0
? do { print "$_ is a fruit.\n" for #$fruits_array_ref }
: print "Maybe you like vegetables?\n";
But why?
The other answers already pointed out that you can't use the ternary operator the way you tried. For the sake of completeness and to give you some sensible use cases, take a look at the following examples:
#1: Used as a subroutine argument
testSub($var eq 'test' ? 'foo' : 'bar');
Here you can see how the subroutine testSub is called with the argument foo if $var equals the string test. Otherwise testSub will be called with bar. This is useful because you cannot use an if-else structure as a sub argument.
#2: Used for conditional assignment
my $result = $var eq 'test' ? 'foo' : 'bar'; # $result will contain 'foo' or 'bar'
The ternary operator is not meant as a simple replacement to an if-else structure. Since it returns a value (here either foo or bar) it makes sense to also use this value. If you don't intend to use the returned value, you should go for the usual if-else instead.
The foreach statement modifier can only be used at the end of a statement.
Why are you using ?:? You would normally only do that if you wanted a single result.
You could wrap the print...foreach... in a do {...}, or you could use map instead of foreach. Or just leave it as an if/else.
The ternary operator takes arguments before ? and :, see in perlop. It can evaluate an expression and use its result for this. But a loop is not an expression and cannot 'run' inside.
For a demonstration -- you could, if you insisted, call a function which will as a side effect print
sub greet { say "hello" for 1..3 }
my $x = 1;
($x == 1) ? greet() : say "bye";
Actualy doing this in production code is a different matter and would likely be a bad idea. The whole point would be to rely entirely on side effects, opposite to what we normally want to do.
To explain my comment above -- the main point of the ternary operator is to return a value, with a choice between two values, in one statement. While it is "equivalent" to an if-else, its use is (ideally) meant to be very different. So doing some other processing inside the ?: arguments, in any way, is really an abuse of notation, a side-effect, since they are intended to produce a value to be returned. Printing out of it is opposite to the idea of producing and returning a value. This is not a criticism, the operator is used often and by many as a syntactic shortcut.
In this sense I would recommend to revert to an if-else for doing what is shown.

Is there a cleaner way to conditionally 'last' out of this Perl loop?

Not really knowing Perl, I have been enhancing a Perl script with help from a friendly search engine.
I find that I need to break out of a loop while setting a flag if a condition comes true:
foreach my $element (#array) {
if($costlyCondition) {
$flag = 1;
last;
}
}
I know that the nicer way to use 'last' is something like this:
foreach my $element (#array) {
last if ($costlyCondition);
}
Of course, this means that while I can enjoy the syntactic sugar, I cannot set my flag inside the loop, which means I need to evaluate $costlyCondition once again outside.
Is there a cleaner way to do this?
you can use a do {...} block:
do {$flag = 1; last} if $costlyCondition
you can use the , operator to join the statements:
$flag = 1, last if $costlyCondition;
you can do the same with the logical && operator:
(($flag = 1) && last) if $costlyCondition;
or even the lower priority and:
(($flag = 1) and last) if $costlyCondition;
at the end of the day, there's no real reason to do any of these. They all do exactly the same as your original code. If your original code works and is legible, leave it like it is.
I agree with Nathan, that while neat looking code is neat, sometimes a readable version is better. Just for the hell of it, though, here's a horrible version:
last if $flag = $costly_condition;
Note the use of assignment = instead of equality ==. The assignment will return whatever value is in $costly_condition.
This of course will not make $flag = 1, but whatever $costly_condition is. But, since that needs to be true, so will $flag. To remedy that, you can - as Zaid mentioned in the comments - use:
last if $flag = !! $costly_condition;
As mentioned, pretty horrible solutions, but they do work.
One thought is to do the loop in a subroutine that returns different values depending on the exit point.
my $flag = check_elements(\#array);
# later...
sub check_elements {
my $arrayref = shift;
for my $ele (#$arrayref) {
return 1 if $costly_condition;
}
return 0;
}
This is possible, but highly not recommended: such tricks decrease readability of your code.
foreach my $element (#array) {
$flag = 1 and last if $costlyCondition;
}

Odd Perl conditional operator behavior

I'm doing some work in Perl and I ran across an odd result using the conditional operator.
The code in question:
($foo eq "blah") ? #x = #somearray : #y = ("another","array");
Trying to compile this code results in the error "Assignment to both a list and a scalar at XXX line YY, near ');'". In trying to pinpoint the source of the error I've written this using a couple different ways of representing an array in Perl and they all return with the same error. Now at first I thought it was just some dumb obvious mistake with the assignment statements, but just to satisfy my curiosity I rewrote the statement in a more verbose way:
if($foo eq "blah") {
#x = #somearray;
} else {
#y = ("another","array");
}
That version of the code compiled perfectly fine.
Is there some fine distinction between how the conditional operator works and a basic if-else statement works that I'm missing here? I always understood the conditional operator to be just a short-hand version of the second statement. If there isn't a functional difference between the two, why would Perl object to the first statement, but not the second?
$ perl -MO=Deparse -e'($foo eq "blah") ? #x = #somearray : #y = ("another","array");'
Assignment to both a list and a scalar at -e line 1, near ");"
-e had compilation errors.
$foo eq 'blah' ? (#x = #somearray) : #y = ('another', 'array');
$ perl -MO=Deparse -e'($foo eq "blah") ? #x = #somearray : (#y = ("another","array"));'
$foo eq 'blah' ? (#x = #somearray) : (#y = ('another', 'array'));
-e syntax OK
Note the parentheses: ?: binds tighter than =.
The Perl conditional operator is meant to be
$variable = (expression) ? true assignment : false assignment;
What you're doing looks like it should work and is basically the same as the if/else statement. But is just different enough from the norm to have issues.
The perlop documentation clearly states you should put parentheses around assignment operators.
Failing to use parentheses is a rod for your own back if you don't understand operator precedence. Stop trying to be too smart for your own good!
This is somewhat orthogonal to your question, but it bears pointing out: Perl's conditional operator propagates context from the first argument down into the second or third arguments, so this would give you undesired results:
$x = ($foo eq "blah") ? $somevalue : ("another","array");
If the conditional were false, $x would instead be assigned a single integer value 2 (the number of elements in the third argument).
If on the other hand you were attempting to perform a purely scalar assignment:
# this is wrong, for the same order-of-operations reasons as with arrays
($foo eq "blah") ? $x = $somevalue : $x = "another value";
This would be a reasonable (and the best) way to resolve the situation:
$x = ($foo eq "blah") ? $somevalue : "another value";
Likewise, you could optimize your original code this way:
#x = ($foo eq "blah") ? #somearray : ("another","array");
This would be a good spot to use the lower precedence 'and' and 'or' operators.
$foo eq 'blah' and #x = #somearray or #y = ('another', 'array');
provided you are sure that #x = #somearray will always be true. or you could flip them around.

Assignment inside Perl ternary conditional operator problems

This snippet of Perl code in my program is giving the wrong result.
$condition ? $a = 2 : $a = 3 ;
print $a;
No matter what the value of $condition is, the output is always 3, how come?
This is explained in the Perl documentation.
Because of Perl operator precedence the statement is being parsed as
($condition ? $a= 2 : $a ) = 3 ;
Because the ?: operator produces an assignable result, 3 is assigned to the result of the condition.
When $condition is true this means ($a=2)=3 giving $a=3
When $condition is false this means ($a)=3 giving $a=3
The correct way to write this is
$a = ( $condition ? 2 : 3 );
print $a;
We got bitten by this at work, so I am posting here hoping others will find it useful.
Once you have an inkling that you might be suffering from precedence problems, a trick to figure out what Perl thought you meant:
perl -MO=Deparse,-p -e '$condition ? $a= 2 : $a= 3 ; print $a;'
In your case, that'll show you:
(($condition ? ($a = 2) : $a) = 3);
print($a);
-e syntax OK
...at which point you should be saying "oh, that explains it"!
Just to extend the previous answer... If, for whatever reason, the assignments need to be part of the conditional, you'd want to write it thusly:
$condition ? ($a=2) : ($a=3);
This would be useful if you're assigning to different variables based on the condition.
$condition ? ($a=2) : ($b=3);
And if you're choosing the variable, but assigning the same thing no matter what, you could even do this:
($condition ? $a : $b) = 3;
Because of Perl operator precedence the statement is being parsed as:
($condition ? $a = 2 : $a ) = 3 ;
Because the ?: operator produces an assignable result, 3 is assigned to the result of the condition.
When $condition is true this means $a=2=3 giving $a=3
When $condition is false this means $a=3 giving $a=3
The correct way to write this is
$a = $condition ? 2 : 3;
In general, you should really get out of the habit of using conditionals to do assignment, as in the original example -- it's the sort of thing that leads to Perl getting a reputation for being write-only.
A good rule of thumb is that conditionals are only for simple values, never expressions with side effects. When you or someone else needs to read this code eight months from now, would you prefer it to read like this?
$x < 3 ? foo($x) : bar($y);
Or like this?
if ($x < 3) {
$foo($x);
} else {
$bar($y);
}
One suggestion to Tithonium's answer above:
If you are want to assign different values to the same variable, this might be better (the copy-book way):
$a = ($condition) ? 2 : 3;