Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why does Perl think that a non-existent multi-level hash element is there?

Tags:

hash

perl

zero

Sorry, this seems like such a basic question but I still don't understand. If I have a hash, for example:

my %md_hash = ();
$md_hash{'top'}{'primary'}{'secondary'} = 0;

How come this is true?

if ($md_hash{'top'}{'foobar'}{'secondary'} == 0) {
    print "I'm true even though I'm not in that hash\n";
}

There is no "foobar" level in the hash so shouldn't that result in false?

TIA

like image 978
Analog Avatar asked Aug 25 '11 00:08

Analog


2 Answers

This isn't a multidimensional hash specific question.

It works the same with

my %foo;
if ( $foo{'bar'} == 0 ) {
    print "I'm true even though I'm not in that hash\n";
}

$foo{'bar'} is undef, which compares true to 0, albeit with a warning if you have warnings enabled as you should.

There is an additional side effect in your case; when you say

my %md_hash = ();
$md_hash{'top'}{'primary'}{'secondary'} = 0;

if ( $md_hash{'top'}{'foobar'}{'secondary'} == 0 ) {
    print "I'm true even though I'm not in that hash\n";
}

$md_hash{'top'} returns a hash reference, and the 'foobar' key is looked for in that hash. Because of the {'secondary'}, that 'foobar' element lookup is in hash-dereference context. This makes $md_hash{'top'}{'foobar'} "autovivify" a hash reference as the value of the 'foobar' key, leaving you with this structure:

my %md_hash = (
    'top' => {
        'primary' => {
            'secondary' => 0,
        },
        'foobar' => {},
    },
);

The autovivification pragma can be used to disable this behavior. People sometimes assert that exists() has some effect on autovifification, but this is not true.

like image 145
ysth Avatar answered Nov 15 '22 08:11

ysth


You are testing an undefined value for numeric zero. Of course you get a true result! What were you expecting?

You should also get a warning under use warnings. Why didn’t you?

If you do not start a program with:

use v5.12;  # or whatever it is you are using
use strict;
use warnings;

you really shouldn’t even bother. :)


EDIT

NB: I am only clarifying for correctness, because the comment lines are not good for that. I really could not possibly care one whingeing whit less about the reputation points. I just want people to understand.

Even under the CPAN autovivification module, nothing changes. Witness:

use v5.10;
use strict;
use warnings;
no  autovivification;

my %md_hash = ();

$md_hash{top}{primary}{secondary} = 0;

if ($md_hash{top}{foobar}{secondary} == 0) {
    say "yup, that was zero.";
}

When run, that says:

$ perl /tmp/demo
Use of uninitialized value in numeric eq (==) at /tmp/demo line 10.
yup, that was zero.

The test is the == operator. Its RHS is 0. Its LHS is undef irrespective of autovivification. Since undef is numerically 0, that means that both LHS and RHS contain 0, which the == correctly identifies as holding the same number.

Autovivification is not the issue here, as ysth correctly observes when he writes that “This isn’t a multidimensional hash specific question.” All that matters is what you pass to ==. And undef is numerically 0.

You can stop autoviv if you really, really want to — by using the CPAN pragma. But you will not ever manage to change what happens here by suppressing autoviv. That shows that it is not an autoviv matter at all, just an undef one.

Now, you will get “extra” keys when you do this, since the undefined lvalue will get filled in on the way through the dereference chain. These are neccesarily all the same:

$md_hash{top}{foobar}{secondary}            # implicit arrow for infix deref
$md_hash{top}->{foobar}->{secondary}        # explicit arrow for infix deref
${ ${ $md_hash{top} }{foobar} }{secondary}  # explicit prefix deref

Whenever you deref an lvaluable undef in Perl, that storage location always gets filled in with the proper sort of reference to a newly allocated anonymous referent of the proper type. In short, it autovivs.

And that you can stop either by suppressing or else by sidestepping the autoviv. However, denying the autoviv is not the same as sidestepping it, because you just change what sort of thing gets returned. The overall expession is still fully evaluated: there is no automatic short-circuiting just because you suppress autoviv. That’s because autoviv is not the problem (and if it were not there, you would be really annoyed: trust me).

If you want short-circuiting, you have to write that yourself. I never seem to need to myself. In Perl, that is. On the other hand, C programmers are quite accustomed to writing

 if (p && p->whatever) { ... }

And so you can do that, too, if you want to. However, it is pretty rare in my own experience. You almost have to bend over wrongwards in Perl for that ever to make a difference, as it is quite easy to arrange for your code not to change how it acts if there are empty levels.

like image 21
tchrist Avatar answered Nov 15 '22 07:11

tchrist