Tcl Part I · Module 4 · Operators and Expressions
Logical Operators and Short-Circuit Evaluation
Three operators combine conditions, and they come with a behaviour that is easy to describe and easy to underestimate. When the left-hand side of an and already settles the answer, the right-hand side is never evaluated at all. That is usually introduced as a performance detail. It is not — it is what allows one test to protect another, so that a check which would fail on missing data simply never runs. This chapter proves the behaviour with a side effect you can see, then builds the guard pattern that depends on it.
Foundation12 min readTclPart IOperatorsLogicGuards
Part I · Module 4 · Chapter 4.5 · Logical Operators and Short-Circuit Evaluation
1. The Three Operators
puts "1 && 1 = [expr {1 && 1}]"
puts "1 && 0 = [expr {1 && 0}]"
puts "0 || 1 = [expr {0 || 1}]"
puts "0 || 0 = [expr {0 || 0}]"
puts "!0 = [expr {!0}]"
puts "!5 = [expr {!5}]"1 && 1 = 1
1 && 0 = 0
0 || 1 = 1
0 || 0 = 0
!0 = 1
!5 = 0&& is and, || is or, ! is not. Their operands are read as truth values, which from Chapter 3.6 means anything on the accepted list — and anything else is an error, not false.
2. Combining Comparisons
The normal use:
set count 5
set limit 10
set mode "fast"
puts "in range: [expr {$count > 0 && $count < $limit}]"
puts "either: [expr {$count > 100 || $mode eq "fast"}]"
puts "not fast: [expr {!($mode eq "fast")}]"in range: 1
either: 1
not fast: 0Comparisons bind tighter than && and || — Chapter 4.2 — so $count > 0 && $count < $limit groups the way it reads.
The third line is better written as $mode ne "fast". Negating a comparison with ! is usually a sign there is a direct operator for what you meant.
3. The Rule That Matters
&&stops as soon as the left side is false.||stops as soon as the left side is true.
If the answer is already settled, the right-hand side is never evaluated.
|| is the mirror image: a true left operand settles it, and the right side is skipped.
4. Proving It
You cannot show this with return values alone, because 0 && x is 0 whether or not x ran. You need a right-hand side that leaves a trace:
proc noisy {} {
puts " (the right side ran)"
return 1
}
puts "0 && noisy -> [expr {0 && [noisy]}]"
puts "1 && noisy -> [expr {1 && [noisy]}]"
puts "1 || noisy -> [expr {1 || [noisy]}]"
puts "0 || noisy -> [expr {0 || [noisy]}]"0 && noisy -> 0
1 && noisy -> (the right side ran)
1
1 || noisy -> 1
0 || noisy -> (the right side ran)
1Two of the four printed the message and two did not.
0 && …— false already, right side skipped.1 && …— not settled, right side ran.1 || …— true already, right side skipped.0 || …— not settled, right side ran.
The message appears before the result because the inner command runs first — Chapter 2.5.
5. The Payoff: One Test Protecting Another
This is why the rule matters, and it is a correctness matter rather than a speed one.
set f "nope.txt"
puts "guarded: [expr {[file exists $f] && [file size $f] > 0}]"guarded: 0file size on a file that does not exist raises an error. It was never called, because file exists returned false and settled the answer.
Reverse the order and the protection is gone:
set f "nope.txt"
puts [catch {expr {[file size $f] > 0 && [file exists $f]}} m]
puts $m1
could not read "nope.txt": no such file or directorySame two tests, same operator, opposite order — and now it fails.
6. What They Return
A detail that catches people arriving from Python:
puts "5 && 3 = [expr {5 && 3}]"
puts "0 || 7 = [expr {0 || 7}]"5 && 3 = 1
0 || 7 = 1Tcl returns 1 or 0, never the operand. In Python, 5 and 3 is 3 and 0 or 7 is 7, which is why the a or default idiom works there.
That idiom does not exist in Tcl. For a default value, use Chapter 3.4's info exists pattern or the conditional operator in Chapter 4.7.
7. Chaining and Grouping
&& binds tighter than ||:
puts "1 || 0 && 0 = [expr {1 || 0 && 0}]"
puts "(1 || 0) && 0 = [expr {(1 || 0) && 0}]"1 || 0 && 0 = 1
(1 || 0) && 0 = 0Different answers. The first is 1 || (0 && 0).
Per Chapter 4.2 §6: when && and || appear in the same expression, parenthesise. Nobody should have to recall the order to review your condition.
8. Common Mistakes
Putting the expensive or risky test first. §5. Order matters for correctness.
Expecting && to return the operand. §6.
Mixing && and || without parentheses. §7.
Using ! where a direct operator exists. !($a eq $b) is $a ne $b.
Assuming a non-boolean right side is safe because it is skipped. It is safe only when it is actually skipped — which depends on the left side at run time, not on what you expect.
Relying on a side effect in the right operand. If it must happen, do not put it in a condition.
9. Exercises
Exercise 1 — the truth tables. Produce all four combinations for && and for ||, and both cases for !.
Exercise 2 — see the skip. Write the §4 noisy proof yourself. Then add a fifth line, expr {0 && [noisy] && [noisy]}, and predict how many messages appear before running it.
Exercise 3 — build a guard. Write one condition that is true only when a variable limit exists and holds a number greater than zero. Test it with limit unset, set to abc, set to 0, and set to 5. It must not error in any case.
Exercise 4 — break it by reordering. Take your Exercise 3 answer and move the existence test to the end. Which inputs now fail, and why?
Exercise 5 — the division guard. Write a condition that safely tests whether $total / $count exceeds 10 when count might be zero. Confirm it does not error for count equal to 0.
Exercise 6 — the missing idiom. A colleague writes set name [expr {$input || "default"}] expecting Python behaviour. Explain what Tcl actually produces, then write the version that works.
10. Remember
&&,||and!combine truth values, and their operands must be valid truth values.&&stops when the left side is false;||stops when the left side is true. The right side is then never evaluated.- That is a correctness feature. It lets one test protect another.
- Order conditions cheapest and most protective first —
[info exists x] && $x > 0, not the reverse. - They return
1or0, never the operand. The Pythona or defaultidiom does not exist here. &&binds tighter than||. Parenthesise when both appear.- Prefer a direct operator to negation:
neover!(... eq ...).
Next: the operators that work on the bits inside a number.
Where this fits
Part of the Tcl curriculum.
