Monday, September 21, 2009

hit by operator precedence and right associativity

While studying a bug, I wrote the following test program :

use strict;
use warnings;
use Data::Dumper;
my $bool = 1;
my %h;
$bool ? $h{true} = 't' : $h{false} = 'f';
print Dumper(\%h);

The ternary expression starting with $bool was supposed to be a concise way to write a conditional, but the result was a disaster. Can you guess the output ?

Here it is : $VAR1 = { 'true' => 'f' };

This really seems totally insane : something is assigned to the 'true' slot of the hash, but the value comes from what was supposed to be in the 'false' slot !

OK, the ternary expression above is wrong, because the '?:' operator has higher precedence than '=', so one should really write

$bool ? ($h{true} = 't') : ($h{false} = 'f');

But how comes that perl issues no error, no warning, and happily produces a very strange result ? It seems that both sides of the conditional are executed simultaneously, and collapse in a mysterious way.

I tried running the script through B::Deparse to understand how it was parsed, but the output was exactly like the original source, so it really seems to be legal Perl !

It really took me a while until the 'aha' moment that made me realize that because of right-associativity, and because conditional expressions can be lvalues, and because "Unlike in C, the scalar assignment operator produces a valid lvalue" (perlop dixit), this was parsed as

($bool ? ($h{true} = 't') : $h{false}) = 'f';

So the $bool test chooses an lvalue between $h{true} and $h{false}, and it doesn't matter that this lvalue is first assigned a 't', because later the main assignment puts a 'f' into it.

Obvious, isn't it ?

2 comments:

  1. Well, yes, it is obvious if you know the precedence levels. The biggest problem here is that you are abusing the conditional operator. The conditional operator is there so we can factor out common parts of and if and else statement:

    if ($bool) {
        print "this is true\n";
    } else {
        print "this is false\n";
    }

    can be factored into

    print "this is", $bool ? "true" : "false", "\n";

    But your code has no portion that can be factored. It should be an if statement instead.

    You don't tend to be bitten when you are using it the right way. For instance, you may be tempted to write broken code like:

    print "this is ", $bool ? $save = "true" : $save = $false, "\n";

    which exhibits the same behavior as your example, but if you look closely you see that the code is not fully factored:

    print "this is ", $save = $bool ? "true" : "false", "\n";

    A similar thing occurs for

    print "this is ", $bool ? $x = $bool : $y = $bool, "\n"

    which can be properly factored into

    print "this is ", $bool ? $x : $y = $bool, "\n"

    You are also using the conditional statement outside of an expression, which is a sure sign you really want an if statement. If your only goal is to avoid typing if () {} else {} (i.e. you don't care about readability), you can always just use and and or:

    $bool and $h{true} = 't' or $h{false} = 'f';

    ReplyDelete
  2. B::Deparse would have helped you more if you'd given it the -p "add more parentheses" parameter. It makes things like this this clear.

    perl -MO=Deparse,-p program.pl

    ReplyDelete