19

I have Hash where values of keys are other Hashes.

Example: {'key' => {'key2' => {'key3' => 'value'}}}

How can I iterate through this structure?

Axeman
  • 29,660
  • 2
  • 47
  • 102
Jay Gridley
  • 713
  • 2
  • 12
  • 33
  • 1
    Could you give a more realistic example. Where do you encounter such a structure? What is it used for? What do you want to do? Maybe another data structure would be more appropriate for the task? – Aurril Mar 02 '10 at 13:04
  • @Aurril: Nested hash structures are useful for many things, see the link in my post below for an example. – Zaid Mar 02 '10 at 13:13
  • Does each individual hash have more than one key? – Svante Mar 02 '10 at 13:22
  • 1
    I believe one of the perl xml parsers will parse an xml file into a nested structure of hash tables. – Kevin Kibler Mar 02 '10 at 14:16
  • Also, please read through [perldoc perldsc](http://perldoc.perl.org/perldsc.html). You can learn about hashes in depth – ghostdog74 Mar 02 '10 at 13:01

8 Answers8

26

This answer builds on the idea behind Dave Hinton's -- namely, to write a general purpose subroutine to walk a hash structure. Such a hash walker takes a code reference and simply calls that code for each leaf node in the hash.

With such an approach, the same hash walker can be used to do many things, depending on which callback we give it. For even more flexibility, you would need to pass two callbacks -- one to invoke when the value is a hash reference and the other to invoke when it is an ordinary scalar value. Strategies like this are explored in greater depth in Marc Jason Dominus' excellent book, Higher Order Perl.

use strict;
use warnings;

sub hash_walk {
    my ($hash, $key_list, $callback) = @_;
    while (my ($k, $v) = each %$hash) {
        # Keep track of the hierarchy of keys, in case
        # our callback needs it.
        push @$key_list, $k;

        if (ref($v) eq 'HASH') {
            # Recurse.
            hash_walk($v, $key_list, $callback);
        }
        else {
            # Otherwise, invoke our callback, passing it
            # the current key and value, along with the
            # full parentage of that key.
            $callback->($k, $v, $key_list);
        }

        pop @$key_list;
    }
}

my %data = (
    a => {
        ab => 1,
        ac => 2,
        ad => {
            ada => 3,
            adb => 4,
            adc => {
                adca => 5,
                adcb => 6,
            },
        },
    },
    b => 7,
    c => {
        ca => 8,
        cb => {
            cba => 9,
            cbb => 10,
        },
    },
);

sub print_keys_and_value {
    my ($k, $v, $key_list) = @_;
    printf "k = %-8s  v = %-4s  key_list = [%s]\n", $k, $v, "@$key_list";
}

hash_walk(\%data, [], \&print_keys_and_value);
FMc
  • 41,963
  • 13
  • 79
  • 132
12

Is this what you want? (untested)

sub for_hash {
    my ($hash, $fn) = @_;
    while (my ($key, $value) = each %$hash) {
        if ('HASH' eq ref $value) {
            for_hash $value, $fn;
        }
        else {
            $fn->($value);
        }
    }
}

my $example = {'key' => {'key2' => {'key3' => 'value'}}};
for_hash $example, sub {
    my ($value) = @_;
    # Do something with $value...
};
dave4420
  • 46,404
  • 6
  • 118
  • 152
9

This post may be useful.

foreach my $key (keys %hash) {
    foreach my $key2 (keys %{ $hash{$key} }) {
        foreach my $key3 (keys %{ $hash{$key}{$key2} }) {
            $value = $hash{$key}{$key2}->{$key3};
            # .
            # .
            # Do something with $value
            # .
            # .
            # .
        }
    }
}
Community
  • 1
  • 1
Zaid
  • 36,680
  • 16
  • 86
  • 155
  • In the OP, the first brackets of the data structure are curly brackets which indicates it's a hash ref. my $hash = {'key' => {'key2' => {'key3' => 'value'}}} So you will need to dereference – ccheneson Mar 02 '10 at 13:20
  • 1
    This solution only works, if there is a defined fixed number of subhashes. If the Hashstructure is autogenerated, then you need a more generic approach. A recursive Algorithm would be a better solution imo. I'm unfamiliar with Perl, otherwise I would give an example. – Aurril Mar 02 '10 at 13:33
  • @ccheneson: No need to dereference. It is what it is. – Zaid Mar 02 '10 at 13:46
7

The earlier answers show how to roll your own solution, which is good to do at least once so you understand the guts of how perl references and data structures work. You should definitely take a read through perldoc perldsc and perldoc perlref if you haven't already.

However, you don't need to write your own solution -- there is already a module on CPAN which will iterate through arbitrarily-complex data structures for you: Data::Visitor.

Ether
  • 53,118
  • 13
  • 86
  • 159
  • +1 Thanks, `Data::Visitor` looks useful. It wasn't immediately apparent from the docs how to do something simple -- for example, traversing a nested hash structure, printing leaf values and their keys (immediate and their ancestors). I'm sure it's doable; just need to wrap my head around it for a bit. :) – FMc Mar 02 '10 at 20:05
2

This is not really a new answer but I wanted to share how to do more than just print all the hash values recursively but also to modify them if needed.

Here is my ever so slight modification of the dave4420's answer in which the value is passed to the callback as a reference so my callback routine could then modify every value in the hash.

I also had to rebuild the hash as the while each loop creates copies not references.

sub hash_walk {
   my $self = shift;
    my ($hash, $key_list, $callback) = @_;
    while (my ($k, $v) = each %$hash) {
        # Keep track of the hierarchy of keys, in case
        # our callback needs it.
        push @$key_list, $k;

        if (ref($v) eq 'HASH') {
            # Recurse.
            $self->hash_walk($v, $key_list, $callback);
        }
        else {
            # Otherwise, invoke our callback, passing it
            # the current key and value, along with the
            # full parentage of that key.
            $callback->($k, \$v, $key_list);
        }

        pop @$key_list;
        # Replace old hash values with the new ones
        $hash->{$k} = $v;
    }
}

hash_walk(\%prj, [], \&replace_all_val_strings);

sub replace_all_val_strings {
    my ($k, $v, $key_list) = @_;
    printf "k = %-8s  v = %-4s  key_list = [%s]\n", $k, $$v, "@$key_list";
    $$v =~ s/oldstr/newstr/;
    printf "k = %-8s  v = %-4s  key_list = [%s]\n", $k, $$v, "@$key_list";
}
stephenmm
  • 2,640
  • 3
  • 30
  • 48
1

If you are using perl as a "CPAN interpreter" then in addition to Data::Visitor and Data::Deep there is the super simple Data::Traverse:

use Data::Traverse qw(traverse);
 
my %test_hash = (
  q => [qw/1 2 3 4/],
  w => [qw/4 6 5 7/],
  e => ["8"],
  r => { 
         r => "9"  ,
         t => "10" ,
         y => "11" ,
      } ,
);

traverse { return if /ARRAY/; print "$a => $b\n" if /HASH/ && $b > 8 } \%test_hash;

Output:

t => 10
y => 11

$a and $b are treated as special variables here (as with sort()) while inside the traverse() function. Data::Traverse is a very simple but immensely useful module with no non-CORE dependencies.

toolic
  • 57,801
  • 17
  • 75
  • 117
G. Cito
  • 6,210
  • 3
  • 29
  • 42
0
foreach my $keyname (keys(%foo) {
  my $subhash = $foo{$keyname};
  # stuff with $subhash as the value at $keyname
}
monksp
  • 929
  • 6
  • 14
0

You will have to loop through it twice. i.e.

while ( ($family, $roles) = each %HoH ) {
   print "$family: ";
   while ( ($role, $person) = each %$roles ) {
      print "$role=$person ";
   }
print "\n";
}
Marcos Placona
  • 21,468
  • 11
  • 68
  • 93