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).
Closures
A closure is a function bundled together with values from its surrounding scope. You define it in one place and pass it somewhere else to run. The closure carries captured data with it. The compiler transforms it to a type with both behavior and storage. The code executing the closure doesn't need to know where that data came from.
This allows closures to carry state and configuration into code that executes later or elsewhere. Configuration uses capture conventions to specify how the closure interacts with captured values, choosing between read-only references, mutable references, copies, or moves.
Mojo closures look like nested functions, but they use a special syntax. Curly braces after the argument list form a capture list that declares which outer values the closure captures and how it interacts with them.
The capture list is what distinguishes a closure from an ordinary nested function. It gives the compiler the information needed to manage captured values safely and eliminate ambiguity. When you specify how a value is captured, the compiler can enforce correct usage:
def main():
var multiplier = 3
def scale(x: Int) {read multiplier} -> Int:
return x * multiplier
print(scale(5)) # 15
scale is a closure. It captures multiplier from the enclosing
scope using its capture list ({read multiplier}). When you call
scale(5), the closure multiplies 5 by the captured value of
multiplier and returns 15.
Without a capture list, the inner function can't see anything outside its own arguments.
Why closures matter
Closures package behavior together with the data that behavior needs. You define the work in one place, then pass that package for execution later, elsewhere, or on different hardware.
This separation between defining work and executing work is central to how Mojo expresses computation.
Closures already appear throughout the Mojo standard library.
Functions such as parallelize, vectorize, and elementwise
accept closures. You describe the work; the library decides how to
distribute it across SIMD lanes, threads, or cores.
GPU kernels extend the same idea. A closure captures the values a kernel needs, then executes across a hardware dispatch geometry. This pattern appears in reductions and accumulations, where a closure captures running state and updates it while processing elements. It also appears in custom iteration, where the iterator controls traversal and the closure controls behavior.
In many languages, closures also power asynchronous programming. You pass a closure as a callback that runs after an operation completes, carrying the state and cleanup logic it needs.
Mojo doesn't yet support async execution or escaping closures (closures that outlive their enclosing scope), but the underlying model is the same: closures define what should happen, while something else decides when and where it runs.
The capture list
A capture list such as {read x, mut y} tells the compiler how the
closure captures and uses values from the surrounding scope.
For this list, x is captured as an immutable reference. The closure
can read it but can't modify it. y is captured as a mutable
reference, so the closure can modify it and those changes are visible
in the outer scope.
Mojo requires captures to be explicit. In a systems language, knowing exactly which values a closure holds, and whether it reads, copies, or takes ownership of them, matters for both performance and correctness.
Capture by immutable reference: read
Use read when the closure needs to see a value but not change it:
def main():
var threshold = 100
def is_over(x: Int) {read threshold} -> Bool:
return x > threshold
print(is_over(50)) # False
print(is_over(200)) # True
The closure reads an immutable reference to threshold. It sees the
current value each time it's called, including changes made after the
closure was created:
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 (sees updated limit)
Because read captures a reference, the closure reflects the
live state of the original value.
To capture every used outer value by immutable reference without
naming each one, use {read}:
def main():
var a = 1
var b = 2
def sum_ab() {read} -> Int:
return a + b
print(sum_ab()) # 3
Capture by mutable reference: mut
Use mut when a closure needs to modify a captured value and make
those changes visible in the enclosing scope:
def main():
var total = 0
def accumulate(x: Int) {mut total}:
total += x
accumulate(10)
accumulate(20)
print(total) # 30
Changes to total inside the closure modify the original variable
directly. This is a mutable reference, not a copy.
The implicit form {mut} captures every used outer value with a
mutable reference:
def main():
var count = 0
var items = List[String]()
def record(name: String) {mut}:
items.append(name)
count += 1
record("alpha")
record("beta")
print(count) # 2
print(items) # ['alpha', 'beta']
Capture by copy: var
Use var when the closure needs its own independent copy of a
value. Changes to the original don't affect the closure, and
changes inside the closure don't affect the original.
def main():
var snapshot_val = 42
def frozen() {var snapshot_val} -> Int:
return snapshot_val
snapshot_val = 999
print(frozen()) # 42 (captured the value at definition time)
The closure copied snapshot_val when it was created. Later
changes to snapshot_val in the outer scope don't affect the
closure's copy.
The implicit form {var} copies every used outer value:
def main():
var x = 10
var y = 20
def snap() {var} -> Int:
return x + y
x = 0
y = 0
print(snap()) # 30 (uses copied values)
Move capture: var name^
Use var name^ to transfer ownership of a value into the closure.
The closure consumes the outer binding, which can't be used after
the closure is created:
def main():
var data: List[Int] = [1, 2, 3]
def take_data() {var data^}:
print(data)
take_data() # [1, 2, 3]
# data can't be used here: ownership transferred to the closure
# print(data) # Uncomment for error: 'data' is uninitialized after move
Move capture avoids a copy entirely. The value moves into the closure's storage. This is useful for types that are expensive to copy or for transferring unique ownership.
Copyable closures: var^
The {var^} capture list moves all referenced outer values into the
closure. When the captured types are Copyable, the closure value also
becomes copyable.
This allows the closure itself to be assigned to new variables or passed by value.
def main():
var label = "sensor-1"
def tag() {var^} -> String:
return label
var also_tag = tag # copies the closure (and its captures)
print(tag()) # sensor-1
print(also_tag()) # sensor-1
Without {var^}, closures can't be assigned to new variables or
copied.
Caller-determined mutability: ref
Use {ref name} when the closure's mutability depends on the
caller's context. If the caller provides a mutable reference, the
closure captures mutably. If immutable, the closure captures
immutably.
The following example uses comptime if origin_of(items).mut to inspect
how the closure captures the value at each call site:
def show_mutability(ref items: List[Int]):
def report() {ref items}:
comptime if origin_of(items).mut:
print("mut")
else:
print("immut")
report()
# Show immutability: `xs` uses the default `read` argument convention
def from_read(xs: List[Int]):
show_mutability(xs) # xs is an immutable reference here
# Show mutability: `xs` uses the `mut` argument convention
def from_mut(mut xs: List[Int]):
show_mutability(xs) # xs is a mutable reference here
def main():
var nums: List[Int] = [10, 20, 30]
from_read(nums) # immut
from_mut(nums) # mut
{ref name} doesn't choose a mutability. It shares the captured name's
existing origin. The mutability is whatever that origin already carries,
decided wherever name was bound. This is often the function's own ref
parameter, ultimately resolved at the call site.
Empty capture list: {}
An empty capture list means the closure uses nothing from its surrounding scope. It's a plain function that happens to be defined inside another function:
def main():
def doubled(x: Int) {} -> Int:
return x * 2
print(doubled(5)) # 10
The body may only use its own arguments. Referencing any outer value is a compile error:
# This example doesn't compile
def main():
var a = 42
def wrong() {}:
print(a) # error: no capture convention for 'a'
Mixing capture conventions
A capture list is a comma-separated sequence of independent entries. Each entry specifies its own convention, and conventions don't carry over from one entry to the next.
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 definition time)
Each entry is self-contained: read config is a read-only reference,
mut count is a mutable reference, and var label is a copy. A bare name
without a convention keyword defaults to read:
def main():
var x = 10
def show() {x}: # same as {read x}
print(x)
show() # 10
Setting a default convention
A convention list can mix implicit and explicit entries, such as
{read, mut count, var label}. You may use at most one implicit
entry per capture list, and you can place the entries in any order.
{mut count, var label, read} is equivalent to
{var label, read, mut count}.
For example:
def main():
var a = 1
var b = 2
var z = "snapshot"
def mixed() {mut, var z}:
a += 10
b += 20
print(a, b, z)
mixed() # 11 22 snapshot
z = "changed"
mixed() # 21 42 snapshot
# z was copied at def-time
# Changes to outer z don't reach the closure
Closures in practice
Configurable behavior
Closures let you build specialized behavior from general-purpose parts. The following generic function accepts any callable with the expected signature. The closure carries the configuration:
# `G` matches any `def(String) -> None` callable
def greet_all[G: def(String) -> None](names: List[String], greet: G):
for n in names:
greet(n)
def main():
var names: List[String] = ["Alice", "Bob"]
var greeting = "Hello"
def greeter(name: String) {read greeting}:
print(greeting + ", " + name + "!")
greet_all(names, greeter)
# Hello, Alice!
# Hello, Bob!
greeting = "Hi"
greet_all(names, greeter)
# Hi, Alice!
# Hi, Bob!
The inner function greeter captures greeting as an immutable read-only
reference.
greet_all doesn't know anything about the greeting itself. It only
knows how to call a function with the type
def(String) -> None.
Because the closure captures greeting by reference instead of by
copy, changes to greeting between calls are visible inside the
closure.
Accumulating state
Closures with mut captures can build up results across
multiple calls.
def main():
var log = List[String]()
def record(event: String) {mut log}:
log.append(event)
record("started")
record("processed item")
record("finished")
for entry in log:
print(entry)
# started
# processed item
# finished
The closure record mutates log in the outer scope. Each call
appends to the same list without passing it as an argument.
Separating logic from execution
The standard library uses closures to separate what you compute from how it gets executed. You provide the logic; the library handles parallelism, vectorization, or hardware dispatch.
from std.algorithm import parallelize
def main():
var results = List[Int](length=8, fill=0)
def work(i: Int) {mut results}:
results[i] = i * i
parallelize(work, 8)
print(results) # [0, 1, 4, 9, 16, 25, 36, 49]
You define work with the logic for a single element. parallelize calls
it across available threads. Each call to work accesses the same results
through mutable capture. The squared values land in the outer list, ready
to use after parallelize finishes.