iLikeDirt iLikeDirt - 7 months ago 9
Perl Question

Automatically call hash values that are subroutine references

I have a hash with a few values that are not scalar data but rather anonymous subroutines that return scalar data. I want to make this completely transparent to the part of the code that looks up values in the hash, so that it doesn't have to be aware that some of the hash values may be anonymous subroutines that return scalar data rather than just plain old scalar data.

To that effect, is there any way to have the anonymous subroutines executed when their keys are accessed, without using any special syntax? Here's a simplified example that illustrates the goal and the problem:

#!/usr/bin/perl

my %hash = (
key1 => "value1",
key2 => sub {
return "value2"; # In the real code, this value can differ
},
);

foreach my $key (sort keys %hash) {
print $hash{$key} . "\n";
}


The output I would like is:

perl ./test.pl
value1
value2


Instead, this is what I get:

perl ./test.pl
value1
CODE(0x7fb30282cfe0)

Answer

There's a feature called "magic" that allows code to be called when variables are accessed.

Adding magic to a variable greatly slows down access to that variable, but some are more expensive than others.

  • There's no need to make access to every element of the hash magical, just some values.
  • tie is an more expensive form of magic, and it's not needed here.

As such, the most efficient solution is the following:

use Time::HiRes     qw( time );
use Variable::Magic qw( cast wizard );

{
   my $wiz = wizard(
      data => sub { my $code = $_[1]; $code },
      get => sub { ${ $_[0] } = $_[1]->(); },
   );

   sub make_evaluator { cast($_[0], $wiz, $_[1]) }
}

my %hash;
$hash{key1} = 'value1';
make_evaluator($hash{key2}, sub { 'value2@'.time });

print("$hash{$_}\n") for qw( key1 key2 key2 );

Output:

value1
value2@1462548850.76715
value2@1462548850.76721

Other examples:

my %hash; make_evaluator($hash{key}, sub { ... });
my $hash; make_evaluator($hash->{$key}, sub { ... });

my $x; make_evaluator($x, sub { ... });
make_evaluator(my $x, sub { ... });

make_evaluator(..., sub { ... });
make_evaluator(..., \&some_sub);

You can also "fix up" an existing hash. In your hash-of-hashes scenario,

my $hoh = {
   { 
      key1 => 'value1',
      key2 => sub { ... },
      ...
   },
   ...
);

for my $h (values(%$hoh)) {
   for my $v (values(%$h)) {
      if (ref($v) eq 'CODE') {
         make_evaluator($v, $v);
      }
   }
}