Search code examples
perlhashhash-of-hashes

Perl: Access hash of dynamic depth


I am struggling with accessing/ modifying hashes of unknown (i.e. dynamic) depth.

Suppose I am reading in a table of measurements (Length, Width, Height) from a file, then calculating Area and Volume to create a hash like the following:

#                       #Length  Width  Height  Results
my %results = (     
                        '2' => {        
                                '3' => {        
                                        '7' => {
                                                'Area' => 6,
                                                'Volume' => 42,
                                                },
                                        },
                                },
                        '6' => {        
                                '4' => {        
                                        '2' => {
                                                'Area' => 24,
                                                'Volume' => 48,
                                                },
                                        },
                                },
                        );

I understand how to access a single item in the hash, e.g. $results{2}{3}{7}{'Area'} would give me 6, or I could check if that combination of measurements has been found in the input file with exists $results{2}{3}{7}{'Area'}. However that notation with the series of {} braces assumes I know when writing the code that there will be 4 layers of keys.

What if there are more or less and I only discover that at runtime? E.g. if there were only Length and Width in the file, how would you make code that would then access the hash like $results{2}{3}{'Area'}?

I.e. given a hash and dynamic-length list of nested keys that may or may not have a resultant entry in that hash, how do you access the hash for basic things like checking if that key combo has a value or modifying the value?

I almost want a notation like:

my @hashkeys = (2,3,7);

if exists ( $hash{join("->",@hashkeys)} ){
    print "Found it!\n";
}

I know you can access sub-hashes of a hash and get their references so in this last example I could iterate through @hashkeys, checking for each one if the current hash has a sub-hash at that key and if so, saving a reference to that sub-hash for the next iteration. However, that feels complex and I suspect there is already a way to do this much easier.

Hopefully this is enough to understand my question but I can try to work up a MWE if not.

Thanks.


Solution

  • So here's a recursive function which does more or less what you want:

    sub fetch {
        my $ref = shift;
        my $key = shift;
        my @remaining_path = @_;
    
        return undef unless ref $ref;
        return undef unless defined $ref->{$key};
        return $ref->{$key} unless scalar @remaining_path;
        return fetch($ref->{$key}, @remaining_path);
    }
    
    fetch(\%results, 2, 3, 7, 'Volume');  # 42
    fetch(\%results, 2, 3);               # hashref
    fetch(\%results, 2, 3, 7, 'Area', 8); # undef
    fetch(\%results, 2, 3, 8, 'Area');    # undef
    

    But please check the comment about bad data structure which is already given by someone else, because it's very true. And if you still think that this is what you need, at least rewrite it using a for-loop, as perl does not optimize tail recursion.