Fun with eval

gav on 2003-07-15T02:27:50

I had a little problem while working on a site for a client. The structure was defined using a field that contained the item's parents seperated by colons. For example, "A:B:C", meant that the root was A which had a child B which had a child C (which is the parent of the item).

After starting doing something far too complicated I came up with an easier way. Why not use eval? You end up with something a simple as:

my $hash = join '', map "{$_}", split /:/, $path;
eval "\$tree->$hash ||= 1";

This relies on the Perl's autovivification. Remember, Data::Dumper is your friend.


remarkably similar

gizmo_mathboy on 2003-07-15T02:47:52

It's interesting to see that structure. It's similar to what I did in a mysql database in that the child had a field for it's parent. In hindsight maybe I should have created another field that listed the geneology of the child. I seemed to be determining the parents of a child much more than the children of a parent.

Don't do this with eval!

merlyn on 2003-07-15T06:26:21

Well, go ahead and do this with eval if you're not concerned with either speed or security.

Eval means you're firing up the compiler. Fun fun. Slow slow.

Eval also means that you have to be very careful about the data. It'd be trivial to launch arbitrary code by selecting the proper data here.

There's really no need. You can run a lot faster and a lot safer with:

$tree ||= {};
my $place = $tree;
$place = $place->{$_} ||= {} for split /:/, $path;
(This may not be precisely the code, but this seemed to handle the cases I ran it on.)

Also note that in your version, first following "A::B" then "A::B::C" means you were storing data into %1 symbolically (disabled with "use strict"). Bad.

Re:Don't do this with eval!

Juerd on 2003-07-15T06:44:22

$tree ||= {};
my $place = $tree;
$place = $place->{$_} ||= {} for split /:/, $path;

Something like that lets you end with a HASHref instead of the SCALARref. Unfortunately, you can't create a reference to a hash element's value part.

But adding something like a _value key solves that:
sub walk_tree {
    my ($hashref, @path) = @_;
    $hashref = \%{ $hashref->{$_} } for @path;
    return $hashref;
}
 
my $tree = {};
${ walk_tree $tree => qw(A B C) }{_value} = "value";
 
use Data::Dumper;
print Dumper($tree);

Tied hash

runrig on 2003-07-15T18:40:17

You could do something like what Win32::TieRegistry does, and create a tied hash class which just accepts a delimited string as a hash key, but internally breaks it up into separate keys. That is, if there isn't already a generic module which does this :-)

Re:Tied hash

runrig on 2003-07-15T22:08:04

And on that note, here's a very incomplete tied hash class. Possible improvements (besides making it a complete tied hash class), might be to add options for recursively making tied hashes, so you could say something like: $href = $hash{"A:B"}; $value = $href->{"C:D"}; (Note: the way it is now, only the top level hash is tied).
#!/usr/bin/perl

use strict;
use warnings;
use Data::Dumper;

package Tie::Hash::DelimitedKey;

sub TIEHASH {
  my ($class, $delimiter) = @_;
  $delimiter = ":" unless defined $delimiter;
  my $re = qr/\Q$delimiter/ unless ref($delimiter);
  bless {DELIM=>$re, HASH=>{}}, $class;
}

sub STORE {
  my $self = shift;
  my ($href, $last_key) = $self->_walk_keys(shift);
  $href->{$last_key} = shift;
}

sub FETCH {
  my $self = shift;
  my ($href, $last_key) = $self->_walk_keys(shift);
  $href->{$last_key};
}

sub _walk_keys {
  my ($self, $key) = @_;
  my $href = $self->{HASH};
  my @keys = split $self->{DELIM}, $key;
  my $last_key = pop @keys;
  for (@keys) {
    $href->{$_} = {} unless exists $href->{$_};
    $href = $href->{$_};
  }
  return $href, $last_key;
}

package main;
my $href = tie my %hash, 'Tie::Hash::DelimitedKey';

$hash{"A:B:C:D"} = 5;
print $hash{"A:B:C:D"},"\n";
my $sref = \$hash{"A:B:C:D"};
$$sref = 6;
print Dumper($href);