Tcl Part I · Module 9 · Loops and Iteration
for: Index-Driven Loops
Chapter 9.1 argued that foreach should be your default and that reaching for an index by reflex costs you four extra things to get wrong. This chapter is about the cases where the index genuinely is the job — numbering output, stepping by something other than one, walking a range that has no list behind it, or comparing an element with its neighbour. Tcl's for handles all of those, and it is worth seeing clearly that it is not special syntax: it is a command taking four arguments, three of which are scripts and one of which is an expression, which explains both what you can put in them and why they are braced.
Foundation10 min readTclPart ILoopsforIndexes
Part I · Module 9 · Chapter 9.4 · for: Index-Driven Loops
1. The four arguments
for {set i 0} {$i < 4} {incr i} {
puts "bit $i"
}bit 0
bit 1
bit 2
bit 3In order: a start script, a test expression, a next script, and a body script. Tcl runs the start once, then repeatedly evaluates the test, runs the body, and runs the next script.
None of that is syntax. for is a command, like if in Chapter 8.1, taking four ordinary arguments — which is why there are no semicolons or parentheses, and why all four are braced.
2. The start and next are scripts
Because they are scripts rather than single statements, they can contain several commands separated by semicolons (Chapter 2.10):
for {set i 0 ; set j 10} {$i < $j} {incr i 3 ; incr j -3} {
puts "i=$i j=$j"
}i=0 j=10
i=3 j=7Two counters converging. You will not need this often, and it is worth knowing that nothing special is happening — they are just scripts.
The start script can also be empty when the variable is already set:
set i 0
for {} {$i < 3} {incr i} {
puts $i
}0
1
23. The test is an expression, and it is braced
Everything Chapter 8.2 said about if applies here. The test is handed to expr, and bracing it means the variable is read fresh on every pass rather than substituted once while the line is parsed.
Getting that wrong does not produce an error — it produces a loop that never ends. That is important enough to have its own chapter: Chapter 9.7.
4. When for earns its place
Stepping by something other than one:
for {set i 0} {$i < 32} {incr i 8} {
puts "byte boundary at $i"
}byte boundary at 0
byte boundary at 8
byte boundary at 16
byte boundary at 24Counting down:
for {set i 3} {$i >= 0} {incr i -1} {
puts "bit $i"
}bit 3
bit 2
bit 1
bit 0A range with no list behind it — there is nothing to foreach over:
set total 0
for {set i 1} {$i <= 100} {incr i} {
incr total $i
}
puts "sum 1..100 = $total"sum 1..100 = 5050Comparing an element with its neighbour, which needs two positions at once:
set readings {3 5 4 9 9 2}
for {set i 1} {$i < [llength $readings]} {incr i} {
set prev [lindex $readings [expr {$i - 1}]]
set cur [lindex $readings $i]
if {$cur > $prev} {
puts "rose at index $i: $prev -> $cur"
}
}rose at index 1: 3 -> 5
rose at index 3: 4 -> 95. for returns nothing
set r [for {set i 0} {$i < 2} {incr i} {expr {$i}}]
puts "'[set r]'"''Same as foreach. Loops are run for their effect; collecting results is Chapter 9.9.
6. continue still runs the next script
This is the detail worth taking away. Inside a for, continue skips the rest of the body — but the next script still runs:
set seen {}
for {set i 0} {$i < 5} {incr i} {
if {$i == 2} continue
lappend seen $i
}
puts "seen = $seen"
puts "i ended at $i"seen = 0 1 3 4
i ended at 52 was skipped and the loop still advanced and still finished. Now the same logic written as a hand-rolled while:
set i 0
set seen {}
while {$i < 5} {
if {$i == 2} continue ;# <- skips the incr below: never ends
lappend seen $i
incr i
}That loop hangs. continue jumps back to the test, the increment at the bottom of the body never runs, and i stays at 2 forever.
7. A worked example
Formatting a list into fixed-width columns, where the index really is the point:
set items {clk rst din dout en valid ready ack}
set percol 3
for {set i 0} {$i < [llength $items]} {incr i $percol} {
set row {}
for {set j 0} {$j < $percol} {incr j} {
set idx [expr {$i + $j}]
if {$idx < [llength $items]} {
lappend row [format "%-8s" [lindex $items $idx]]
}
}
puts " [join $row]"
} clk rst din
dout en valid
ready ackThe outer loop steps by three, the inner one fills a row, and the if stops the last row running off the end. Neither loop could be a foreach, because both use the index arithmetically.
8. Exercises
Exercise 1. Name the four arguments of for and say which one is an expression rather than a script.
Exercise 2. Write a loop printing the even numbers from 0 to 10 inclusive.
Exercise 3. Why does this run forever, and what is the minimal fix?
set i 0
while {$i < 5} {
if {$i == 2} continue
incr i
}Exercise 4. Rewrite as a foreach and say why that is better:
for {set i 0} {$i < [llength $l]} {incr i} { puts [lindex $l $i] }Exercise 5. Write a loop over {4 7 2 9} printing each value and whether it is larger than the one before it.
Exercise 6. Give two cases where for is the right choice and foreach cannot do the job.
Answers
1. Start script, test expression, next script, body script. The test is the expression; the other three are scripts.
2.
for {set i 0} {$i <= 10} {incr i 2} {
puts $i
}0
2
4
6
8
103. When i reaches 2, continue jumps straight back to the test without reaching incr i, so i stays at 2 and the condition stays true forever. The minimal fix is to advance before the continue can be reached — or to use for, which keeps the increment in its own argument:
for {set i 0} {$i < 5} {incr i} {
if {$i == 2} continue
}4.
foreach item $l { puts $item }The index was used only to fetch the element, so it was never needed. Removing it removes the bound, the increment, and the out-of-range lindex that would silently yield an empty string.
5.
set readings {4 7 2 9}
for {set i 0} {$i < [llength $readings]} {incr i} {
set cur [lindex $readings $i]
if {$i == 0} {
puts "$cur (first)"
} else {
set prev [lindex $readings [expr {$i - 1}]]
puts "$cur ([expr {$cur > $prev ? "up" : "down"}])"
}
}4 (first)
7 (up)
2 (down)
9 (up)6. Any two of: stepping by something other than one; counting down; iterating a numeric range with no list behind it; needing two positions at once to compare neighbours; using the index itself in the output.
9. Remember
for {start} {test} {next} {body}— four arguments, three scripts and one expression. It is a command, not syntax.- Start and next are scripts, so they can hold several commands separated by
;, and start may be empty. - The test is braced for the same reasons an
ifcondition is — and getting it wrong here causes an infinite loop rather than an error. - Use
forwhen the index is the job: a step other than one, counting down, a numeric range, or comparing neighbours. - If the body only uses the index for
lindex, you wantedforeach. forreturns the empty string.continueinside aforstill runs the next script, so the loop still advances.- A hand-rolled
whilethat increments at the bottom hangs oncontinue— that is the strongest reason to preferfor.
Where this fits
Part of the Tcl curriculum.
