http://qs321.pair.com?node_id=451791

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

Observing a hash changes its contents...

I'm using a hash of hashes, and doing an existence check. If the check fails, I print a usage giving the valid keys. Strangely, the invalid key is being created by the existence check. I added a line to delete it, which fixes it, but why is this happening at all?

Here's a sample repro:

#!/usr/bin/perl -w use strict; my %hash = ('a' => { 'value' => 1, 'foo' => 'bar', }, 'b' => { 'value' => 2, 'foo' => 'bar', }, 'c' => { 'value' => 3, 'foo' => 'bar', }, ); if (! $hash{'d'}->{'value'}) { print "That key doesn't exist. Valid keys are: "; print join (", ", keys %hash); }
'd' appears in the output list. No change if I use defined() for the exist check.

---
A fair fight is a sign of poor planning.

Replies are listed 'Best First'.
Re: Heisenberg Uncertainty Hash
by japhy (Canon) on Apr 26, 2005 at 22:48 UTC
    You're being bitten by "autovivification". When you check to see if $hash{d}{value} exists, Perl will autovivify $hash{d} if it doesn't already exist.

    Your best solution is to do if ($hash{d} and !$hash{d}{value}) { ... }.


    Jeff japhy Pinyan, P.L., P.M., P.O.D, X.S.: Perl, regex, and perl hacker
    How can we ever be the sold short or the cheated, we who for every service have long ago been overpaid? ~~ Meister Eckhart
      Instead of just checking the truth or otherwise of $hash{d}{value}, check for existence using the exists() function. That way you won't get weird bugs when the value is (eg) zero or the empty string. You might also want to check that $hash{d} is a reference to a hash before trying to access it.
        Putting an exists there, (exists $hash{'d'}->{'value'}) still autovivificates $hash{'d'}. This might be surprising, but it is logical. After all, you are enquiring about the hash %{$hash{'d'}} (asking whether it contains the key 'd'). Since that hash doesn't exists yet, Perl does what it always does in that case: it creates it.

        If you want to cover all your bases, and make sure no hash gets created as a side effect, be more explicite:

        if (exists $hash{'d'} && ref $hash{'d'} eq "HASH" && exists $hash{ +'d'}->{value}) { ... }
        A reply falls below the community's threshold of quality. You may see it by logging in.
Re: Heisenberg Uncertainty Hash
by Zaxo (Archbishop) on Apr 26, 2005 at 22:50 UTC

    The higher-level 'd' key is autovivified by checking for a lower-level key. Check existance of the higher-level key before doing anything with the lower level.

    if (exists $hash{'d'} and !$hash{'d'}->{'value'}) { print "That key doesn't exist. Valid keys are: "; print join (", ", keys %hash); }

    This is a well-known gotcha.

    After Compline,
    Zaxo

Re: Heisenberg Uncertainty Hash
by ikegami (Patriarch) on Apr 26, 2005 at 22:51 UTC

    You've been bitten by auto-vivification. It means that since $hash{'d'} is undefined and you're asking it to be a hash reference, Perl creates an anonymous hash for you.

    You probably want

    # If record 'd' does not exist: if (!$hash{'d'})

    Not as likely, but you might even want

    # If record 'd' does not exist, or # if record 'd' exists but has no field 'value': if (!$hash{'d'} || !exists($hash{'d'}{'value'}))

    or

    # If record 'd' does not exist, # if record 'd' exists but has no field 'value', or # if record 'd' exists and has field 'value' but is undefined: if (!$hash{'d'} || !defined($hash{'d'}{'value'}))

    However, the equivalent to your code would be

    # If record 'd' does not exist, # if record 'd' exists but has no field 'value', # if record 'd' exists and has field 'value' but is undefined, # if record 'd' exists and has field 'value' but is 0, # if record 'd' exists and has field 'value' but is "0", or # if record 'd' exists and has field 'value' but is '': if (!$hash{'d'} || !$hash{'d'}{'value'})

    Thanks for reducing the problem to a its minimum form.

Re: Heisenberg Uncertainty Hash
by Roy Johnson (Monsignor) on Apr 27, 2005 at 04:30 UTC
    Here's a demo of a sub you might find useful. It descends the hash chain checking existence at each level. It returns [a reference to, to ensure logical truth] the value if everything exists.
    #!perl use warnings; use strict; sub safe_exists { my ($href, @list_of_descent) = @_; use List::Util 'first'; if (first { not (exists $href->{$_} and $href=$href->{$_}) } @list_of_descent) { return (); } else { return \$href; } } my %hash = ('a' => { 'value' => 1, 'foo' => 'bar', }, 'b' => { 'value' => 2, 'foo' => 'bar', }, 'c' => { 'value' => 3, 'foo' => 'bar', }, ); for (qw(a b c d)) { if (my $href = safe_exists(\%hash, $_, 'value')) { print "$_ value is $$href\n"; } else { print "Key $_ doesn't exist. Valid keys are: "; print join (", ", keys %hash), "\n"; } }

    Caution: Contents may have been coded under pressure.
Re: Heisenberg Uncertainty Hash
by tlm (Prior) on Apr 27, 2005 at 00:12 UTC

    Protocol demands it that I begin this by saying something like "sir, consider yourself bitten by AUTOVIVIFICATION". With that out of the way, here's a useful tutorial on the subject; also, here's a rant I wrote on this.

    the lowliest monk

OT: Heisenberg Uncertainty link
by inman (Curate) on Apr 27, 2005 at 09:40 UTC
    Completely off topic but this link takes you to a web page that describes Heisenberg's Uncertainty Principal. (or maybe it doesn't. You just can't be sure... You've got to ask yourself Mr. Schroedinger, does the webpage exist before you click on the link?)