6.6. Applying a Bit of IndirectionSome 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. |