http://www.perlmonks.org?node_id=740274

zerohero has asked for the wisdom of the Perl Monks concerning the following question:

Are there versions of map and grep that operate on hashes, rather than arrays? I'm looking for terse, well behaved, faithful cousins of array map and grep (or perhaps techniques using normal map and grep, but these seem un-terse).

These would iterate over the (k,v) pairs in the hash, providing them in v = $_, k = something, and produce a new hash.

hmap would only change the value (key is the same, it's part of the identity of the pair), but could be used in the BLOCK. hgrep includes (k,v) if expr is true.

I can come up with an implementation, but is there a really good one already done? Really good = on level of normal map and grep.

Thanks, Roger

Replies are listed 'Best First'.
Re: map and grep (but for hashes)
by BrowserUk (Patriarch) on Jan 30, 2009 at 19:28 UTC

    Not pre-existing, but something along these lines?

    #! perl -slw use strict; sub hmap(&%) { use vars qw[ $h $v ]; my $code = shift; my @rv; local( $h, $v ) = splice( @_, 0, 2 ), push @rv, $code->() while @_; return @rv } sub hgrep (&%) { use vars qw[ $h $v ]; my $code = shift; my @rv; local( $h, $v ) = splice( @_, 0, 2 ), push @rv, $code->() ? ( $h, $v ) : () while @_; return @rv; } my %h = 'a' .. 'z'; print %h; my %r = hmap{ $h => ++$v } %h; print %r; my %s = hgrep{ $h le 'm' } %r; print %s; __END__ [19:25:50.05] C:\test>hmap wxefabmnstyzuvcdklqrghijop egwyacmosuyaauwcekmgiqsoqik egcekmacgimoik

    Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
    "Science is about questioning the status quo. Questioning authority".
    In the absence of evidence, opinion is indistinguishable from prejudice.

      Wonderful! But slightly off the specs. Well, perhaps. ;-)

      #!perl -l # sub hmap (&%) { my $code = shift; local $_; my @rv; push @rv, shift, $code->($_=shift) while @_; @rv; } sub hgrep (&%) { my $code = shift; local $_; my @rv; push (@rv, ($code->($_=$_[1]) ? (shift,$_):())),shift while @_; @rv; } %h = (foo => 1, bar => 2, baz => 3); %new = hmap { ++$_ } %h; print "hmap result:"; print "$_ => $new{$_}" for keys %new; %new = hgrep { /2/ } %h; print "hgrep result:"; print "found $_ => $new{$_}" for keys %new; __END__ hmap result: bar => 3 baz => 4 foo => 2 hgrep result: bar => 2

      update: well, this operates on the value only, localizing $_ and doesn't create package vars. For the key... hm. What do we have? $", $/, $\ ... - no, they could be used inside the block. But then... we could localize @_ here!

      sub hmap (&%) { my $code = shift; my @i = @_; local @_; my @rv; push @rv, $code->(@_=(shift @i,shift @i)) while @i; @rv; } %h = (foo => 1, bar => 2, baz => 3); %new = hmap { $_[0]."l",++$_[1] } %h; print "hmap result:"; print "$_ => $new{$_}" for keys %new; __END__ hmap result: bazl => 4 barl => 3 fool => 2

      update 2: BrowserUk, as you may have noticed, I ommitted the 'hgrep' part here, since my solution doesn't work - seem to not dunno how to change its arguments inside the block... ;-)

        But slightly off the specs.

        You're missing the " k = something," part of the spec :) How about?

        #! perl -slw use strict; sub hmap(&%) { use vars qw[ $h ]; my $code = shift; my @rv; local( $h, $_ ) = splice( @_, 0, 2 ), push @rv, $h, $code->() while @_; return @rv } sub hgrep (&%) { use vars qw[ $h ]; my $code = shift; my @rv; local( $h, $_ ) = splice( @_, 0, 2 ), push @rv, $code->() ? ( $h, $_ ) : () while @_; return @rv; } my %h = 'a' .. 'z'; print %h; my %r = hmap{ ++$_ } %h; print %r; my %s = hgrep{ $h le 'm' } %r; print %s; __END__ [19:25:50.05] C:\test>hmap wxefabmnstyzuvcdklqrghijop egwyacmosuyaauwcekmgiqsoqik egcekmacgimoik

        Examine what is said, not who speaks -- Silence betokens consent -- Love the truth but pardon error.
        "Science is about questioning the status quo. Questioning authority".
        In the absence of evidence, opinion is indistinguishable from prejudice.

        True, ok, so one refinement would be you'd likely need some way to look at the key, and not just the value. It seems like the way to do this is by using a local variable as in the previous example.

      Very nice. As Wayne and Garth would say "We're not worthy!" This is much better than what I would have come up with, and I'm going to have to study some of the little details here.

      The cool thing is this seems to look and smell like map and grep but for hashes.

      God I love perl.

      THANKS!

