For the complete Mojo documentation index, see llms.txt. Markdown versions of all pages are available by appending .md to any URL (e.g. /docs/manual/basics.md).
Mojo closure declarations reference
A closure is a nested function with a capture list that controls how it accesses values from its enclosing scope:
def main():
var multiplier = 3
def scale(x: Int) {read multiplier} -> Int:
return x * multiplier
print(scale(5)) # 15
{read multiplier} references multiplier from the enclosing
scope as an immutable reference. Without the capture list,
referencing any outer value is a compile error.
A closure is created when its enclosing def runs and exists
only for the lifetime of that enclosing scope. Mojo doesn't
support escaping closures or async execution.
Closure syntax
def name(argument-list) {capture-list} -> ReturnType:
body
def name[parameter-list](argument-list) {capture-list}
-> ReturnType:
body
def name(argument-list) raises {capture-list} -> ReturnType:
body
Effects (raises) go between the argument
list and the capture list. The capture list appears immediately
before the return arrow. It can be empty ({}) or omitted
entirely; both forms prohibit references to outer values.
The argument list, parameter list, effects, return type, and
where clauses follow the same rules as top-level functions.
See Function declarations.
Capture list grammar
A capture list is a brace-enclosed, comma-separated sequence of entries:
| Form | Meaning |
|---|---|
<conv> name | Capture name with convention <conv> |
<conv> | Default convention for all free variables |
name | Capture name with convention read |
<conv> name^ | Move-capture (only with var or no <conv>) |
<conv> is one of read, mut, var, ref. Position within
the list isn't significant: {mut, var z} and {var z, mut}
are equivalent. Trailing commas are accepted.
At most one entry can omit a name (the default-convention entry). A second produces:
error: default capture convention was already specified;
remove the duplicate
The ^ marker is only legal on var entries or entries with no
convention keyword:
error: '^' requires 'var' convention; write 'var x^' to
move a capture
Capture conventions
| Convention | Form | Storage in closure | Lifetime tie to outer |
|---|---|---|---|
read | {read name} / {read} | Immutable reference | Live |
mut | {mut name} / {mut} | Mutable reference | Live |
ref | {ref name} / {ref} | Reference, mutability from origin | Live |
var | {var name} / {var} | Owned copy | Independent |
| Move | {var name^} | Owned, consumed from outer | Consumes outer |
| Copyable | {var^} | Owned, closure is Copyable | Independent |
read
Immutable reference. The closure observes the outer value's current state at each call:
def main():
var limit = 10
def check(x: Int) {read limit} -> Bool:
return x < limit
print(check(5)) # True
limit = 3
print(check(5)) # False
{read} (no name) applies read to every free variable in
the body. A bare name without a convention keyword also defaults
to read: {x} is equivalent to {read x}.
mut
Mutable reference. Writes inside the closure modify the outer binding:
def main():
var total = 0
def accumulate(x: Int) {mut total}:
total += x
accumulate(10)
accumulate(20)
print(total) # 30
{mut} (no name) applies mutable-reference capture to every
free variable in the body.
var
Owned copy. The closure receives its own value, constructed by calling the type's copy constructor when the closure is declared. Later changes to the outer binding don't affect the closure's copy, and vice versa:
def main():
var snapshot = 42
def frozen() {var snapshot} -> Int:
return snapshot
snapshot = 999
print(frozen()) # 42
{var} (no name) copies every free variable in the body.
The copy constructor runs once per closure declaration.
Capturing a large List or String by var allocates at that
point. Use read or mut when an independent copy isn't
needed.
Move capture: var name^
Transfers ownership of name into the closure. The outer
binding is consumed; using it after the closure declaration is a
compile error:
def main():
var data: List[Int] = [1, 2, 3]
def take_data() {var data^}:
print(data)
take_data() # [1, 2, 3]
# print(data) # error: 'data' is uninitialized
# after move
Move capture skips the copy that var name would perform and
is the only way to capture a move-only type by value.
Constraints:
- Only legal after
varor after a bare (convention-less) name. {read name^},{mut name^}, and{ref name^}are rejected.- A bare
name^is equivalent tovar name^. Both produce the samekConventionMoveentry. The compiler accepts both forms, butmojo formatcurrently rejects the bare form. Use{var name^}in code that must round-trip through the formatter.
Copyable closures: var^
{var^} (no name) applies move capture as the default for
every free variable in the body. When every captured type is
Copyable, the resulting closure value is also Copyable:
def main():
var label = "sensor-1"
def tag() {var^} -> String:
return label
var clone = tag # closure value copied
print(tag()) # sensor-1
print(clone()) # sensor-1
Copying the closure invokes the copy constructor of each captured value. The constructor fires at the assignment, not at the closure declaration.
Constraints:
{var^}is a default-convention entry. At most one default-convention entry per capture list.- If any captured type is move-only, the closure is
Movablebut notCopyable.
Comparison with {var name^}:
| Form | Captured names | Closure value |
|---|---|---|
{var name^} | Only name, by move | Not Copyable by default |
{var^} | All referenced names, by move | Copyable if captures are Copyable |
ref
Reference whose mutability is inherited from the outer
binding's origin. The closure doesn't pick read or mut; it
forwards whatever the origin already carries:
def show_mutability(ref items: List[Int]):
def report() {ref items}:
comptime if origin_of(items).mut:
print("mut")
else:
print("immut")
report()
# `xs` uses default `read` convention, immutable reference
def from_read(xs: List[Int]):
show_mutability(xs)
# `xs` uses `mut` convention, mutable reference
def from_mut(mut xs: List[Int]):
show_mutability(xs)
def main():
var nums: List[Int] = [10, 20, 30]
from_read(nums) # immut
from_mut(nums) # mut
ref is the only convention that forwards origin information
unchanged. read and mut create references with a fixed
mutability; var removes the origin relationship entirely.
ref captures are intended for generic code that must operate
across mutability contexts. In ordinary closures, read and
mut produce clearer signatures.
Empty and omitted capture lists
{} and no capture list at all produce the same result: any
reference to an outer value is rejected:
error: Could not infer capture convention of the captured
value a
Both forms allow a body that uses only its own arguments. The function then behaves as a plain nested function with no captures.
{} is preferred when the absence of captures is intentional;
the explicit braces make the constraint visible at the
declaration.
Mixing conventions
Each entry carries its own convention independently:
def main():
var config = "prod"
var count = 0
var label = "run-1"
def process() {read config, mut count, var label}:
count += 1
print(config, count, label)
process() # prod 1 run-1
label = "run-2"
process() # prod 2 run-1
# (label was copied at declaration time)
A bare name in a mixed list uses read, not the convention of
its neighbors:
# y is captured as 'read', not 'mut'
def f() {var z, mut x, y}:
# ...
Default convention
A convention keyword without a name sets the default for every free variable not named explicitly:
def main():
var a = 1
var b = 2
var z = "snapshot"
def mixed() {mut, var z}:
a += 10 # 'a' uses default: mut
b += 20 # 'b' uses default: mut
print(a, b, z)
mixed() # 11 22 snapshot
z = "changed"
mixed() # 21 42 snapshot
# ('z' was copied at declaration)
Rules:
- At most one default-convention entry per list.
- Position within the list isn't significant.
- Trailing commas are accepted.
- The default doesn't apply to names covered by an explicit
entry. In
{mut, var z}, the explicitvar zoverrides the default forz.
Parametric closures
A closure can declare its own compile-time parameter list:
def main():
# The `Intable` trait supports `Int` conversion
def double[T: Intable](x: T) {} -> Int:
return Int(x) * 2
print(double[Int](5)) # 10
print(double[Float64](3.4)) # 6
The parameter list, capture list, effects, and return type
appear in the same order as on top-level functions:
name[parameters](arguments) effects {captures} -> ReturnType.
Parameters and captures compose: the closure's parameters are
bound at each call site, while the capture list controls its
relationship to the enclosing scope. Variadic parameters are
also legal (def closure[*Ts: Coord](*args: *Ts)).
Effects
Effects appear between the argument list and the capture list.
| Effect | Form |
|---|---|
raises | (args) raises {captures} -> T |
register_passable | (args) register_passable {captures} -> T |
raises example:
def main() raises:
var y = 2
def divide(x: Int) raises {var y} -> Int:
if y == 0:
raise Error("divide by zero")
return x // y
print(divide(10)) # 5
register_passable example:
def main():
var base = 10
def shift(x: Int) register_passable {var base} -> Int:
return x + base
print(shift(3)) # 13
thin and abi("C") apply only to closure types used as
function parameters, not to closure declarations. thin
describes a non-capturing function type, which is incompatible
with a closure that captures. See Function
declarations.
Nesting
Closures can nest inside closures. Each level has its own capture list. A name captured at one level is visible to inner levels through their own capture lists:
def main():
var y = 4
def outer() {var y} -> Int:
def inner() {var y} -> Int:
return y
return inner() + y
print(outer()) # 8
An inner closure can capture an outer closure by name. This is how nested callbacks compose:
def main():
def make_adder(n: Int):
def add(x: Int) {var n} -> Int:
return x + n
def twice(x: Int) {var add} -> Int:
return add(add(x))
print(twice(5)) # ((5 + 3) + 3) = 11
# add(add(5)) = add(8) = 11
make_adder(3)
Closures are values and can be used in capture lists. An inner closure that
names an outer closure must declare the same kind of capture (var,
read, etc.) as it would for any other value.
Capture-list errors
| Compiler complaint | Trigger |
|---|---|
Transfer sigil ^ without var convention | ^ after mut, read, or ref |
| Duplicate default convention | Two bare convention keywords in one list |
| Unrecognized token in capture position | Token that isn't a convention keyword or name |
| Missing comma between entries | Identifier followed by an unrecognized token |
| Unterminated capture list | Missing closing } |
| Outer name not covered by capture list | Body references an outer name the capture list doesn't cover |
Copy or move capture of non-register-passable type in register_passable closure | {var name} or {var name^} with a non-register-passable type |
| Use after move capture | Reference to a name after {var name^} consumed it |
Restrictions
- No escape. A closure can't outlive its enclosing scope. Returning a closure from its declaring function or storing it past the enclosing scope's end isn't supported.
- No
thinorabi("C")on declarations. These apply only to closure types used as function parameters; a declaration with captures can't bethin. - Trait conformance through captures. A struct can contain a
closure-typed field and conform to a trait through it, but
every method of that trait must be declared
capturinguntil the capturing effect is removed (seeunified_closure_structs.mojo).