Skip to content
VLSI Mentor

Tcl Part I · Module 8 · Conditions and Control Flow

Nested Conditions, and When to Flatten Them

Conditions nest, and after three levels a reader has to hold three facts in their head at once to understand the line they are looking at. Most of that nesting is accidental — it is two independent tests that happened to get written one inside the other — and it collapses into something shorter that says the same thing. Some of it is not accidental, and flattening it either changes the meaning or breaks outright. This chapter gives you two transformations, a way to check that each one preserved behaviour rather than trusting that it did, and an honest account of when to leave the nesting alone.

Foundation11 min readTclPart IControl flowReadabilityGuard clauses

Part I · Module 8 · Chapter 8.3 · Nested Conditions, and When to Flatten Them

1. Nesting works, and it accumulates

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc describe {ok count} {
    if {$ok} {
        if {$count > 0} {
            return "ready with $count"
        } else {
            return "ready but empty"
        }
    } else {
        return "not ready"
    }
}

foreach pair {{1 5} {1 0} {0 5}} {
    lassign $pair ok count
    puts [format "ok=%s count=%s -> %s" $ok $count [describe $ok $count]]
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ok=1 count=5 -> ready with 5
ok=1 count=0 -> ready but empty
ok=0 count=5 -> not ready

That is correct and perfectly readable at two levels. The trouble starts at three or four, when the body of the innermost branch is far enough from its condition that you have to scroll to see what is true there.

2. Transformation one: combine with &&

Two nested conditions with nothing between them collapse into one:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# before
proc old {ok count} {
    if {$ok} {
        if {$count > 0} {
            return "go"
        }
    }
    return "stop"
}

# after
proc new {ok count} {
    if {$ok && $count > 0} {
        return "go"
    }
    return "stop"
}

The important part is not that it looks nicer — it is checking that it behaves identically. Run both over every input that matters:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set same 1
foreach ok {0 1} {
    foreach count {-1 0 5} {
        set a [old $ok $count]
        set b [new $ok $count]
        puts [format "ok=%s count=%2s  old=%-4s new=%-4s %s" \
            $ok $count $a $b [expr {$a eq $b ? "" : "<-- DIFFERS"}]]
        if {$a ne $b} { set same 0 }
    }
}
puts "identical on every input: $same"
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
ok=0 count=-1  old=stop new=stop 
ok=0 count= 0  old=stop new=stop 
ok=0 count= 5  old=stop new=stop 
ok=1 count=-1  old=stop new=stop 
ok=1 count= 0  old=stop new=stop 
ok=1 count= 5  old=go   new=go   
identical on every input: 1

3. Transformation two: the guard clause

The deeper nesting usually comes from handling the failure cases inside the success path. Turning them into early exits flattens everything:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# before — the real work is three levels in
proc run_old {job} {
    if {[dict exists $job name]} {
        if {[dict exists $job target]} {
            if {[dict get $job target] ne ""} {
                return "running [dict get $job name] on [dict get $job target]"
            } else {
                return "error: empty target"
            }
        } else {
            return "error: no target"
        }
    } else {
        return "error: no name"
    }
}

# after — failures first, work last, never indented
proc run_new {job} {
    if {![dict exists $job name]}   { return "error: no name" }
    if {![dict exists $job target]} { return "error: no target" }
    if {[dict get $job target] eq ""} { return "error: empty target" }
    return "running [dict get $job name] on [dict get $job target]"
}

Check them against each other:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
set jobs [list \
    [dict create name smoke target top] \
    [dict create name smoke target ""] \
    [dict create name smoke] \
    [dict create target top]]

foreach j $jobs {
    set a [run_old $j] ; set b [run_new $j]
    puts [format "%-28s %s" "'$j'" [expr {$a eq $b ? "same: $a" : "DIFFERS"}]]
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
'name smoke target top'      same: running smoke on top
'name smoke target {}'       same: error: empty target
'name smoke'                 same: error: no target
'target top'                 same: error: no name

The second version has the real work on its last line, at zero indentation, and each failure sits next to the test that detected it. That is the shape to aim for.

4. When nesting is right

Nesting is correct when the inner question only makes sense if the outer one is true:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc report {cfg} {
    if {[dict exists $cfg stages]} {
        set stages [dict get $cfg stages]
        if {[dict exists $stages parse]} {
            return "parse timeout [dict get $stages parse timeout]"
        }
        return "stages present, no parse stage"
    }
    return "no stages"
}

puts [report [dict create stages [dict create parse [dict create timeout 60]]]]
puts [report [dict create stages [dict create build [dict create timeout 300]]]]
puts [report [dict create other 1]]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
parse timeout 60
stages present, no parse stage
no stages

Here the inner dict exists $stages parse cannot be asked until stages has been fetched, which cannot happen until the outer test passed. The nesting reflects a real dependency. Flattening it would mean fetching stages before knowing it exists.

5. The honest limit on &&

&& can replace nesting because it short-circuits — the rule from Chapter 4.5. That is doing real work, and it means the order of the two tests is part of the meaning.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc safe {d} {
    if {[dict exists $d n] && [dict get $d n] > 5} {
        return "big"
    }
    return "small"
}
puts [safe [dict create n 9]]
puts [safe [dict create]]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
big
small

The second call did not error, because dict exists was false and && never evaluated the right-hand side. Now swap the two tests:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc unsafe {d} {
    if {[dict get $d n] > 5 && [dict exists $d n]} {
        return "big"
    }
    return "small"
}
puts [unsafe [dict create]]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
key "n" not known in dictionary

Same two tests, same &&, and it fails — because the guard is no longer first. The same thing happens with arithmetic:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc odd_or_even {v} {
    if {$v % 2 == 0 && [string is integer -strict $v]} { return "even" }
    return "no"
}
puts [odd_or_even abc]
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
can't use non-numeric string as operand of "%"

6. A third option: dispatch

Sometimes the nesting is really a multi-way choice wearing a disguise:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
# before
proc classify_old {mode} {
    if {$mode eq "fast"} {
        return 1
    } else {
        if {$mode eq "full"} {
            return 10
        } else {
            if {$mode eq "deep"} {
                return 100
            } else {
                return 0
            }
        }
    }
}

# after
proc classify_new {mode} {
    if {$mode eq "fast"} {
        return 1
    } elseif {$mode eq "full"} {
        return 10
    } elseif {$mode eq "deep"} {
        return 100
    }
    return 0
}

foreach m {fast full deep other} {
    puts "$m: [classify_old $m] / [classify_new $m]"
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
fast: 1 / 1
full: 10 / 10
deep: 100 / 100
other: 0 / 0

elseif exists exactly so that an else containing nothing but another if does not have to be written as nesting. And when every branch compares the same variable against a constant, as here, there is a better form still — that is Chapter 8.4.

7. A practical limit

A rule of thumb that holds up: if you are writing a third level of nesting, stop and look for a guard clause. Almost always one of the outer conditions is a precondition that can be turned into an early return, and almost always the result is shorter and reads top to bottom.

If after that you still need three levels, you probably have genuinely dependent logic, as in section 4 — and three levels is then the honest representation of it.

8. Exercises

Exercise 1. Flatten this into a single if, and say what makes the flattening safe:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if {$enabled} {
    if {$count > 0} {
        puts go
    }
}

Exercise 2. Rewrite with guard clauses and verify both versions agree on all four inputs:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc f {a b} {
    if {$a ne ""} {
        if {$b ne ""} {
            return "$a-$b"
        } else {
            return "no b"
        }
    } else {
        return "no a"
    }
}

Exercise 3. Why does if {[dict exists $d n] && [dict get $d n] > 5} survive an empty dictionary while the reversed version does not?

Exercise 4. Give an example where nesting should be kept rather than flattened, and say what makes it so.

Exercise 5. Rewrite this chain as elseif and say how many levels of indentation you removed:

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if {$x == 1} { set r a } else { if {$x == 2} { set r b } else { set r c } }

Exercise 6. Someone flattens if {$ok} { if {$n > 0} {...} } into if {$ok || $n > 0}. What did they get wrong, and on which inputs does it show?

Answers

1.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if {$enabled && $count > 0} {
    puts go
}

It is safe because the outer if had no else and nothing between the two conditions, so the only way to reach puts go was for both to be true — which is exactly what && means.

2.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
proc g {a b} {
    if {$a eq ""} { return "no a" }
    if {$b eq ""} { return "no b" }
    return "$a-$b"
}

foreach pair {{x y} {x ""} {"" y} {"" ""}} {
    lassign $pair a b
    puts [format "a='%s' b='%s'  %s" $a $b [expr {[f $a $b] eq [g $a $b] ? "same: [g $a $b]" : "DIFFERS"}]]
}
Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
a='x' b='y'  same: x-y
a='x' b=''  same: no b
a='' b='y'  same: no a
a='' b=''  same: no a

3. Because && short-circuits. With the guard first, dict exists $d n is false and the right-hand side is never evaluated. Reversed, dict get $d n runs first on a dictionary that has no such key and raises key "n" not known in dictionary.

4. Any case where the inner test depends on work done by the outer one — for example fetching [dict get $cfg stages] only after [dict exists $cfg stages] has confirmed it is there. Flattening would perform the fetch before establishing it is valid.

5.

Azvya Education Pvt. Ltd.VLSI Mentor
Snippet
if {$x == 1} {
    set r a
} elseif {$x == 2} {
    set r b
} else {
    set r c
}

One level. With a longer chain it removes one level per additional case, which is why deeply nested else { if ... } grows so quickly.

6. They used || where the nesting meant "and". Nesting one if inside another requires both conditions; || requires only one. It shows on every input where exactly one is true — ok=1, n=0 and ok=0, n=5 both now run the body, and neither did before.

9. Remember

  • Two nested conditions with nothing between them combine with &&.
  • Check every restructuring by running both versions over the same inputs and comparing. It is faster than reasoning and it catches the exceptions.
  • Guard clauses flatten the common case: test the failures first and return early, so the real work ends up on the last line at zero indentation.
  • Keep the nesting when the inner test depends on the outer one having already succeeded — for instance fetching a value only after confirming it exists.
  • && replaces nesting because it short-circuits, so the guard must stay first. Reordering can turn a safe test into an error.
  • else { if ... } should be elseif — that is what it is for.
  • At a third level of nesting, look for a guard clause before writing it.
  • Nesting that compares one variable against several constants wants switch — the next chapter.

Where this fits

Part of the Tcl curriculum.