http://www.perlmonks.org?node_id=269737

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

I have a hash of hashes of hashes of arbitrary depth as part of a class.

So, to access a specific element, I have something like $hash{a}->{b}->{c}->{d}, I would like to write a method which would allow me to access the same element with the identifier 'a.b.c.d' - so far I've come up with either doing it as a recursive sub or constructing a string and using eval.

Are there other ways of doing it? I am basically looking for the least expensive way.

Replies are listed 'Best First'.
Re: quick question about hash element access
by tilly (Archbishop) on Jun 27, 2003 at 21:18 UTC
    Sorry, basically you have to do it recursively or using eval. For a miniscule efficiency gain you can use tail-optimization to turn the recursion into iteration like so:
    sub nested_lookup { my $node = shift; while (ref($node) and @_) { $node = $node->{ shift(@_) }; } return @_ ? undef : $node; }
    and you would call it like this:
    my $elem = nested_lookup(\%hash, split /\./, 'a.b.c.d');
    (Note: could benefit from more error checking.)

      And with very few changes you can make it an lvalue, so you can use it to assign to:

      sub nested : lvalue { @_ == 1 ? $_[0] : nested($_[0]{$_[1]}, @_[2..$#_]); }

      ..and now you can say:

      nested(\%hash, a => b => c =>) = "VALUE";

      TThis can lead to such fun looking constructs as nested $hashref, a => b => c => d => = 42; Note that this code will only work under perl 5.6.0 or later, though; earlier perls had more restricted definitions of lvalues. See also descending a tree of hash references.

      perl -pe '"I lo*`+$^X$\"$]!$/"=~m%(.*)%s;$_=$1;y^`+*^e v^#$&V"+@( NO CARRIER'

Re: quick question about hash element access
by cLive ;-) (Prior) on Jun 27, 2003 at 22:52 UTC
    Nice question :) I had a play using the lovely AUTOLOAD.
    #!/usr/bin/perl use strict; use warnings; our $AUTOLOAD; our $hashref = { 'a' => { 'b' => { 'c' => 'this is c', 'd' => 'this is d' } } }; print "a_b_c() = ".a_b_c()."\n"; print "a_b_d() = ".a_b_d()."\n"; exit(0); sub AUTOLOAD { my ($package,$ref_name) = split /::/, $AUTOLOAD; my $ref = '$hashref'. join '', map { "->{$_}" } grep /^\w+$/, spli +t /_/, $ref_name; return eval $ref; } # output for above is: a_b_c() = this is c a_b_d() = this is d

    .02

    cLive ;-)

Re: quick question about hash element access
by chromatic (Archbishop) on Jun 27, 2003 at 21:16 UTC
Re: quick question about hash element access
by Nkuvu (Priest) on Jun 27, 2003 at 21:12 UTC

    Something like the following?

    #!/usr/bin/perl -w use strict; my %test_hash; $test_hash{'a'}{'b'}{'c'}{'d'} = "Yay"; print get_id("a.b.c.d"), "\n"; sub get_id { my @parts = split /\./, $_[0]; # Currently written to access a global hash return $test_hash{$parts[0]}{$parts[1]}{$parts[2]}{$parts[3]}; }

    I'm sure other monks can find more efficient (or bulletproof) ways. But this seems to work -- you could probably easily alter it so that it accepts a reference to a particular hash and uses that inside the sub.

    Also note that $hash{'a'}{'b'}{'c'} is the same as $hash{'a'}->{'b'}->{'c'}

      I suspect the question was not intended to be limited to hash paths of a specific depth. I'd suggest something like this:
      sub hpath(\%$) { my ($href, $path) = @_; while ($path =~ /\G(.+?)\.?/gc) { $href = $href->{$1}; } return $href; } my %c = (1 => { 2 => { 3 => 4 }}); print hpath(%c, "1.2.3");
      Only with error checking, of course. Substitute while  ... with foreach (split(/\./, $path) (and $1 with $_) for slightly different flavor.
        (For a another flavor altogether, try reduce:
        use List::Util qw(reduce); my %c = (1 => { 2 => { 3 => 4 }}); print reduce { $a->{$b} } \%c, split(/\./, "1.2.3");
        )
      added to your code so it can take a variable length identifier, and accept the hashref.
      my %test_hash; $test_hash{'a'}{'b'}{'c'}{'d'} = "Yay"; print get_id(\%test_hash, "a.b.c.d"), "\n"; sub get_id { my $thingy = shift; my @keys = split /\./, shift; foreach my $key ( @keys ) { $thingy = $$thingy{$key}; } return $thingy; }

        Interesting. But I have a question (not being totally fluent in references).

        What exactly does the $thingy = $$thingy{$key} line do? My guess is that it assigns a scalar value referred to by the reference which is contained in $thingy{$key}. But I'm not even sure that sentence makes sense, much less if it's accurate. :)

      I suspect that the key requirement is variable depth of nesting.

      (Your solution assumes a fixed depth.)

        Hmm, yes, I hadn't thought about that possible requirement. And here I thought I had an easy solution. :)

