Tcl Part I · Module 9 · Loops and Iteration
foreach Over Several Lists at Once
A single foreach can walk several lists side by side, taking one element from each on every pass. It is the natural way to pair up names with values, or signals with widths, when they arrived as separate lists. The feature is easy and the detail that matters is what happens when the lists are not the same length — Tcl does not stop at the end of the shortest one. It keeps going to the end of the longest, supplying empty strings for whatever has run out, which is the opposite of what the equivalent feature does in most other languages and a reliable source of quietly wrong results.
Foundation10 min readTclPart ILoopsforeachParallel iteration
Part I · Module 9 · Chapter 9.2 · foreach Over Several Lists at Once
1. Two lists, one loop
Give foreach another variable-and-list pair:
set names {mode runs width}
set values {fast 3 8}
foreach name $names value $values {
puts "$name = $value"
}mode = fast
runs = 3
width = 8On each pass name takes the next element of the first list and value the next of the second. You can add as many pairs as you like:
foreach a {1 2} b {x y} c {P Q} {
puts "$a $b $c"
}1 x P
2 y QEach pair is a variable name followed by a list — bare name, $list, as always.
2. The lists do not have to be the same length
This is the part to remember:
set names {a b c}
set values {1 2}
foreach n $names v $values {
puts "n=$n v='$v'"
}n=a v='1'
n=b v='2'
n=c v=''Three passes, not two. The loop ran to the end of the longest list, and the exhausted one supplied an empty string.
It works the same way round:
foreach n {a b} v {1 2 3} {
puts "n='$n' v=$v"
}n='a' v=1
n='b' v=2
n='' v=3Now it is n that comes back empty. The rule is simply: the longest list decides how many passes there are, and every shorter list is padded.
3. Why this catches people out
Most languages with a parallel-iteration feature truncate. Python's zip stops at the shortest input; so do the equivalents in Ruby and JavaScript. Someone carrying that assumption writes:
set names {clk rst data}
set widths {1 1}
foreach n $names w $widths {
puts "port $n is $w bits"
}port clk is 1 bits
port rst is 1 bits
port data is bitsThe third line is wrong and it did not raise anything. Had Tcl truncated, data would simply have been skipped — also wrong, but visibly short. Padding produces a line that looks like output.
4. Checking first
When the lists are supposed to correspond, say so:
proc pairup {names values} {
if {[llength $names] != [llength $values]} {
error "list lengths differ: [llength $names] names, [llength $values] values"
}
set out {}
foreach n $names v $values {
lappend out "$n=$v"
}
return $out
}
puts [pairup {a b} {1 2}]
if {[catch {pairup {a b c} {1 2}} msg]} {
puts "rejected: $msg"
}a=1 b=2
rejected: list lengths differ: 3 names, 2 valuesThree lines of guard, and a mismatch now names both counts instead of producing a blank field somewhere downstream.
5. When padding is what you want
It is not always a hazard. Padding is genuinely useful when a list is optional:
set ports {clk rst data valid}
set comments {"the main clock" "active low"}
foreach p $ports c $comments {
if {$c eq ""} {
puts [format " %-6s" $p]
} else {
puts [format " %-6s # %s" $p $c]
}
} clk # the main clock
rst # active low
data
validHere the shorter list means "no comment for these", and padding expresses that directly. The deciding question is whether a short list is expected: if yes, pad and handle the empty case; if no, check the lengths and fail.
6. Parallel iteration versus a list of records
If two lists always travel together, consider whether they should have been one list of records in the first place:
# two parallel lists
set names {clk rst}
set widths {1 1}
# one list of records — they cannot drift apart
set ports {{clk 1} {rst 1}}
foreach p $ports {
lassign $p name width
puts "$name is $width bits"
}clk is 1 bits
rst is 1 bitsThe record version cannot have a length mismatch, because there is only one list. Parallel foreach is the right tool when the lists genuinely arrive separately — from two different sources, or as a function's two return values — and not as a substitute for structuring the data.
7. A worked example
Comparing two runs field by field:
set fields {cycles errors warnings}
set before {12000 0 4}
set after {11500 2 4}
if {[llength $before] != [llength $after]} {
error "runs have different numbers of fields"
}
puts [format "%-10s %8s %8s %8s" FIELD BEFORE AFTER CHANGE]
foreach f $fields b $before a $after {
set delta [expr {$a - $b}]
puts [format "%-10s %8d %8d %+8d" $f $b $a $delta]
}FIELD BEFORE AFTER CHANGE
cycles 12000 11500 -500
errors 0 2 +2
warnings 4 4 +0Three lists walked together, one line per field, and a length check at the top so a missing field cannot silently become a zero.
8. Exercises
Exercise 1. How many passes does foreach a {1 2 3} b {x y} {...} make, and what is b on the last one?
Exercise 2. Write a loop printing name: value for {mode runs} and {fast 3}.
Exercise 3. Explain the difference between Tcl's behaviour and Python's zip when the lists differ in length, and which failure is easier to notice.
Exercise 4. Add a guard to this so a mismatch is reported rather than padded:
foreach n $names w $widths { puts "$n $w" }Exercise 5. Give one situation where the padding is the behaviour you want.
Exercise 6. Two lists in a script keep drifting out of sync as the code grows. What structural change removes the problem entirely?
Answers
1. Three passes — the longer list decides. On the third, b is the empty string.
2.
foreach name {mode runs} value {fast 3} {
puts "$name: $value"
}mode: fast
runs: 33. Tcl runs to the end of the longest list and pads the short ones with empty strings; zip stops at the shortest and drops the extras. Truncation is easier to notice, because the output is visibly short — padding produces a full-length result with a blank field in it, which looks like data.
4.
if {[llength $names] != [llength $widths]} {
error "names and widths differ: [llength $names] vs [llength $widths]"
}
foreach n $names w $widths { puts "$n $w" }5. When the shorter list is genuinely optional — comments, labels or overrides that only some entries have. The empty value then means "none supplied", which is exactly right.
6. Make them one list of records, so there is only one list to keep in step: {{clk 1} {rst 1}} with lassign in the body. A length mismatch becomes impossible rather than merely checked.
9. Remember
foreach a $l1 b $l2 {body}walks several lists in parallel; add as many variable-and-list pairs as you need.- The longest list decides the number of passes.
- Shorter lists are padded with empty strings — Tcl does not truncate the way
zipdoes in other languages. - A padded empty value is indistinguishable from a real empty element, so the body cannot detect the mismatch.
- Check
llengthbefore the loop whenever the lists are supposed to correspond and come from different sources. - Padding is the right behaviour when a list is optional — treat empty as "none supplied".
- If two lists always travel together, make them one list of records and the mismatch becomes impossible.
Where this fits
Part of the Tcl curriculum.
