Closures and Captures

Closure values, callable types, and by-value captures.

A callable is any value that can be called with (...). A closure is a callable value created with a closure literal. Captures are implicit and by value.

fn main() {
    let base = 10;
    let add = fn(x: i32) -> i32 {
        return x + base;
    };

    assert base == 10;
    assert add(5) == 15;
}

Capturing and non-capturing closures use the same call syntax. Whether a closure needs captured storage or can be optimized into a direct call target is an implementation detail.

Because captures are implicit, the important question is ownership: using a name from the outer scope inside the closure either copies that value or moves it into the closure at closure creation time.

Quick Rules

FormPurpose
fn(...) { ... }Create a closure value.
(x) => exprCreate a short closure value.
(T) -> RName a callable type.

Copy captures are copied, so the source value remains usable. Move-only captures are moved into the closure when the closure is created. References and raw pointers cannot be captured in 0.0.0-dev. See Move, Copy, and Clone for the ownership rules behind those transfers.

If a closure should inspect a borrowed value without owning it, make the value an explicit parameter instead of relying on capture.

Captures

Capturing a Copy value copies it into the closure.

fn main() {
    let base = 10;
    let add = fn(x: i32) -> i32 {
        return x + base;
    };

    assert base == 10;
    assert add(5) == 15;
}

Capturing a move-only value moves it into the closure. The original binding cannot be used afterward.

struct Box {
    value: i32,
}

fn main() {
    let box = Box { value: 7 };
    let get = fn() -> i32 {
        return box.value;
    };

    assert get() == 7;
}

The move happens when the closure is created, not when it is first called:

struct Token {
    value: i32,
}

fn main() {
    let token = Token { value: 1 };
    let read = fn() -> i32 {
        return token.value;
    };

    // token is no longer usable here.
    assert read() == 1;
}

References and raw pointers cannot be captured in 0.0.0-dev. Pass borrowed values as parameters instead of hiding them inside a closure capture.

fn inspect(value: &i32, f: (&i32) -> i32) -> i32 {
    return f(value);
}

Callable Types

Callable types use the call signature syntax (ArgTypes...) -> ReturnType. Closures can be passed where a matching callable type is expected.

fn apply(value: i32, f: (i32) -> i32) -> i32 {
    return f(value);
}

fn main() {
    let base = 10;
    let value = apply(5, fn(x: i32) -> i32 {
        return x + base;
    });

    assert value == 15;
}

Argument and return types must match the callable signature.

Callable values can also be returned from functions:

fn make_add(base: i32) -> (i32) -> i32 {
    return fn(x: i32) -> i32 {
        return x + base;
    };
}

fn main() {
    let add = make_add(8);
    assert add(7) == 15;
}

After a closure is assigned to a callable-typed variable, the value is owned and move-only. Passing, assigning, or storing that callable transfers ownership.

fn call_once(f: () -> i32) -> i32 {
    return f();
}

fn main() {
    let base = 41;
    let f: () -> i32 = fn() -> i32 {
        return base + 1;
    };

    assert call_once(f) == 42;
}

Short Closures

Short closure syntax uses the same capture rules once parameter and return types are known.

fn apply(value: i32, f: (i32) -> i32) -> i32 {
    return f(value);
}

fn main() {
    let doubled = apply(21, (x) => x * 2);
    assert doubled == 42;
}

Short closures need an expected callable signature for parameter and return type inference in current source. A typed let, function parameter, return type, match arm, or array element can provide that context.

fn main() {
    let base = 4;
    let add: (i32) -> i32 = (x) => x + base;
    assert add(6) == 10;
}

Thread-Capable Boundaries

Closures are thread-send-safe only when every captured field is thread-send-safe. Direct closure expressions and named functions can cross a channel boundary when all captures are send-safe.

import std.channel;

async fn main() {
    let (tx, rx) = channel.bounded<() -> i32>(1);
    let base = 40;

    try await tx.send(fn() -> i32 {
        return base + 2;
    });

    let msg = await rx.recv();
    match msg {
        .Value(cb) => assert cb() == 42,
        .Closed => assert false,
    }
}

Already-erased callable values are conservatively rejected at channel boundaries because the checker can no longer inspect the original captures. Capturing borrowed references, raw pointers, strings, arrays, or non-copy structs is also rejected when the closure must move across thread-capable boundaries.