Sunday, April 8, 2012

multi-dimensional hash slices in Perl

I've tripped over this enough times that you'd think I'd remember it by now, but I never do. And looking up the answer on google always seems to take me an age. So, in keeping with the title of this blog, I shall squirrel it away here in hopes that I'll remember to look here first next time...

I love slices in perl. They're brilliant. But they really don't quite do what you wish they'd do if you're trying to take a slice in the first dimension of a multi-dimensional hash. So if I've got:

%thing = ( "A" => [ "A0","A1","A2","A3" ],
"B" => ["B0","B1","B2","B3"],
"C" => ["C0","C1","C2","C3"]
);

I can happily slice up the last dimension on a constant "column" of the first:

print join ", ", @{$thing{B}}[1..2], "\n";

I can't do the reverse:

# doesn't work
#print join ", ", @thing{B..C}[1], "\n";

There are a couple of ways around this, some of which only work in certain circumstances:

print join ", ", ( map {$_->[1]} @thing{B..C} ), "\n";

works for output, but you don't have a ref to the original object anymore, so you can't change it. So while:

foreach ( @{$thing{B}}[1..2] ) {
s/B/E/;
}

Does what you'd expect - changes the values in B1 and B2:

foreach ( map {$_->[1]} @thing{B..C} ) {
s/1/4/;
}

...doesn't do the equivalent. Instead you need something like:

foreach ( B..C ) {
$thing{$_}[1] =~ s/1/4/;
}

(All of which seems pretty trivial from those examples, but when you try to do something complicated then being able to do all the referencing in the foreach parameter, and just work with $_ inside makes the code a LOT easier to read...)