Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Perl conditional (ternary) operator does no shortcut evaluation

The conditional (ternary) operator suggests the ternary operator is a substitute for if ... else. I always thought so, but recently I have a logical problem with that.

Consider this short debug session:

  DB<1> $s='X'

  DB<2>  1 ? $s .= '_' : $s = '_'

  DB<3> x $s
0  '_'

So if 1 is true, then the expression $s .= '_' should be evaluated (and not $s = '_').

But why is $s just '_' at the end?

like image 664
U. Windl Avatar asked Sep 07 '21 13:09

U. Windl


3 Answers

The ternary conditional operator (?:) has a higher precedence than the assignment operator (=) (the table of the precedence of Perl's operator can be found in the Operator Precedence and Associativity section of perlop). As such, the line

1 ? $s .= '_' : $s = '_'

is parsed by Perl as

(1 ? ($s .= '_') : $s) = '_'

(You can check that by yourself by running perl -MO=Deparse <your program>)

Note also that $s .= '_' returns $s with the added _ at the end, and that this $s can be assigned to (in technical terms, it's an lvalue). This is documented in the Assignment Operators section of perlop:

Unlike in C, the scalar assignment operator produces a valid lvalue. Modifying an assignment is equivalent to doing the assignment and then modifying the variable that was assigned to.

So, basically, your code is doing

($s .= '_') = '_';

Which is equivalent to

$s .= '_';
$s = '_';
like image 161
Dada Avatar answered Oct 09 '22 06:10

Dada


It is an issue of operator precedence.

$ perl -MO=Deparse,-p -e '   
> $s='X';
> $t=1;
> $t ? $s .= '_' : $s = '_';
> print $s'
($s = 'X');
($t = 1);
(($t ? ($s .= '_') : $s) = '_');
print($s);
-e syntax OK

Whether the ternary condition is true or false, ultimately $s is set to "_".

To do what you intend to do, you need to add at least one set of parentheses:

1 ? $s .= '_' : ($s = '_');
like image 18
mob Avatar answered Oct 09 '22 04:10

mob


The conditional operator only evaluates what's necessary, but you have a precedence problem.


First of all, the conditional operator is indeed guaranteed to use short-circuit evaluation, meaning it only evaluates what's necessary.

$ perl -M5.010 -e'
   sub f { say "f" }
   sub g { say "g" }
   $ARGV[0] ? f() : g();
' 0
f

$ perl -M5.010 -e'
   sub f { say "f" }
   sub g { say "g" }
   say $ARGV[0] ? f() : g();
' 1
g

This is even true for E1 || E2, E1 or E2, E1 && E2 and E1 and E2. They only evaluate their right-hand side operand if necessary.

$ perl -M5.010 -e'
   sub f { say "f"; $ARGV[0] }
   sub g { say "g"; $ARGV[1] }
   say f() || g();
' 3 4
f
3

$ perl -M5.010 -e'
   sub f { say "f"; $ARGV[0] }
   sub g { say "g"; $ARGV[1] }
   say f() || g();
' 0 4
f
g
4

This is why you can can evaluate open(...) or die(...) safely. Without short-circuiting, it would evaluate die whether open was successful or not.


Now, let's explain the following:

$s = "X";   1 ? $s .= "_" : $s = "_";         say $s;   # _

It's a precedence problem. The above is equivalent to

$s = "X";   ( 1 ? ($s .= "_") : $s ) = "_";   say $s;   # _

$s .= "_" returns $s, so the conditional operator returns $s, so the string _ is assigned to $s. If we add parens to get the desired parsing, we get the expected result.

$s = "X";   1 ? ($s .= "_") : ($s = "_");     say $s;   # X_

Alternative:

$s = "X";   $s = ( 1 ? $s : "" ) . "_";       say $s;   # X_
like image 15
ikegami Avatar answered Oct 09 '22 06:10

ikegami