6.6. Applying a Bit of Indirection
Some problems that may appear very complex are actually simple once we've seen a solution or two. For example, suppose we want to find the items in a list that have odd digit sums but don't want the items themselves. What we want to know is where they occurred in the original list.
All that's required is a bit of indirection
. First, we have a selection problem, so we use a grep. Let's not grep the values themselves but the index for each item:
my @input_numbers = (1, 2, 4, 8, 16, 32, 64);
my @indices_of_odd_digit_sums = grep {
...
} 0..$#input_numbers;
Here, the expression 0..$#input_numbers will be a list of indices for the array. Inside the block, $_ is a small integer, from 0 to 6 (seven items total). Now, we don't want to decide whether $_ has an odd digit sum. We want to know whether the array element at that index has an odd digit sum. Instead of using $_ to get the number of interest, use $input_numbers[$_]:
my @indices_of_odd_digit_sums = grep {
my $number = $input_numbers[$_];
my $sum;
$sum += $_ for split //, $number;
$sum % 2;
} 0..$#input_numbers;
The result will be the indices at which 1, 16, and 32 appear in the list: 0, 4, and 5. We could use these indices in an array slice to get the original values again:
my @odd_digit_sums = @input_numbers[ @indices_of_odd_digit_sums ];
The strategy here for an indirect grep or map is to think of the $_ values as identifying a particular item of interest, such as the key in a hash or the index of an array, and then use that identification within the block or expression to access the actual values.
Here's another example: select the elements of @x that are larger than the corresponding value in @y. Again, we'll use the indices of @x as our $_ items:
my @bigger_indices = grep {
if ($_ > $#y or $x[$_] > $y[$_]) {
1; # yes, select it
} else {
0; # no, don't select it
}
} 0..$#x;
my @bigger = @x[@bigger_indices];
In the grep, $_ varies from 0 to the highest index of @x. If that element is beyond the end of @y, we automatically select it. Otherwise, we look at the individual corresponding values of the two arrays, selecting only the ones that meet our match.
However, this is a bit more verbose than it needs to be. We could simply return the boolean expression rather than a separate 1 or 0:
my @bigger_indices = grep {
$_ > $#y or $x[$_] > $y[$_];
} 0..$#x;
my @bigger = @x[@bigger_indices];
More easily, we can skip the step of building the intermediate array by simply returning the items of interest with a map:
my @bigger = map {
if ($_ > $#y or $x[$_] > $y[$_]) {
$x[$_];
} else {
( );
}
} 0..$#x;
If the index is good, return the resulting array value. If the index is bad, return an empty list, making that item disappear.