Re: quick question about hash element access
by jdklueber (Beadle) on Jun 28, 2003 at 00:23 UTC
    This does what I think you're looking for. One caveat: It returns ANYTHING it finds at the end of the argument- even a hash ref (as though you hadn't gotten to the actual terminus). So if that's not what you're intending, you'll need to check your output.
    use strict;
    
    my %variable_hash;
    
    $variable_hash{a}{b}{c}{d}    = 40;
    $variable_hash{e}{f}{g}{h}{i} = 50;
    $variable_hash{a}{b}{c}{j}    = 45;
    
    
    sub get_value
    {
        my $argument = shift;
        my @tmp = split /\./, $argument;
        my %lastref = %variable_hash;
        my $max = @tmp;
        my $current = 0;
        my $found = 0;
        foreach my $arg (@tmp)
        {
            $current++;
            last unless (exists($lastref{$arg}));
            if ($current == $max)
            {
                return $lastref{$arg};
            }
            %lastref = %{$lastref{$arg}};
        }
        undef %lastref;
        return %lastref;
    }
    
    print get_value("a.b.c.d");
    print get_value("e.f.g.h.i");
    print get_value("a.b.c.j");
    print get_value("a.b.c.k");
    print get_value("a");
    
    
    
    --
    Jason Klueber
    ookami@insightbb.com

    /(bb)|^b{2}/
    --Shakespeare
Re: quick question about hash element access
by antirice (Priest) on Jun 28, 2003 at 04:58 UTC

    How come everyone else gets to have the fun? Let's see if you'll let me in with this little hack that lets you play with all sorts of mixtures of structures...hashes, and arrays, and anonymous subs...OH MY! And in case something goes haywire and the key you specify doesn't exist [:'(] have no fear, it just returns undef and allows you to go along on your merry way! Follow the yellow br...err...I mean...umm...here's the code:

    #!/usr/bin/perl require 5.6.1; use strict; my $hash; $hash = { a => { b=> [ \\\$hash, { c => sub { $_ = shift; return [ join "","j",$_,"h" ]; } } ] } }; sub retrievekey { my $endvalue = shift; my $keys = shift; my @keys = split /\./, $keys; foreach my $x (@keys) { return undef unless defined $endvalue; $endvalue = $$endvalue while (ref($endvalue) eq "REF"); eval { if (ref($endvalue) eq "HASH") { $endvalue = $endvalue->{$x}; } elsif (ref($endvalue) eq "ARRAY") { $endvalue = $endvalue->[$x]; } elsif (ref($endvalue) eq "CODE") { $endvalue = $endvalue->($x); } else { return undef; } }; return undef if ($@); } $endvalue; } print retrievekey($hash, 'a.b.0.a.b.1.c.ap.0'),$/; __DATA__ output: japh

    Hey, it derefs any references to references and follows paths for hashes, arrays, and subs. This is just a silly bit of fun. Of course, don't use this or whoever maintains the code will hunt you down and shoot you... and nobody wants that... at least I don't :-P

    antirice    
    The first rule of Perl club is - use Perl
    The
    ith rule of Perl club is - follow rule i - 1 for i > 1

Re: quick question about hash element access
by shemp (Deacon) on Jun 27, 2003 at 21:37 UTC
    Just wondering, why are you using an identifier like 'a.b.c.b'. It occurs to me that if i were doing this, id probably have the identifier be an array (ref), i.e.
    my @identifier = qw(a b c d); ...
Re: quick question about hash element access
by DrHyde (Prior) on Jun 28, 2003 at 12:20 UTC
    Very quick and simple solution:
    $hash{a}{b}{c}{d}{e} = 'E'; print getthingy(\%hash, 'a.b.c.d.e'); sub getthingy { my($hash, $key) = @_; $hash = $hash->{$_} foreach(split/\./, $key); $hash; }
Re: quick question about hash element access
by glwtta (Hermit) on Jun 28, 2003 at 18:00 UTC
    Thanks for all the responses, as I suspected people have managed to come up with quite a few things I haven't thought of.

    In my case, I eneded up doing something much less cool, but more functional - since I only need the hash for read-only access, I just "flatten" the hash-of-hashes-of-hashes... into a simple hash with strings I wanted ('a.b.c.d') as the keys when I load it, and go from there - not as fun, but quicker.

    Thanks, again.