You trampoline a recursion whose work happens after the recursive call returns; why does it still need memory proportional to depth?
answer
- what is still pending after the call
- the invocation was holding something
- make the leftover work a value
- a chain of continuations, not invocations
- relocated depth, not removed depth
basics
~20 sThe work pending after the call cannot vanish. To let each step return, that leftover is packaged as a continuation value, so for a chain of n steps you hold n continuations instead of n unfinished invocations.
solid answer
~40 sA trampoline only flattens a call that is a step's last act. When a step must combine its own value with the result of the rest of the chain, something is still pending, so the step cannot return yet. You first make the leftover explicit: rewrite it as a one-argument function — a continuation — and pass it to the next step, which hands its result to it instead of returning. Now every call really is the last act, so the loop can drive it. But the pending work moved rather than disappeared: n levels of it are now a chain of n continuation values. You traded a small fixed limit that kills the process for a large growable one you can measure.
code
pseudocode · 4 linesfunction totalFrom(step)
if step.isFinal then
return step.amount
return step.amount + totalFrom(step.next)go deeper
The takeaway is that a step which still has work to do after the inner call cannot simply return. Something has to remember that leftover work, and remembering it costs space.
Explain the two shapes and why only one of them bounces directly. Be able to describe the leftover as a one-argument function passed forward, and say that n steps then mean n such functions held at once.
Show that you would measure it: memory proportional to depth, an allocation failure instead of an abrupt death, and a test chain longer than the old limit. Name the unwind as the second place the design can still nest.
The call is whether to accept linear memory for unbounded input at all, or to change the data so nothing is pending. Continuations are the fallback when the combination genuinely needs the result of the rest first.
## The shape a trampoline alone cannot flatten A trampoline works by letting a step **finish** and hand back a description of the next call. That is only possible when the next call is the last thing the step does. Now take the other shape: a step in an approval chain that must add its own amount to the total of everything after it. It calls the rest of the chain, waits, and then adds. The addition is unfinished work, and unfinished work means the step cannot return — so there is nothing to hand a loop. Wrapping the inner call in a description changes nothing here, because the step would have to invoke that description itself in order to get a number to add to. So the first move is not the trampoline at all. It is making the pending work **explicit**. ## Turning the leftover into a value The leftover — *whatever comes back, add my amount to it, then do whatever my own caller was going to do* — is a function of one argument. Name it, pass it along as an extra parameter, and you have **continuation-passing style**: every step receives the rest of the computation as a value and hands its result to that value instead of returning it. 1. **Name the leftover.** Turn `my amount + result of the rest` into a one-argument function: given the result of the rest, produce the final answer. 2. **Pass it down.** The step stops waiting for a result. It invokes the next step with a *larger* continuation — the one it was given, wrapped in its own addition. 3. **Bounce it.** Every call is now a step's last act, so each one can be returned to a driver loop as a description rather than performed. The base case no longer returns a number to a caller. It hands its number to the continuation it was given, which is the only thing that knows what to do with it. ## Where the depth actually went Let n be the number of steps in the chain. The pending additions did not evaporate; they were relocated. Before, n pieces of unfinished work sat inside n unfinished invocations. After, they sit in a chain of n continuation values, each holding the amount captured from its own step and a reference to the continuation it wraps. | | direct recursion | after the transform | |---|---|---| | where pending work lives | inside unfinished invocations | inside continuation values | | how much, for n steps | n pieces | n pieces | | how it is reached | returning through every caller | applying the outermost continuation | | what a too-long chain does | exhausts a small fixed region abruptly | exhausts a large growable one, or survives | | how to make it smaller | restructure so nothing is pending | restructure so nothing is pending | The last row is the honest one: the transform changes *where* depth is paid, not *whether*. The gain is real all the same — the region it moves into is typically far larger, it grows on demand, its use can be measured while the job runs, and exhausting it is an allocation failure you can see approaching rather than a collapse at a depth nobody chose. - If nothing is pending after the recursive call, no continuations are built and only the latest description is live. That is the genuinely constant-space case, and it is the one to aim for first. - If something is pending, expect memory proportional to depth, whatever shape the rewrite takes. - A step that captures a large value into its continuation keeps that value alive for the whole run, so a cheap-looking step can be an expensive link in the chain. ## The trap at the end of the chain Once the base case hands its result to the continuation, the whole chain has to be applied: the innermost continuation calls the one it wraps, which calls the one *it* wraps. If those applications are ordinary calls, the unwind is a recursion n deep — the same collapse, relocated to the end of the run, where a short test chain will never expose it. A trampolined design has to bounce here too: applying a continuation must **return** a description to the driver loop rather than call the next continuation directly. Then the unwind is n further turns of the same loop. This single detail is what separates a design that survives the longest chain anyone can construct from one that survives the demo. ## What to check before trusting it - Is anything actually pending after the recursive call? If not, do the simpler rewrite; continuations are not needed and cost more. - Does every continuation application go back through the loop, including the final unwind? - Does memory grow with input, and is the largest realistic input inside that budget? - Is the chain length genuinely unbounded, or bounded by something you control? - Has it been run against a chain longer than any depth the fixed region could ever have held?
- The chain no longer dies at 50,000 steps but memory climbs with input. Is that a bug in the rewrite?No, it is the rewrite working as designed for this shape. The pending combination has to be recorded somewhere, and continuations are that record. It is a bug only if nothing was actually pending, which means the simpler returning rewrite would have used constant space.
- Why is a short test chain a bad test of a continuation-passing trampoline?Because the shape fails in two places, and a short chain exercises neither. Depth failures need a chain longer than the fixed region ever held, and the unwind failure only appears when the built-up continuations are applied — which a short chain does trivially, whether or not each application returns to the loop.
- Can you avoid building continuations at all for this chain?Sometimes: if the combination is one that can be applied on the way down rather than on the way back, carry the running total forward instead, and nothing is left pending. Where the combination genuinely needs the result of the rest first, the leftover must be recorded somewhere and a continuation is that somewhere.
saying these in an interview costs you the question
- Says trampolining alone makes any recursive shape safe at depth
- Claims the transform removes the pending work rather than relocating it
- Thinks the continuation chain is free because it is not invocations
- Assumes applying the built-up continuations needs no bouncing
- Treats unbounded memory growth as harmless once the crash is gone