Re: map and grep (but for hashes)
by DStaal (Chaplain) on Jan 30, 2009 at 19:17 UTC

    In general, I find using map or grep on keys %hash is usually all you need. If you really want to get complex, there are some ideas in the book 'Higher Order Perl' that allow you to do some interesting things.

    But honestly, what's wrong with: grep { $hash{$_} == $expr } keys %hash; ?

      The result of this expression is an array, not a hash. So now we need to combine the values from the keys and produce a hash. The expression is starting to get long (esp. compared with how map and grep look for arrays).

      A lot of transformations want me to keep things as hashes. I'd describe most data structures in perl as being multi-level hashes, with a smattering of arrays, not vice versa.

        The expression above will return a list of keys. Then getting your filtered hash is as simple as:
        my %new_hash; @new_hash{@filtered_keys} = @old_hash{@filtered_keys};
Re: map and grep (but for hashes)
by ikegami (Patriarch) on Jan 30, 2009 at 20:18 UTC

    Are there versions of map and grep that operate on hashes, rather than arrays?

    Always reducing things to array

    Just a nit: map and grep know nothing of arrays. They operate on lists, not arrays.
      Good point, thanks for the correction.
Re: map and grep (but for hashes)
by leocharre (Priest) on Jan 30, 2009 at 19:31 UTC

    Give a pseudocode example of how it would behave.

    I have a gut feeling you'll get fifty examples of how you can already do it - really well, with little code.

    Perl is merciless for already being able to do whatever you're thinking it should be able to do.

    Here's an example. If you look at the op, the whole thing is humbling. I thought that ikegami was conjuring magic, but he pointed out it's in perldoc..
    Makes you wonder just what the diff between a mediocre dev like myself and a real guru is.. maybe it's that they RTFM.

      I'm kind of hoping for 50 answers. With 2 of them possibly shedding new light on it. I'm also humbled by how clever people can be with these types of questions. But it is the place for a Seeker of Perl Wisdom to be humbled!

Re: map and grep (but for hashes)
by lostjimmy (Chaplain) on Jan 30, 2009 at 19:31 UTC
    No matter how good I get at Perl, I always find myself confused when using map with a hash. I quite often end up with something like %new_hash = map { (condition involving $hash{$_}) ? ($_, $hash{$_}) : () } keys %hash. It's the one thing I always struggle with, and I'm sure there's a better way. Anyone care to enlighten?

      It would depend on what you are trying to do, probably. The one thing I've noticed is that usually I don't actually need a new hash: I just need to know which values are in the new hash.

      My other trick would be to split it into two lines, and use a hash slice:

      @new_keys = grep { $hash{$_} == $condition } keys %hash; @new_hash{@new_keys} = @hash{@new_keys};

      I'm sure there are other ways, but that's fairly short and clear.

      (Fixed: Thanks lostjimmy.)

        I agree, I do find that I almost always need just the keys. I think I just always end up thinking too much about so I come up with something convoluted. It's like my brain turns off when it comes to map and hashes.

        BTW, that should be @new_keys = grep, not map

        >> @new_keys = grep { $hash{$_} == $condition } keys %hash; >> @new_hash{@new_keys} = @hash{@new_keys};

        OK, sorry I missed this second line. This is how you put everything together. Fairly terse. I'd imagine map is a little less terse.

        So I think this argues for a little function. I just wanted to make sure that there wasn't any really terse form that eliminated the need for a function.

      That's part of the motivation for my question. One of the things perl got absolutely right was building hash and array into the DNA of the language. I'm shuddering for a moment thinking of the "platypus" like "array+hash" thingees in php.

      So I'm trying to get a good TIMTOWTDI overview and make some a good selection from the various answers.

Re: map and grep (but for hashes) (List::Pairwise)
by lodin (Hermit) on Jan 31, 2009 at 11:59 UTC

    Does the grepp and mapp functions in List::Pairwise do what you want? $a and $b is used for the keys and the values. The synopsis shows some simple examples.

    I'm also a fan of using pair (from the same module) when suitable, and in particular when I need to sort the data. I often use a hash when I really want a list of tuples. pair gives me that and when I want to use e.g. grep I simply use $_->[0] for the key and $_->[1] for the value. No extra magic needed. If I want to turn it into a hash-like list I simply do map @$_, .... (Using pair can be excessively memory consuming though. mapp should not suffer from that.)

    lodin

Re: map and grep (but for hashes)
by rir (Vicar) on Jan 30, 2009 at 22:01 UTC
    I'm not up on guts but just being able to access a hashes state might help a bit:
    print each %hash until %hash->ITER_END;
    Reusing $a and $b might be worth a look.

    I'm not sure if you also want rhgrep and rhmap, which would mean either the hash or $_ is readonly.

    Be well,
    rir

Re: map and grep (but for hashes)
by metaperl (Curate) on Feb 03, 2009 at 18:19 UTC