Tcl Part I · Module 9 · Loops and Iteration
while: Looping Until Something Changes
foreach and for both need to know what they are iterating before they start — a list, or a range. while does not. It keeps going as long as a condition holds, which makes it the right shape whenever you cannot say in advance how many passes there will be: reading until the input runs out, retrying until something succeeds, consuming a buffer until it is empty. That freedom is also the risk. A for loop carries its own advance; a while loop does not, and nothing in the language checks that your condition will ever become false. This chapter gives you a three-part habit that catches that before you run it.
Foundation11 min readTclPart ILoopswhileTermination
Part I · Module 9 · Chapter 9.5 · while: Looping Until Something Changes
1. The shape
set i 0
while {$i < 3} {
puts "pass $i"
incr i
}pass 0
pass 1
pass 2Two arguments: a test expression and a body. Tcl evaluates the test, runs the body if it is true, and repeats. Both are braced, for the reasons Chapter 8.2 gave — and here getting that wrong is worse than an error, which is Chapter 9.7.
2. When while is the right loop
Use while when the number of passes is not known in advance. The classic shape is consuming something until it runs out:
set queue {a b c d}
while {[llength $queue] > 0} {
set item [lindex $queue 0]
set queue [lrange $queue 1 end]
puts "handling $item ([llength $queue] left)"
}handling a (3 left)
handling b (2 left)
handling c (1 left)
handling d (0 left)The loop does not know there are four items. It knows to stop when the queue is empty, which is a different and often more honest statement of the job.
3. Retrying until something succeeds
proc attempt {n} {
# pretend the third try is the one that works
return [expr {$n >= 3}]
}
set tries 0
set ok 0
while {!$ok && $tries < 5} {
incr tries
set ok [attempt $tries]
puts "attempt $tries -> [expr {$ok ? "ok" : "failed"}]"
}
if {$ok} {
puts "succeeded after $tries attempts"
} else {
puts "gave up after $tries attempts"
}attempt 1 -> failed
attempt 2 -> failed
attempt 3 -> ok
succeeded after 3 attemptsNote the second half of the condition. !$ok alone would loop forever if the operation never succeeded; && $tries < 5 makes failure terminate too. A retry loop needs both.
4. while {1} with a break is legitimate
Sometimes the exit condition is only computable in the middle of the body:
set n 0
while {1} {
incr n
if {$n * $n > 50} break
}
puts "first n with n squared over 50: $n"first n with n squared over 50: 8This is not a code smell when the test genuinely belongs mid-body. It is clearer than duplicating the computation once before the loop and once at the bottom. Chapter 9.6 covers break properly.
The thing to avoid is while {1} with the break buried in a nested if several levels down, where a reader cannot see what ends the loop.
5. The three-part termination check
Before you run any while, name three things:
- The variable in the condition exists before the loop starts.
- The body can change it.
- Something guarantees it eventually does.
Applied to section 1:
set i 0 ;# 1. exists
while {$i < 3} {
puts "pass $i"
incr i ;# 2. the body changes it
} ;# 3. incr always moves toward 3The first is easy to check and easy to get wrong:
unset -nocomplain j
while {$j < 3} { incr j }can't read "j": no such variableThat one fails loudly. The third is the one that bites, because nothing reports it. Here is the most common way it goes wrong, carried over from Chapter 9.4 §6:
set i 0
set seen {}
while {$i < 5} {
if {$i == 2} continue ;# never reaches the incr below
lappend seen $i
incr i
}That loop hangs. continue jumps back to the test, the incr at the bottom never runs, and i stays at 2.
6. A safety counter while developing
When the termination argument is not obvious, make the loop prove it:
set guard 0
set x 1
while {$x < 1000} {
set x [expr {$x * 3}]
if {[incr guard] > 100} {
error "loop did not terminate after $guard passes"
}
}
puts "x = $x after $guard passes"x = 2187 after 7 passesIf the loop is right, the guard costs nothing. If it is wrong, you get an error with a number instead of a hung terminal. Remove it once the loop is settled, or leave it in if the condition depends on data you do not control.
7. while returns nothing
set i 0
set r [while {$i < 2} {incr i}]
puts "'[set r]'"''Same as foreach and for.
8. A worked example
Consuming a list of tokens with a condition that depends on what it finds:
set tokens {open a b close open c close end x y}
set depth 0
set groups 0
set i 0
while {$i < [llength $tokens]} {
set t [lindex $tokens $i]
incr i
if {$t eq "end"} break
switch -- $t {
open { incr depth }
close { incr depth -1 ; if {$depth == 0} { incr groups } }
}
}
puts "groups closed : $groups"
puts "stopped at : index $i of [llength $tokens]"
puts "depth : $depth"groups closed : 2
stopped at : index 8 of 10
depth : 0The loop advances before anything can break or continue, the bound is checked on every pass, and the end token stops it early — all three parts of the termination check are visible in eight lines.
9. Exercises
Exercise 1. What are the two arguments to while, and why are both braced?
Exercise 2. Write a loop halving $n (integer division) until it reaches 0, printing each value.
Exercise 3. Name the three things that must be true for a while to terminate.
Exercise 4. Why does this hang, and give two different fixes?
set i 0
while {$i < 5} {
if {$i == 2} continue
incr i
}Exercise 5. Rewrite as a foreach and say why while was the wrong choice:
set i 0
while {$i < [llength $items]} { puts [lindex $items $i] ; incr i }Exercise 6. A retry loop is written while {!$ok} { set ok [try_once] }. What is missing and what can happen?
Answers
1. A test expression and a body script. Both are braced so they are evaluated afresh on each pass rather than substituted once when the line is parsed — for the test, failing to brace it freezes the condition and the loop never ends.
2.
set n 20
while {$n > 0} {
puts $n
set n [expr {$n / 2}]
}20
10
5
2
13. The condition's variable exists before the loop; the body can change it; and something guarantees it eventually does.
4. When i is 2, continue returns to the test without reaching incr i, so i never moves again. Fix one: advance before the continue can be reached. Fix two: use for, whose next script runs even on continue.
5.
foreach item $items { puts $item }while was wrong because the number of passes was known in advance — it is the length of the list. while is for when it is not.
6. The loop has no bound on the number of attempts, so if try_once never succeeds it runs forever. It needs a second clause — while {!$ok && $tries < $max} — and a counter, so that permanent failure terminates and can be reported.
10. Remember
while {test} {body}— two arguments, both braced, test re-evaluated every pass.- Use it when the number of passes is not known in advance: consuming until empty, reading until end of input, retrying until success.
while {[gets $fp line] >= 0}is the shape you will meet most; file handles come in Module 12.- A retry loop needs a bound as well as a success test, or permanent failure never terminates.
while {1}with a visiblebreakis fine when the exit test belongs mid-body.- Check three things before running it: the variable exists, the body can change it, and something guarantees it does.
continuein awhileskips an increment at the bottom of the body and hangs the loop — advance first, or usefor.- Add a guard counter while developing a loop whose termination is not obvious.
whilereturns the empty string.
Where this fits
Part of the Tcl curriculum.
