Item 35: Use map and grep to manipulate complex data structures. Sometimes it's useful to take a "slice" out of a multidimensional array or hash, or to select slices that have certain characteristics. Conversely, you may need to assemble a collection of lists into a 2-D array, or perhaps assemble a collection of 2-D arrays into a 3-D array. Perl's map and grep operators are a perfect choice for chores like these. Slicing with map Let's begin with a program that reads a file containing 3-D coordinates into memory: Reading a file of 3-D coordinates into memory This program will read a file of 3-D coordinates into memory. Each line of the file will contain the x, y, and z coordinates of a single point, separated by whitespace. For example: | # point data 1 2 3 4 5 6 9 8 7 | | open POINTS, "points" or die "couldn't read points data: $!\n"; while (<POINTS>) { next if /^\s*#.*$/; push @xyz, [ split ]; } | Skip comments, then split a line into 3 values, put 'em in an anonymous array, and append it to @xyz . | foreach $pt (@xyz) { print "point ", $i++, ": x = $pt->[0], y = $pt->[1], ", "z = $pt->[2]\n"; } | Prints: point 1: x = 1, y = 2, z = 3 point 2: x = 4, y = 5, z = 6 point 3: x = 9, y = 8, z = 7 | The point data is read into a structure that looks like the following: Now, let's suppose you would like to have just the x (0th) element from each point, as indicated by the shading in the figure. You could write a loop using an explicit index, or perhaps use a foreach loop: for ($i = 0; $i < @xyz; $i++) { push @x, $xyz[$i][0]; } | Here's a for loop with an explicit index. | foreach (@xyz) { push @x, $_->[0]; } | The same general idea, with a foreach loop. | But, really, this is a natural application for map : Use map to take slices of complex data structures. @x = map { $_->[0] } @xyz; | Select the 0th element from each anonymous array in @xyz . | Nesting with map On the other hand, suppose that you are starting out with parallel arrays @x , @y , and @z containing vectors of points: You now would like to assemble them into a single 3-D structure like the one shown earlier. Once again, you could use some sort of explicit looping structure: for ($i = 0; $i < @x; $i++) { $xyz[$i][0] = $x[$i]; $xyz[$i][1] = $y[$i]; $xyz[$i][2] = $z[$i]; } | Turn @x , @y , and @z into @xyz , the slow and tedious way. | However, map provides a much more elegant alternative: Use [ ] inside map to create more deeply nested structures. @xyz = map { [ $x[$_], $y[$_], $z[$_] ] } 0 .. $#x; | Turn @x , @y , and @z into @xyz , the Perl-ish way. | No doubt you can envision a host of variations on the slicing and nesting themes. For example, switching the x (0th) and y (1st) coordinates: @yxz = map { [ $_->[1], $_->[0], $_->[2] ] } @xyz; | Swap x and y coordinates. | @yxz = map { [ @$_[1, 0, 2] ] } @xyz; | It's prettier using a slice. | Or, perhaps, creating a new list containing the magnitudes of the points: @mag = map { sqrt( $_->[0] * $_->[0] + $_->[1] * $_->[1] + $_->[2] * $_->[2] ) } @xyz; | Compute magnitude of each point in @xyz and put results into a list. | The Schwartzian Transform (see Item 14) is an application that uses both slicing and nesting operations with map : @sorted_by_mtime = map { $_->[0] } sort { $a->[1] <=> $b->[1] } map { [ $_, -M $_ ] } @files; | . . . then slice. | First, nest . . . | Selecting with grep Suppose that you would like to filter @xyz so that it contains only points whose y coordinate is greater than its x coordinate. You could write a loop (how did you guess I was going to say that?): foreach $pt (@xyz) { if ($pt->[1] > $pt->[0]) { push @y_gt_x, $pt; } } | Select points with y > x, using a foreach loop. | But this time, we have a task that is perfectly suited to grep : Use grep to select elements from nested structures. @y_gt_x = grep { $_->[1] > $_->[0] } @xyz; | Select points with y > x. | Of course, you can combine map and grep for example, to gather up the x coordinates of the points with y greater than x : @x = map { $_->[0] } grep { $_->[1] > $_->[0] } @xyz; | Select x coordinates for points with y > x. | @x = map { $_->[1] > $_->[0] ? ($_->[0]) : () } @xyz; | Select x coordinates for points with y > x, another way. | |