Perl Part I · Module 7 · References
Arrays of Arrays
This is the chapter that pays off a debt. Chapter 5.12 showed that putting two arrays into one array gives you neither — the boundary between them disappears and you are left with a single flat list. An array of array references is the fix, and it produces the shape most engineering data actually has: a table, with rows you can count and index independently. Everything in it is already familiar, because an array of arrays is an ordinary array whose elements happen to be references, and ordinary array operations work on it unchanged. The one genuinely new idea is that rows are independent, which means they can be different lengths — a feature rather than an oversight, and the thing that makes ragged data easy.
Intermediate17 min readPerlPart IReferencesData Structures
Part I · Module 7 · Chapter 7.6 · Arrays of Arrays
1. The Debt From Chapter 5.12
use strict;
use warnings;
my @front = (1, 2);
my @back = (3, 4);
my @flat = (@front, @back);
my $nested = [ \@front, \@back ];
printf "flat : %d element(s)\n", scalar(@flat);
printf "nested : %d element(s), first is a %s of %d\n",
scalar(@$nested), ref($nested->[0]), scalar(@{ $nested->[0] });flat : 4 element(s)
nested : 2 element(s), first is a ARRAY of 2Four elements and no rows, or two elements that are rows. (@front, @back) flattens, exactly as Chapter 5.12 established; [ \@front, \@back ] does not, because its elements are scalars holding references and Chapter 7.1 showed that scalars do not flatten.
That is the entire mechanism. Everything else in this chapter is notation.
2. Building a Table
In practice you write the rows inline with constructors rather than naming them.
use strict;
use warnings;
my $table = [
["alu", -0.12, 1840],
["fifo", 0.34, 920],
["regf", -0.05, 2310],
];
printf "rows : %d\n", scalar(@$table);
printf "columns : %d\n", scalar(@{ $table->[0] });
printf "last row index : %d\n", $#{ $table };
print "\n";
for my $row (@$table) {
printf "%-5s %+6.2f %5d\n", @$row;
}rows : 3
columns : 3
last row index : 2
alu -0.12 1840
fifo +0.34 920
regf -0.05 2310Four things in that short program are worth naming explicitly:
scalar(@$table)is the row count. The outer array's elements are rows.scalar(@{ $table->[0] })is the width of row 0 — arrow to navigate to the row reference, then@{ }to open it, which is Chapter 7.4 §4's combination.$#{ $table }is the last row index, which is what you want for a0 ..loop.printf "...", @$rowsupplies all three arguments at once. The dereferenced row is the argument list. This is one of the places where flattening — normally the thing you are fighting — is exactly what you want.
The shape, drawn
An array of arrays is one scalar, three row references, and the cells inside them
The diagram makes one point that the code does not: the rows are separate arrays. They are not laid out contiguously and nothing holds them to a common width. That is what the next two sections are about.
3. Walking by Index
Iterating rows directly — for my $row (@$table) — is usually what you want. When you need the index as well, both levels take $#{ }.
use strict;
use warnings;
my $m = [ [1, 2, 3], [4, 5, 6] ];
for my $r (0 .. $#{ $m }) {
for my $c (0 .. $#{ $m->[$r] }) {
printf "m[%d][%d] = %d\n", $r, $c, $m->[$r][$c];
}
}m[0][0] = 1
m[0][1] = 2
m[0][2] = 3
m[1][0] = 4
m[1][1] = 5
m[1][2] = 6Note $#{ $m->[$r] } in the inner loop: the width is read from the current row, not from row 0 or from a stored constant. That costs nothing and makes the loop correct for ragged data, which is the subject of §4.
$m->[$r][$c] is the cell — first subscript the row, second the column, with the arrow dropped between them per Chapter 7.4 §3.
4. Rows Can Be Different Lengths
An array of arrays is not a matrix. Nothing enforces a rectangle.
use strict;
use warnings;
my $groups = [
["clk"],
["a", "b", "c"],
[],
];
for my $i (0 .. $#{ $groups }) {
printf "group %d : %d item(s) [%s]\n",
$i, scalar(@{ $groups->[$i] }), join(" ", @{ $groups->[$i] });
}
push @{ $groups->[2] }, "rst";
printf "after push, group 2 : %d item(s) [%s]\n",
scalar(@{ $groups->[2] }), join(" ", @{ $groups->[2] });group 0 : 1 item(s) [clk]
group 1 : 3 item(s) [a b c]
group 2 : 0 item(s) []
after push, group 2 : 1 item(s) [rst]One row of one, one of three, one empty — and the empty one grows when pushed to. push @{ $groups->[2] }, "rst" is how you append to an inner row, and it is worth reading slowly: @{ ... } opens the inner array so that push has an array to work on.
This is the normal shape for grouped data, where the groups genuinely have different sizes: ports per block, violations per corner, fields per line of a report that is not fully populated.
5. Building a Table From Input
This is the pattern that matters, and the constructor placement is the whole trick.
use strict;
use warnings;
my @lines = (
"alu -0.12 1840",
"fifo 0.34 920",
"regf -0.05 2310",
);
my @table;
for my $line (@lines) {
my @fields = split ' ', $line;
push @table, [ @fields ];
}
printf "rows : %d\n", scalar(@table);
for my $row (@table) {
printf "%-5s %+6.2f %5d\n", @$row;
}
my @failing = grep { $_->[1] < 0 } @table;
printf "failing : %d\n", scalar(@failing);rows : 3
alu -0.12 1840
fifo +0.34 920
regf -0.05 2310
failing : 2push @table, [ @fields ] is the line to remember. The brackets build a new array per iteration, holding a copy of that iteration's fields, which is exactly Chapter 7.5 §2's copy behaviour being useful.
6. Everything Ordinary Still Works
An array of arrays is an array. Chapter 5.9's sort and Chapter 5.10's grep and map need no special handling — only a subscript inside the block.
use strict;
use warnings;
my @table = (
["alu", -0.12, 1840],
["fifo", 0.34, 920],
["regf", -0.05, 2310],
);
my @by_slack = sort { $a->[1] <=> $b->[1] } @table;
printf "worst first : %s %s %s\n", map { $_->[0] } @by_slack;
my @names = map { $_->[0] } @table;
printf "names : %s\n", join(" ", @names);
my @wide = grep { $_->[2] > 1000 } @table;
printf "over 1000 : %d row(s)\n", scalar(@wide);
my $total = 0;
$total += $_->[2] for @table;
printf "total paths : %d\n", $total;worst first : alu regf fifo
names : alu fifo regf
over 1000 : 2 row(s)
total paths : 5070$a->[1] and $b->[1] in the comparator, $_->[0] in the map block, $_->[2] in the grep block. The functions are unchanged; only the way you reach the field differs.
7. Why This Matters in Engineering Work
Parsed reports are tables. A timing report, a DRC summary, a coverage dump — read line by line and split, each line becomes a row, and the file becomes an array of arrays. The push @table, [ @fields ] line in §5 is the core of every parser you will write.
Ragged is the normal case. Ports per block, violations per corner, instances per cell type — real groupings have different sizes, and an array of arrays handles that without padding or sentinels. Reading each row's own length is all it takes.
And ordering is the reason to prefer this over a hash. A hash cannot preserve the order lines arrived in, as Chapter 6.5 established. When "the fourth path in the report" is a meaningful thing to say, the outer container has to be an array.
8. Common Mistakes
Expecting (@a, @b) to nest. It flattens. Use [ \@a, \@b ] or [ [@a], [@b] ].
push @table, \@fields with @fields declared outside the loop. Every row ends up referring to the same array, and all rows show the last line. Write [ @fields ].
Reading the row width from row 0. Ragged rows make that wrong. Use $#{ $row }.
Walking columns on ragged data. Missing cells are undef and may not warn.
Forgetting @{ } when pushing to an inner row. push @{ $t->[2] }, $x, not push $t->[2], $x.
Using scalar(@$table) for the column count. That is the row count.
Positional fields in long rows. Use a hash reference per row once the fields have names.
9. Exercise
Exercise 1 — the debt. Build @front and @back with two elements each. Make my @flat = (@front, @back) and my $nested = [ \@front, \@back ]. Print both counts, and ref() plus the length of $nested->[0].
Exercise 2 — a table. Build §2's three-row table with constructors. Print the row count, the width of row 0, the last row index, and the formatted table using printf "...", @$row.
Exercise 3 — index walk. Build [ [1,2,3], [4,5,6] ] and print every cell as m[r][c] = v using nested 0 .. $#{ } loops. Then change row 1 to have four elements and confirm your loop prints all four without modification.
Exercise 4 — ragged. Build the three-group structure from §4, including the empty group. Print each group's count and contents, then push onto the empty one and reprint it.
Exercise 5 — the column trap. On that ragged structure, loop over the rows printing $row->[2]. Record what appears for the short rows and whether anything reached STDERR. Then add a guard and report the difference.
Exercise 6 — build from input. Take §5's three lines, split each, and build the table with push @table, [ @fields ]. Print the row count and the formatted rows, then count the rows with a negative second field.
Exercise 7 — the shared-array bug. Repeat Exercise 6 twice with push @table, \@fields: once with my @fields inside the loop, once with it declared before the loop. Print both tables. Explain in two sentences why only one of them is wrong.
Exercise 8 — ordinary operations. On §6's table, sort by the second field ascending and print the names in order, extract all names with map, count the rows whose third field exceeds 1000, and total the third field.
10. Summary
- An array of arrays is an ordinary array whose elements are references. That is why nothing flattens and why every array operation still works.
scalar(@$table)is the row count;scalar(@{ $table->[0] })is row 0's width;$#{ $table }is the last row index.$table->[$r][$c]is a cell — row first, then field.printf "...", @$rowsupplies the whole row as arguments.- Rows are independent arrays and may have different lengths, including zero. Read each row's width with
$#{ $row }. - Walking columns assumes a rectangle. Missing cells are
undefand will not always warn. - Build from input with
push @table, [ @fields ]. With\@fieldsand a declaration outside the loop, every row refers to the same array. push @{ $t->[$i] }, $xappends to an inner row.sort,grepandmapwork unchanged —$a->[1],$_->[0].- Positional fields do not document themselves. Prefer a hash reference per row once fields have names.
Chapter 7.7 keeps the lists and replaces the outer array with a hash, which is how grouped data is actually accumulated: one named list per key, built with a single push.
Where this fits
Part of the Perl curriculum.
