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

Accept functions and closures through one API

Constrain a type parameter by its call signature to accept named functions and captured closures through one parameter. Borrow the callback when its caller should keep it:

def apply[T, E, F: Callable[[T], Result[T, E]]](f: &F, value: T) -> Result[T, E]:
    f(value)

def double(value: i64) -> Result[i64, OverflowError[i64]]:
    Ok(value * 2)

def main() -> Result[(), IoError | OverflowError[i64] | OverflowError[u8]]:
    offset = 2u8
    add = def [offset](value: u8) -> Result[u8, OverflowError[u8]]:
        Ok(offset + value)
    print(apply(&double, 3)?)?
    print(apply(&add, 3u8)?)?
    Ok(())
6
5

A short callback can be written in the call. An anonymous function needs parentheses to be the operand of &:

def apply[T, E, F: Callable[[T], Result[T, E]]](f: &F, value: T) -> Result[T, E]:
    f(value)

def main() -> Result[(), IoError | OverflowError[i64]]:
    offset = 2
    print(apply(&(def [offset](value: i64) -> Result[i64, OverflowError[i64]]: Ok(offset + value)), 3)?)?
    print(apply(&(def(value: i64) -> Result[i64, OverflowError[i64]]: Ok(value * value)), 3)?)?
    Ok(())
5
9

F preserves the callback’s concrete environment type. Both arguments agree on T, the input and successful output type; E preserves the callback’s error type. The caller handles the returned Result normally. Use &mut F for a callback that changes its captured state. Borrowed captures stay protected throughout the call.

Borrow a named function directly with &double, or with &mut double for a &mut F parameter; a separate binding is optional. A function has no state, so each &mut borrow takes its own copy of the code address.

A concrete parameter such as f: Callable[[i64], i64] holds only a function’s code address. The constraint F: Callable[[i64], i64] instead checks whether the actual type can be called with that signature; its captured state remains inline. Adding & to the concrete Callable type does not make it accept closures. Closure[...] describes a concrete environment, chiefly when returning one from a factory.

Passing captured state to a code-only parameter explains the mismatch and suggests the generic form:

def apply(f: Callable[[i64], Result[i64, OverflowError[i64]]], value: i64) -> Result[i64, OverflowError[i64]]:
    f(value)
def main() -> ():
    offset = 3
    add = def [offset](value: i64) -> Result[i64, OverflowError[i64]]:
        Ok(value + offset)
    apply(add, 4)
for a generic callback API, use `F: Callable[[i64], Result[i64, OverflowError[i64]]]` with `callback: &F`

Let the callback determine a separate result type

The first helpers require the input and successful output to have the same type T. Give the complete result its own parameter U to allow other transformations, including infallible callbacks. The callback’s concrete signature determines U, including when it does not appear in another argument. This example uses both forms of callback through that shared interface:

def apply[T, U, F: Callable[[T], U]](callback: &F, value: T) -> U:
    callback(value)

def double(value: i64) -> Result[i64, OverflowError[i64]]:
    Ok(value * 2)

def main() -> Result[(), IoError | OverflowError[i64]]:
    offset = 7
    add = def [offset](value: i64) -> Result[i64, OverflowError[i64]]:
        Ok(offset + value)
    print(apply(&double, 10)?)?
    print(apply(&add, 10)?)?
    Ok(())
20
17