tags:

views:

124

answers:

6

I'm using dynamic multilevel hashes from which I read data but also writes data.

A common pitfall for me is accessing non-existing keys (typos, db revisions etc.). I get undefs which propagate to other parts and cause problems. I would like to die whenever I try to read a non-existing key, but still be allowed to add new keys.

So the wanted behavior is:

my %hash;
$hash{A} = 5;  # ok
print $hash{A}, "\n";  # ok
print $hash{X}, "\n";  # should die
$hash{B}{C}{D} = 10; # ok 
print $hash{B}{C}{X}, "\n";  # should die

I previously posted a similar question and got great answers. I especially like the accepted one, which allows using the normal hash syntax. The only problem is I'm not sure how to easily generalize this to deep hashes as in the example above.

p.s. I find this feature really useful and I wonder if I'm missing something, since it does not seem very popular. Perhaps it is not common to read/write from/to the same hash?

+3  A: 

It's late for me so I'll be brief, but you could do this using the tie functionality -- have your hash represented by an object underneath, and implement the functions needed to interact with the hash.

Check out perldoc -f tie; there are also many classes on CPAN to look at, including Tie::Hash itself which is a good base class for tied hashes which you could build on, overriding a few methods to add your error checking.

Ether
I would appreciate some more hints on how to get the recursive effect. I have used some temporary solution using a wrapping `get` function, but it makes my code look terrible: `my $val = get($hashref, qw (key1 key11 key111))` instead of `my $val = $hashref->{key1}{key11}{key111}`.
David B
+3  A: 

If you want to wrap checks around a hash, create a subroutine to do it and use it as your interface:

 use 5.010;
 use Carp qw(croak);

 sub read_from_hash {
     my( $hash, @keys ) = @_;

     return check_hash( $hash, @keys ) // croak ...;
     }

But now you're starting to look like a class. When you need specialized behavior, start writing object-oriented classes. Do whatever you need to do. That's the part you're missing, I think.

The problem with sticking to the hash interface is that people expect the hash syntax to act as normal hashes. When you change that behavior, other people are going to have a tough time figuring out what's going on and why.

brian d foy
+2  A: 

With warnings pragma switched on then you do get Use of uninitialized value in print at... warnings at the two lines you want to die.

So if you make warnings fatal then they would die instead:

use warnings FATAL => 'all';


Update

Based on comments you've made I assume your common case issue is something along these lines:

my $x = $hash{B}{C}{X};

Which won't throw warning/error until you actually use $x later on.

To get around this then you can do:

my $x = $hash{B}{C}{X} // 'some default value';

my $z = $hash{B}{C}{Z} // die "Invalid hash value";

Unfortunately the above would mean a lot of extra typing :(

Here is at least a short cut:

use 5.012;
use warnings FATAL => 'all';
use Carp 'croak';

# Value Or Croak!
sub voc { $_[0] // croak "Invalid hash" }

Then below would croak!

my $x = voc $hash{B}{C}{X};

Hopefully this and also the fatal warnings are helpful to you.

/I3az/

draegtun
This works for `print`, but generally I don't print these deep values but use them in other ways.
David B
Interesting ... you could also write a custom `$SIG{__WARN__}` handler (see `perlvar`) and fatally trap those warning messages.
mobrule
@David B: Could you provide some examples of the other ways where you have issues.
draegtun
@draegtun: e.g passing `$href->{KEY}{FAKE}` to some sub (which might work with `undef`, but this is not what I want) etc.
David B
@draegtun: (+1) for your update. that's almost perfect for me. If I could just eliminate the need for calling `voc` and make this default :)
David B
by the way, what exactly does `//` do? I don't even know how to search for it.
David B
draegtun
+1  A: 

If you don't know what keys the hash might have, use one of the tied hash suggestions or just turn on warnings. Be aware that tying is very slow, nine times slower than a regular hash and three times slower than an object.

If you have a fixed set of possible keys, what you want is a restricted hash. A restricted hash will only allow you to access a given set of keys and will throw an error if you try to access anything else. It can also recurse. This is much faster than tying.

Otherwise, I would suggest turning your data into an object with methods rather than direct hash accesses. This is slower than a hash or restricted hash, but faster than a tied hash. There are many modules on CPAN to generate methods for you starting with Class::Accessor.

If your data is not fixed, you can write simple get() and set() methods like so:

package Safe::Hash;

use strict;
use warnings;
use Carp;

sub new {
    my $class = shift;
    my $self = shift || {};
    return bless $self, $class;
}

sub get {
    my($self, $key) = @_;
    croak "$key has no value" unless exists $self->{$key};
    return $self->{$key};
}

sub set {
    my($self, $key, $value) = @_;
    $self->{$key} = $value;
    return;
}

You can get recursive behavior by storing objects in objects.

my $inner = Safe::Hash->new({ foo => 42 });
my $outer = Safe::Hash->new({ bar => 23 });
$outer->set( inner => $inner );
print $outer->get("inner")->get("foo");

Finally, since you mentioned db revisions, if your data is being read from a database then you will want to look into an object relation mapper (ORM) to generate classes and objects and SQL statements for you. DBIx::Class and Rose::DB::Object are two good examples.

Schwern
A: 

re: your comment - hints on how to get the recursive effect on Ether's tie answer.

Its not for the faint hearted but below is a basic example of one way that you might do what your after by using Tie::Hash:

HashX.pm

package HashX;
use 5.012;
use warnings FATAL => 'all';
use Carp 'croak';
use Tie::Hash;
use base 'Tie::StdHash';

sub import {
    no strict 'refs';
    *{caller . '::hash'} = sub {
        tie my %h, 'HashX', @_;
        \%h;
    }
}

sub TIEHASH {
    my $class = shift;
    croak "Please define a structure!" unless @_;
    bless { @_ }, $class;
}

sub STORE {
    my ($self, $key, $value) = @_;
    croak "Invalid hash key used to store a value" unless exists $self->{$key};
    $self->{$key} = $value;
}

sub FETCH {
    my ($self, $key) = @_;
    exists $self->{$key} 
        ?  $self->{$key} 
        :  croak "Invalid hash key used to fetch a value";
}

1;

Above module is like a strict hash. You have to declare the hash structure up front then any FETCH or STORE will croak unless the hash key does exist.

The module has a simple hash function which is imported into calling program and is used to build the necessary tie for everything to work.

use 5.012;
use warnings;
use HashX;

# all my hashref are ties by using hash()
my $hash = hash(
    a => hash(
        b => hash(
            c => undef,
        ),
    ),
);

$hash->{a}{b}{c} = 1;      # ok
$hash->{a}{b}{c} = 2;      # also ok!
$hash->{a}{b}{d} = 3;      # throws error
my $x = $hash->{a}{b}{x};  # ditto

Remember this is a quick & dirty example and is untested beyond above. I'm hoping it will give you the idea of how it could be done using Tie::Hash and even whether it worth attempting it :)

/I3az/

draegtun
A: 

Use DiveDie from Data::Diver:

use Data::Diver qw(DiveDie);

my $href = { a => { g => 4}, b => 2 };

print DiveDie($href, qw(a g)), "\n"; # prints "4"
print DiveDie($href, qw(c)), "\n";   # dies
runrig