Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Cleanup before a tail call

A function that ends with a call makes a tail call. Plenty evaluates the arguments, cleans up the function’s remaining values, and only then enters the called function:

class Resource:
    name: str

    def __del__(self) -> ():
        print(("release " + self.name).unwrap()).unwrap()

def finish(total: i64) -> Result[i64, IoError | OverflowError[i64]]:
    print("finish")?
    Ok(total)

def step(total: i64) -> Result[i64, IoError | OverflowError[i64]]:
    scratch = Resource("scratch")
    finish(total + 1)

def main() -> Result[(), AllocError | IoError | OverflowError[i64]]:
    print(step(41)?)?
    Ok(())
release scratch
finish
42

step and finish deliberately share a Result type, including the arithmetic error that can originate in step, so forwarding the result remains a tail call. scratch is released before finish runs. A value passed as an argument moves to the called function, which cleans it up instead. Tail recursion therefore keeps its constant stack use when each step owns a resource. A call that borrows one of the function’s values, such as inspect(&scratch) or a method call on it, cannot release that value first: it runs as an ordinary call and cleanup follows. A reference the function received as a parameter refers to a value outside the function, so passing it on is still a tail call.

The destructor’s printing is only an observation aid for this cleanup example; its explicit unwrap() terminates if that diagnostic output fails. Ordinary work, including finish, propagates its errors instead.