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.
| Form | Purpose |
|---|---|
fn(...) { ... } | Create a closure value. |
(x) => expr | Create a short closure value. |
(T) -> R | Name 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.
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 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 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;
}
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.