Perl - Hash of hashes - is the only option to use "eval" to access variable length key-sequences? {k[0]}=>{k[1]}=>

117 views Asked by At

I am going to keep my question high level, because that is me, that is my style, and that is what I need answer for.

I have a multi-level hash to work with (hash of hashes). I also have algorithms that give me a sequence of keys in @key (to various values to look up).

My technique to access individual values is:

Simply build an expression looking like

 $h-> {$key[0] }=> {$key[1]} => ... e.t.c.

and then "eval" that expression.

Are there better techniques to deal with variable key sequences, without eval?

( The hash is a mirror of a directory structure. Values are individual files, and my program needs to read the content of those files.)

I tried and it works with the eval option.

5

There are 5 answers

0
ikegami On

Data::Diver makes this easy.

use Data::Diver qw( DiveVal Dive );

my $h;

DiveVal( $h, map \$_, @keys ) = 123;

say Dive( $h, map \$_, @keys );   # 123

You can use ${ DiveRef( ... ) } instead of DiveVal.


You could also use the following:

sub dive {
   my $x = shift;
   $x &&= $x->{ $_ } for @_;
   $x
}

sub dive_ref {
   my $p = \shift;
   $p = \( ($$p)->{ $_ } ) for @_;
   $p
}

sub dive_val :lvalue {
   my $p = \shift;
   $p = \( ($$p)->{ $_ } ) for @_;
   $$p
}

my $h;

dive_val( $h, @keys ) = 123;

say dive( $h, @keys );   # 123

You can use ${ dive_ref( ... ) } instead of dive_val.


dive_ref can be adapted to create a data structure from a file tree.

sub load_data {
   my $p       = \shift;
   my $dir_qfn =  shift;

   my ( @d_fns, @f_fns );

   {
      opendir( my $dh, $dir_qfn )
         or die( "Can't open `$dir_qfn`: $!\n" );

      while ( defined( my $fn = readdir( $dh ) ) ) {
         next if $fn =~ /^\./;

         my $qfn = "$dir_qfn/$fn";
         stat( $qfn )
            or die( "Can't stat `$qfn`: $!\n" );

         push @{ -d _ ? \@d_fns : \@f_fns }, $fn;
      }
 
      closedir( $dh )
         or die( "Error reading `$dir_qfn`: $!\n" );
   }

   ($$p)->{ ".files" } = \@f_fns;  # Or whatever.

   load_data( ($$p)->{ $_ }, "$dir_qfn/$_" ) for @d_fns;
}

load_data( my $data, "." );
1
Schwern On

If you're evaling a string, there is almost certainly a better way. If you already have a list of keys, you can walk the hash until you hit something that is not another hash.

my @keys = qw(one two three);
my $h = {
  one => {
    two => {three => 42, bar => 23}
  }
};
while( ref $h eq 'HASH' ) {
  $h = $h->{shift @keys};
}
print $h;  # 42

The hash is a mirror of a directory structure.

For your particular case, use Path::Tiny, File::Find, File::Find::Rule, or similar module to walk the directory structure. This will avoid having to load the whole directory structure into memory, and it handles the walk for you.

In the more general case, there are modules such as Data::Walk to traverse a data structure.

If you want to do it by hand, walk the hash recursively.

sub handle_files {
  my $dir = shift;

  for my $entry (values %$dir) {
    if( ref $entry eq 'HASH' ) {
      # It's a directory, recurse.
      handle_files($entry);
    }
    elsif( ref $entry eq '' ) {
      print "It's a file named $entry\n";
    }
    else {
      die "Unexpected reference found: $entry";
    }
  }
}
0
roger On

use reduce from List::Util

something like

use List::Util qw/reduce/;
my @keys = qw/a b c/;
my $h = +{ a => { b => { c => 1 } } };
my $final = reduce { $a->{$b} || {} }  $h, @keys;
0
LanX On

For completeness, Perl supports Multi-dimensional hashes which make your task quite easy

Usage:

$h{ join( $; , @keys ) }  = 1;

# or when the number is known
$h{ $keys[0], $keys[1], $keys[2], ... } = 1
  • Pro: they can be more performant and memory efficient than Hashes of Hashes
  • Con: you have to make sure that $; (or another special character) can't be part of your keys.

It really depends on your use case.

0
gugod On

A recursive solution would seem intuitive to me, and I imagine it to be easier-to-understand than the string-building-then-eval solution.

use v5.36;
use Ref::Util qw(is_hashref);

sub get ($h, $ks) {
    return $h if @$ks == 0 || !is_hashref($h);

    my $k = shift @$ks;
    return get($h->{$k}, $ks);
}

my $h = +{ a => { b => { c => 1 } } };

my $v;
$v = get($h, [qw( a b c )]); #=> 1
$v = get($h, [qw( x y z )]); #=> undef

OTOH if the data in $h mirrors a file system, I don't see why it cannot be a simple, non-nested hash, while keys being the full path such as "/foo/bar/baz.txt".... but that's obviously a different discussion.