Skip to content
VLSI Mentor

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

Azvya Education Pvt. Ltd.VLSI Mentor
alias-trim.pl
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);
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
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

Azvya Education Pvt. Ltd.VLSI Mentor
Normalising a list in place
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 newlines
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output — all four applied in that order
names  : [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:

Azvya Education Pvt. Ltd.VLSI Mentor
The same thing the long way
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.

Azvya Education Pvt. Ltd.VLSI Mentor
alias-literal.pl
use strict;
use warnings;

for my $b ("alu", "fifo") {
    $b = uc $b;
}
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output — STDERR
Modification of a read-only value attempted at alias-literal.pl line 5.
Azvya Education Pvt. Ltd.VLSI Mentor
Measured exit status
255

Reading the same literal is perfectly fine:

Azvya Education Pvt. Ltd.VLSI Mentor
alias-read.pl
use strict;
use warnings;

for my $b ("alu", "fifo") {
    my $u = uc $b;
    print "$u ";
}
print "\n";
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
ALU FIFO
Azvya Education Pvt. Ltd.VLSI Mentor
Measured exit status
0

The 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.

Azvya Education Pvt. Ltd.VLSI Mentor
alias-copy.pl
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);
Azvya Education Pvt. Ltd.VLSI Mentor
Captured output
iterating a copy  : original [alu | fifo]
copy inside body  : original [alu | fifo]
FormEffect
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:

Azvya Education Pvt. Ltd.VLSI Mentor
Captured output — @copy = @rows, then $copy[0]{blk} =
original row 0 blk = CHANGED

Chapter 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 foreach variable 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 %hash aliases too; keys %hash does not. grep and map alias 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.