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

Recursion

Functions can be recursive. Every call creates an activation: the function’s body, live, with its own state — its subscriptions, its timers, its counts, its own bindings. Each iteration of a recursion owns its state, exactly as each element owns its callback in array::map.

A recursive call in tail position whose body holds no state (no async operation, no stateful builtin such as count or uniq, no <- to one of its own bindings) and compiles to native code runs as a native loop instead: constant stack, any depth, answering exactly what the activations would. A body that does not compile keeps an activation per call, which costs memory and time for every call: one per level for a linear recursion, one per call in the whole call tree for a branching one like a naive fib. The things that keep a body out of native code are reading a reference (*r), a ? raising to a handler, a <- to a variable outside the function, and a builtin without a fast call; move them out of the loop when the loop is deep.

That is what makes recursion the way to write a reactive traversal:

let rec publish_all = |paths: Array<string>, i: i64| select paths {
  [] => null,
  [p, rest..] => {
    sys::net::publish(p, i)$;
    publish_all(rest, i + 1)
  }
}

Every path gets its own publish site, retained across cycles. When the array changes, the activations are fed again by position, and those past the new end are deleted, their publishes withdrawn; a later regrow builds fresh ones.

Recursion is bounded by memory, not by a limit: non-tail recursion nests on heap-allocated stack segments, in the interpreter and in compiled code alike. An embedder may cap it with a stack budget (graphix_compiler::set_stack_budget, or GRAPHIX_STACK_BUDGET in bytes or with a K, M or G suffix, as in 64M), and with an activation budget, the live activations (calls, collection slots) a runtime may hold (Control::set_activation_budget, or GRAPHIX_ACTIVATION_BUDGET); unlimited by default. A program that exceeds a budget is stopped the way Ctrl-C stops it.

If you rely on a function running as a constant-space loop, assert it:

#[tail_recursive]
let rec sum_to = |n: i64, acc: i64| -> i64 select n {
  0 => acc,
  _ => sum_to(n - 1, acc + n)
};
let total = #[native] sum_to(1000000, 0)

#[tail_recursive] asserts the shape and #[native] that the call compiles to native code. #[tail_recursive] is a compile-time check: if any recursive call is not in tail position, if the body is stateful or async, if a parameter is labeled (the loop rebinds positional parameters), if the body holds memory of its own per call (a collection operation, or a call to a function that holds one, which each activation keeps apart), if the function does not recurse at all, or if it recurses mutually, the program does not compile. Only compiled code runs the loop: the interpreter keeps an activation per call, so the assertion holds when fusion is on and the body compiles to native code, and a body that does not (it takes a reference, say) fails the compile. With --no-fusion it is not checked, as #[native] is not.

Two related assertions document a function’s timing: #[sync] asserts that every output appears on the same cycle as its trigger (the body reaches no timer, IO, or other async operation), and #[async] asserts the opposite. Like #[tail_recursive] they never change how the function compiles — they only fail the compile when the compiler disagrees with your expectation.

The check needs a call to look at. A function called only through a value chosen at runtime (let g = select b { true => f, false => h }; g(x)) is checked when that call first reaches it; the program is already running then, so a failure is reported the way an unhandled error is, on stderr and in the log.

With that out of the way, lets write a recursive function to add up pairs of numbers in an array,

let rec add_pairs = 'a: Number |a: Array<'a>| -> Array<'a> select a {
  [e0, e1, tl..] => array::push_front(add_pairs(tl), e0 + e1),
  a => a
}

running this we see,

〉add_pairs([1, 2, 3, 4, 5])
-: Array<'a: i64>
[3, 7, 5]