Perl Part I · Module 9 · Loops
Changing the Loop Variable Changes the Array
A foreach loop variable is not a copy of the array element. It is another name for it, which means the loop body can write to the array without ever mentioning the array. Used deliberately that is the shortest way in Perl to clean, normalise or scale every item of a list — one line, no index, no assignment back. Used by accident it means a loop that looked like it was only inspecting the data has quietly rewritten it, and the next thing that reads that array sees the modified version with nothing to indicate why. There is also a sharper edge: whether writing through the alias is allowed depends on what the item actually is, so one list can contain items that accept a write and items that make the program die.
Intermediate14 min readPerlPart IControl FlowLoops
Part I · Module 9 · Chapter 9.2 · Changing the Loop Variable Changes the Array
1. The Loop Body Rewrote the Array
use strict;
use warnings;
my @blocks = ("alu ", " fifo", "regf ");
for my $b (@blocks) {
$b =~ s/^\s+|\s+$//g;
}
printf "after the loop: [%s]\n", join("|", @blocks);after the loop: [alu|fifo|regf]@blocks is not mentioned inside the loop. There is no $blocks[$i] = ..., no assignment back, no index. The array still changed.
$b is an alias — a second name for the element currently being visited. The substitution matched on $b and therefore matched on the element, and the trimmed values are what @blocks now holds.
That is the whole mechanism, and the rest of this chapter is its consequences.
2. Used Deliberately, It Is the Shortest Form Perl Has
s/\s+//g for @names; # strip all whitespace
$_ = uc $_ for @names; # upper-case every item
$_ *= 1000 for @delays; # convert to picoseconds
chomp for @lines; # drop trailing newlinesnames : [ALU|FIFO]
delays : [1500|2250]
lines : [a|b]Each is one line that rewrites the whole array. The alternatives are longer and have more to get wrong:
for my $i (0 .. $#names) {
$names[$i] =~ s/\s+//g;
}
@names = map { my $c = $_; $c =~ s/\s+//g; $c } @names;The first needs an index and a bound; the second needs a copy inside the block to avoid Chapter 5.10's map aliasing, and reassigns the array. The aliased foreach needs neither.
This is a legitimate and idiomatic Perl pattern. The rule is simply to be deliberate about it — if the loop is meant to modify, say so in a comment or a name, because the code itself does not announce it.
3. Writing to a Literal Is Fatal
The alias points at whatever the list item actually is — and a literal in your source has no storage you are allowed to change.
use strict;
use warnings;
for my $b ("alu", "fifo") {
$b = uc $b;
}Modification of a read-only value attempted at alias-literal.pl line 5.255Reading the same literal is perfectly fine:
use strict;
use warnings;
for my $b ("alu", "fifo") {
my $u = uc $b;
print "$u ";
}
print "\n";ALU FIFO0The error is about writing, not about aliasing. The alias to a literal is created either way; it only fails when something tries to change it. That is the same rule Chapter 7.2 §5 measured for a reference to a literal, and the same diagnostic.
4. Avoiding the Aliasing
Two forms, both measured, both leaving the original untouched.
use strict;
use warnings;
my @blocks = ("alu ", " fifo");
for my $b (my @copy = @blocks) {
$b =~ s/\s//g;
}
printf "iterating a copy : original [%s]\n", join("|", @blocks);
for my $i (0 .. $#blocks) {
my $b = $blocks[$i];
$b =~ s/\s//g;
}
printf "copy inside body : original [%s]\n", join("|", @blocks);iterating a copy : original [alu | fifo]
copy inside body : original [alu | fifo]| Form | Effect |
|---|---|
for my $b (@blocks) | $b aliases the element — writes change the array |
for my $b (my @copy = @blocks) | iterates a fresh array — the original is safe |
for my $i (0 .. $#blocks) { my $b = $blocks[$i]; ... } | explicit copy per item — the original is safe |
The second form is the one to write when you want a guarantee, because the copy is visible on the line that creates it. The third is useful when you need the index anyway.
Note that my @copy = @blocks is a one-level copy, so if the elements are references the inner data is still shared. Measured:
original row 0 blk = CHANGEDChapter 7.13 is the chapter on that, and it applies here unchanged: copying a list of hash references protects the list, not the records. So the copy form guards against the aliasing in this chapter and not against writes that reach through a reference.
5. Why This Matters in Engineering Work
It is the shortest correct way to normalise parsed data. Tool output arrives with trailing whitespace, stray carriage returns and inconsistent case. s/\s+$// for @lines; fixes a whole file's worth in one line, with no index and no reassignment.
And it is a real source of action at a distance. A loop that reads like a report generator — formatting each row, printing it — can rewrite the array it was formatting if one line in the body assigns to the loop variable. The next function to read that array gets the modified values, and nothing in either function mentions the other. That is the kind of bug that gets attributed to the parser.
The mixed-list failure is the one to design around. (@from_somewhere, "a literal") is a natural thing to write, and a loop over it that writes will die on the last item after modifying the others. Building the array first removes the whole class.
6. Common Mistakes
Assuming the loop variable is a copy. It is an alias; writes change the array.
Writing to the loop variable in a loop meant to inspect. The array changes silently.
Writing through an alias to a literal. Modification of a read-only value attempted, exit 255 — and a substitution dies whenever the operator runs, even if the pattern cannot match.
Looping over (@array, "literal") with a body that writes. It succeeds on the array items and dies on the literal, part way through.
Expecting my @copy = @array to protect nested data. It is one level deep.
Forgetting values %h aliases as well. for my $v (values %h) { $v = ... } rewrites the hash.
Using map to transform in place. map { $_ = ... } modifies the source too — Chapter 5.10 measured it.
7. Exercise
Exercise 1 — the aliasing. Build a three-element array with leading and trailing spaces. Trim it with for my $b (@a) { $b =~ s/^\s+|\s+$//g } and print the array afterwards. Confirm you never mentioned the array inside the loop.
Exercise 2 — the one-liners. Using the statement-modifier form, write four separate one-line transforms over an array: strip whitespace, upper-case, multiply by 1000, and chomp. Confirm each changed the array.
Exercise 3 — the literal. Write for my $b ("alu","fifo") { $b = uc $b } and record the exact error and exit status. Then change the body to read only and confirm it runs clean.
Exercise 4 — the mixed list. Reproduce §3's mixed-list program with eval. Record both item outcomes and the final array contents. Then remove the eval and record the exit status and the array state at the point of death.
Exercise 5 — when does a substitution actually die? On a loop over a single literal, run three bodies: s{^\./}{} where the pattern does not match, s/zzz/y/ where it cannot match, and s/zzz/y/ if 0. Record which survived. State the rule in one sentence using the word "runs".
Exercise 6 — the data-dependent failure. Write §3's guarded loop over (@files, "default.rpt") with the body s{^/abs/}{} if $p =~ m{^/abs/};. Run it with relative paths and with one absolute path, and confirm both pass. Then change the literal to "/abs/default.rpt" and record the error. Finally fix it by building my @all = (@files, "default.rpt"); first.
Exercise 7 — avoiding it. Trim an array three ways: aliased, through my @copy = @a, and with an explicit copy inside an indexed loop. Print the original after each and report which left it unchanged.
Exercise 8 — the hash case. Scale every value of a two-key hash by 10 using for my $v (values %h). Then try the same with keys and report what happens.
8. Summary
- The
foreachvariable is an alias, not a copy. Assigning to it changes the array, without the array being named in the loop. - Used deliberately it is the shortest in-place transform Perl has —
s/\s+//g for @names;and similar. - Writing through an alias to a literal is fatal:
Modification of a read-only value attempted, exit 255. Reading it is fine. - A mixed list succeeds on array elements and dies on literals — measured: item 1 written, item 2 fatal, and the array already modified.
- A substitution on a read-only item dies whenever the operator runs, even when the pattern cannot match. It survives only if guarded so it never executes — which is why a guarded write can pass for years and then die when a literal starts satisfying the guard.
- Build the array first rather than looping over
(@array, "literal")when the body writes. for my $b (my @copy = @a)and an explicit copy inside the body both protect the original.- A copy is one level deep — a list of references still shares the referenced data.
values %hashaliases too;keys %hashdoes not.grepandmapalias through$_.
Chapter 9.3 turns to the other loop shape: while and until, for work whose length is not known in advance — and the piece of hidden syntax Perl inserts into the commonest while loop in the language.
Where this fits
Part of the Perl curriculum.
