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

I am trying to follow advice here that suggests map and grep rather than foreach where possible. Currently I use foreach to find the rank of a particular hashref in a sorted array. How could I make the grep below work? ($rank_in_sorted_array) = grep $_->{my_target} @sorted_array;

Replies are listed 'Best First'.
Re (tilly) 1: grep instead of foreach?
by tilly (Archbishop) on May 22, 2001 at 06:49 UTC
    Where is here?

    Scratch that.

    Wherever "here" is, that advice is bad. If you are not using the return value, do not use grep or map! Why not? Well you have made the code conceptually more complex, you are forcing Perl to store more information so it wastes memory, you are forcing Perl to do more work so it runs more slowly, and for what? Just to look cool???

    If you will use the return value sensibly, then by all means use map or grep - that is why they exist. But don't use them to replace loops that could be more clearly written as loops!

    In this case a grep actually is pretty natural:

    my ($rank) = grep {$sorted_array[$_] eq $my_target} 0..$#sorted_array;
    But even so, I would prefer to see it bundled into a function like so:
    sub index_of { my $thing = shift; my @indexes = grep $_[$_] eq $thing, 0..$#_; wantarray ? @indexes : $indexes[0]; }
    and now you can write:
    my $rank = index_of($my_target, @sorted_array);
    which is already 2 wins. First of all you lose having to worry about scalar vs list context (I get that right in the function). Secondly rather than looking at a complex construct and saying, "What does that do?" you can look at a function name that does a decent job of documenting it. And if you wanted to make it more efficient by only searching until you find your (hopefully unique) element, well you can.

    But if you were going to need more than a half-dozen or so ranks, you are probably better off producing a reverse hash lookup like so:

    my %sorted_index; $sorted_index{$sorted_array[$_]} = $_ foreach 0..$#sorted_array;
    Now I can just see your co-workers snort. Why doesn't he use a map? After all there is a perfectly good map out there where you use the return value:
    my %sorted_index = map {($_, $sorted_array[$_])} 0..$#sorted_array;
    And of course there is a perfectly good reason not to do this. And it is because I don't know whether you are using 5.6.1. If you are then the map will work very nicely. If you are not then a map that produces 2 for 1 like this is very slow. Try:
    my @doubled = map {($_, $_)} 1..50_000;
    to see what I mean.

    Secondly I wanted to point out the inline loop construct that allows them to feel like cool Perl hackers without the bad grep/map constructs. But the cool Perl hackers tend to solve that problem something like this:

    my %sorted_index; @sorted_index{@sorted_array} = 0..$#sorted_array;

    UPDATE
    Albannach caught an accidental pair of opening code tags rather than closing it. Fixed.

      That is a cool writeup.

      I have never seen this before:

      my %sorted_index; @sorted_index{@sorted_array} = 0..$#sorted_array;
      Can you explain to me how it works? Is it in the documentation?

      -- iakobski

        is using a hash-slice there. The idea is: to address more then one entry in the hash at once. a simple example:
        my %languages; $languages{'python'} = 'boring'; @languages{'perl', 'perl6'} = ('cool', 'real soon now'); # this is short for ( $languages{'perl'} , $languages{'perl6'} ) = ('cool', 'real soon now'); # or $languages{'perl'} = 'cool'; $languages{'perl6'} = 'real soon now';

        There are also array-slices.

        Read perlman:perldata

        --
        Brigitte    'I never met a chocolate I didnt like'    Jellinek
        http://www.horus.com/~bjelli/         http://perlwelt.horus.at