Why does a comment walker that recurses only into each comment's own replies need no depth counter?
answer
- look at what each call is handed
- a part, never the whole
- strictly fewer pieces every time
- finite value, finitely many parts
- a budget hides a non-shrinking argument
basics
~20 sEvery recursive call receives a part of the value it was given, so the argument is strictly smaller each time and the descent runs out of thread. Termination comes from the data, not from a number someone picked.
solid answer
~40 sEach call is handed something the current value already contains - a comment's reply list, or the rest of the list - never the value itself and never something rebuilt from it. Because a part is strictly contained in the whole, the argument shrinks on every call, and a finite thread has only finitely many parts, so the chain of calls has to reach the empty case. A depth counter would be an independent claim: it has to be tuned, it truncates threads that are legitimately deeper than the number chosen, and it does nothing if the argument was not shrinking in the first place. The structural version puts the termination argument in the data where a reviewer can check it by looking at the call sites.
code
pseudocode · 7 linesfunction depthOf(replies)
if replies is empty
return 0
first = head of replies
rest = tail of replies
// both arguments below are parts of `replies`, not values rebuilt from it
return max(1 + depthOf(first.replies), depthOf(rest))go deeper
Know that the walker stops because each call gets a piece of what the last call had, and a thread has finitely many pieces. That is the answer; the counter is not needed.
Explain the three conditions - finite value, every recursive argument a proper part, a case for every way of building - and say what each one contributes. Be able to check a call site against them.
Show what a depth budget actually costs: it truncates legitimately deep data and turns a non-shrinking argument into a wrong answer rather than a hang. Put the guard at the input boundary instead.
The trade-off is where a system establishes that its data really is the finite shape its types claim. Validating once at the edge buys the guarantee for every walker; guarding everywhere buys tunable numbers nobody owns.
## Termination that you read off the call sites A walker over a nested comment thread recurses into two places: the reply list a comment carries, and the rest of the list the comment sits in. Both are **parts of the value the call was given**. Neither is the value itself, and neither is a new value assembled from it. That single property is the whole termination argument. A part of a finite value is strictly smaller than the whole in a sense nobody has to compute: it has strictly fewer constructors in it. Each call therefore works on a strictly smaller value than its caller, and a finite thread contains only finitely many parts, so no chain of calls can go on forever - every path down the thread runs out of structure and lands on the empty list. The important word is **read**. The argument is checked by looking at what is passed at each recursive call site and asking one question: *is this something the current value already carries?* If the answer is yes everywhere, the descent terminates, and no one has to simulate the walk or reason about how deep real threads get. ## What a depth counter is, and what it is not A counter threaded through the calls and decremented each time is a different kind of guarantee entirely: | | Recursing on parts | Recursing with a depth budget | |---|---|---| | Where the guarantee lives | in the shape of the value | in a number someone chose | | What a reviewer checks | that each argument is a part | that the number is large enough for real data | | Behaviour on unusually deep data | walks all of it | stops early and returns a partial answer | | Effect if the argument was not shrinking | not applicable - it is | hides the bug: the walk stops but the answer is wrong | | Cost of changing the data | none | the number has to be revisited | The last two rows are the ones that matter in review. A budget makes a non-terminating walk *look* fine, because it stops. It converts a control-flow bug into a silently truncated result, which is strictly harder to notice than a hang. And the number is a standing liability: it encodes an assumption about the data that nothing in the code enforces and nothing tells you when it becomes false. That is not an argument that guards are never justified. A thread arriving from outside the system might not be a finite tree at all - a value that refers back to itself is not a value the inductive definition describes, and a walker written against that definition inherits its assumption. The point is where the guard belongs: at the boundary that validates the input, once, rather than smeared across every recursive function that touches the data. ## The property, stated precisely Three conditions do the work together, and it is worth separating them: 1. **The value is finite and built from the listed ways.** No value contains itself, directly or through a chain. 2. **Every recursive argument is a proper part of the current argument.** Not equal to it, not derived from it - contained in it. 3. **Every way of building the value has a case**, including the way that carries no parts. Drop the first and there is nothing to descend through. Drop the second and the descent may never shrink. Drop the third and the descent shrinks to something the walker cannot answer for. With all three, termination is not a property anyone has to test for; it follows from how the value was built. ## Where engineers get this wrong - **Treating "smaller" as "smaller in some number I can measure".** A value with fewer comments in it is not automatically a part of the original. Size and containment are different relations, and only containment is free to check. - **Believing the counter is the safety net.** If both recursive arguments are parts, the counter is dead weight; if one of them is not, the counter changes a hang into a wrong answer. - **Reaching for a budget after one production incident on cyclic input.** The right response is to establish at the boundary that the input really is the finite shape the definition promises, so every walker downstream inherits it. - **Assuming termination implies cheapness.** The walk finishes, but it still visits every comment, and the frames it stacks up follow the thread's nesting depth. Termination is one property; cost and depth are others.
- A thread arrives from outside the system and might refer back to itself. Does the structural argument still hold?No - it assumes the value really is the finite shape the definition describes, and a value containing itself is not. The fix belongs at the boundary: establish once, where the data enters, that it is a finite tree. Every walker downstream then inherits the guarantee instead of each one carrying its own guard.
- Does recursing on a part also bound how long the walk takes?No. It bounds the number of calls to the number of parts, which is why the walk ends, but the time depends on what each case does per comment - a cheap count and an expensive per-comment lookup have the same call structure and wildly different cost.
- Two recursive calls per case - the comment's replies and the rest of the list. Does that weaken the argument?No. Both arguments are parts of the same value, so both branches shrink, and the total number of calls is bounded by the number of pieces in the thread. Branching factor affects how many calls there are, not whether the chain of calls terminates.
saying these in an interview costs you the question
- Adds a maximum-depth guard and calls that the termination argument
- Thinks any argument with fewer comments counts as structurally smaller
- Says termination also proves the walk is cheap or shallow
- Believes two recursive calls per case break the decreasing argument
- Claims the runtime detects repeated calls and stops them