What must hold for a nested if-inside-if access guard to flatten into one AND-joined condition without changing behaviour?
answer
- control flow, not just truth values
- short-circuit preserves the evaluated set
- the else branches are the problem
- commutative in algebra, ordered in code
- invert to guard clauses to keep reasons
basics
~20 sThree things: neither inner nor outer test may have a distinct else branch, no code may run between the two tests, and the conjuncts must keep their original left-to-right order so a test that guards a later one still runs first.
solid answer
~50 sFlattening claims that `if (a) { if (b) { X } }` is the same as `if (a AND b) { X }`. With a short-circuiting AND that claim holds exactly, because the flattened form evaluates `b` only when `a` is true — the same operands in the same order. It stops holding as soon as the nesting carries anything else. An **else on the inner if** does work that the flat form has no place for: the single else now covers two distinct failures and cannot tell them apart. **Statements between the two tests** are dropped or moved. And although AND is commutative as a truth function, the program's AND is control flow, so **reordering the conjuncts** is unsafe whenever the left one establishes that the right one is well-defined. Check those three, then flatten.
code
pseudocode · 7 linesif isAuthenticated then
if hasRole then
allow()
else
deny("role missing")
else
deny("not authenticated")go deeper
Know the basic equivalence: an if inside an if runs its body only when both tests pass, so joining them with AND keeps the same body condition. Look for an else before you rewrite anything.
Explain why short-circuit evaluation makes the flat form evaluate exactly the same operands in the same order, and why an else on the inner test is the thing that blocks the rewrite.
Show the judgment: merged else branches destroy per-reason denials that operators depend on, so propose inverted guard clauses instead, and freeze the conjunct order wherever one test makes the next well-defined.
The tradeoff is between one expression that states policy and a chain that states reasons. Pick deliberately: the first is easier to reason about as a whole, the second is what makes rejections diagnosable in production.
## What flattening claims The reviewed filter is four `if` statements deep, and the proposed cleanup is to join the tests with AND. The claim being made is an equivalence between a **control-flow shape** and a **boolean expression**: `if (a) { if (b) { X } }` is the same as `if (a AND b) { X }` As a statement about truth values this is trivially true: `X` runs exactly when both tests pass. The interesting part is that it is also true as a statement about *evaluation*, provided the AND short-circuits. Trace it: in the nested form, `a` is evaluated; `b` is evaluated only if `a` was true. In the flat form, `a` is evaluated; the AND short-circuits if `a` was false, so `b` is evaluated only if `a` was true. Same operands, same order, same number of evaluations. That is why the rewrite is safe even when the tests are expensive or when the second one would fault if the first had failed. ## The three conditions 1. **No distinct else.** An else on the inner `if` is work that fires when `a` succeeded and `b` failed. The flat form has one else, reached when the conjunction is false, which covers both failures at once and cannot distinguish them. This is the condition that fails most often in a real filter, because the else branches are the rejection reasons. 2. **Nothing between the tests.** Any statement sitting between the outer `if` and the inner one runs whenever `a` passes, regardless of `b`. The flat form has nowhere to put it — hoisting it above the guard changes when it runs, and pushing it inside changes whether it runs. 3. **Order preserved.** The conjuncts must stay in their original left-to-right order unless each is total and free of side effects. Boolean AND is commutative as a truth function; the operator in the code is a control-flow construct that decides whether to evaluate its right operand at all. Those are different objects, and only the first is commutative. ## Why the else branch is the usual defect An access filter rarely just fails to allow — it denies **with a reason**, and the reasons live in exactly the branches that flattening merges. Merging them costs the operator the distinction between "this caller never authenticated" and "this caller authenticated but lacks the role", which are the two rows of the support ticket. The flattened form is only honest when both failures genuinely lead to the same place, which in an access filter usually means a single generic denial by design. When the reasons must be kept, the right rewrite is not a conjunction but the **negation normal form of the reject condition**, expressed as a chain of early-return guard clauses: - test `NOT a` first, deny with a's reason; - then test `NOT b`, deny with b's reason; - then fall through to allow. That chain is a disjunction of independent reject reasons, evaluated in order. It flattens the nesting without merging anything, and each disjunct keeps its own branch. ## Commutativity is about truth values, not about code | statement | true of the truth function? | true of the short-circuit operator? | |---|---|---| | `a AND b` equals `b AND a` | yes | only if both operands are total and pure | | both operands are always evaluated | not applicable | no, the right one may be skipped | | `a AND (b AND c)` regroups freely | yes | yes, order of evaluation is unchanged | | a false left operand fixes the result | yes | yes, and it ends the evaluation | The second column is what the algebra licenses; the third is what the program will do. A test that establishes the well-definedness of the next one — a presence check before a lookup, a bounds check before an index — must stay to the left. Swapping them preserves the truth value on every input where both operands are defined, and faults on the inputs where they are not, which is precisely the set the guard exists to handle. ## A review checklist - Does either `if` have an else, and do those elses do different work? If yes, do not flatten — invert into guard clauses instead. - Is there any statement between the two tests? If yes, decide where it belongs before touching the condition. - Does the left test make the right one well-defined? If yes, freeze the order and say so in a comment or a name. - After flattening, is the resulting condition still a duplicated or negated mess? Then the flattening was the first step, not the fix, and absorption or a De Morgan rewrite finishes it.
- If flattening is unsafe because each failure needs its own reason, what rewrite do you propose instead?Invert the guard and return early. Push the negation inward to get the reject condition as a disjunction of independent reasons, then test each in turn and deny with its own message. The nesting disappears, the reasons survive, and the evaluation order of the original is preserved.
- Does the answer change if the language's AND operator evaluates both operands?Yes, and it is a real difference between ecosystems: some provide only short-circuiting boolean operators, others also offer eager ones. With an eager AND the flat form evaluates the right test even when the left failed, so a presence check no longer protects the lookup behind it and any side effect in the right operand now always fires.
- The flattened condition comes out as a five-clause expression nobody can read. Is that a failure?Only if you stop there. Flattening is a normalisation step, not the end state: once the tests sit at one level you can apply absorption to drop duplicated clauses and De Morgan to push negations onto atoms. If the result is still unreadable, the honest move is to name subconditions rather than to renest them.
saying these in an interview costs you the question
- Nested ifs and one AND-joined condition always mean the same thing
- AND is commutative, so a presence check can move after the lookup
- A short-circuiting AND evaluates both operands anyway
- An inner else can be merged into the outer else unchanged
- Flattening makes the second test run even when the first failed
- Any nesting depth can be flattened as long as the tests are boolean