Skip to main content
Version: main

Recursion

defineRecursiveFunctions defines mutually recursive functions that can call each other. Unlike loop (which is O(1) tail recursion via restart), recursive functions preserve the caller's pipeline across the call, forming a call stack of frames.

Mutual recursion: Peano arithmetic​

The classic is-even / is-odd mutual recursion. Each function checks if the input is zero (base case) or subtracts one and calls the other function (recursive case).

From demos/peano-arithmetic/run.ts:

runPipeline(
defineRecursiveFunctions<[
[number, boolean], // isEven: number → boolean
[number, boolean], // isOdd: number → boolean
]>(
(isEven, isOdd) => [
// isEven: 0 → true, n → isOdd(n - 1)
classifyZero.branch({
Zero: constant(true),
NonZero: subtractOne.then(isOdd),
}),
// isOdd: 0 → false, n → isEven(n - 1)
classifyZero.branch({
Zero: constant(false),
NonZero: subtractOne.then(isEven),
}),
],
)((isEven, _isOdd) => isEven),
7,
);
// isEven(7) → isOdd(6) → isEven(5) → isOdd(4) → isEven(3) → isOdd(2) → isEven(1) → isOdd(0) → false

The type parameter <[[number, boolean], [number, boolean]]> is explicit because TypeScript can't infer input/output types from circular definitions.

The first callback defines the function bodies — isEven and isOdd call each other via the call tokens. The second callback receives the same tokens and returns the workflow entry point.

When to use recursion vs. loop​

loopdefineRecursiveFunctions
Frame costO(1) — tears down and restartsO(n) — preserves caller across call
Mutual recursionNo — single bodyYes — multiple bodies call each other
Non-tail callsNo — body always loops or breaksYes — work after recursive call returns
Use caseIteration, retryTree traversal, mutual recursion, general recursion

Use loop for anything that's a while-loop. Use defineRecursiveFunctions when the recursion can't be expressed as tail recursion — mutual calls, post-recursion work, or tree-shaped call graphs.

How it works​

defineRecursiveFunctions desugars to a ResumeHandle with a Branch-based handler. Each function call is a tagged ResumePerform. The handler dispatches to the correct function body based on the tag (Call0, Call1, ...). The caller's pipeline is preserved as a ResumePerformFrame — when the callee completes, the caller resumes where it left off.

This is the same ResumeHandle/ResumePerform substrate that bind uses, but where bind's handler looks up a value from state, the recursive handler executes a function body.

See algebraic effect handlers for the full compilation model.