Mojo Closures 2026

Mojo Closures

Before this release, Mojo has had several flavors of closures:

  • Legacy capturing[_] closures@parameter functions passed only as compile-time type parameters, with implicit captures (and @__copy_capture to opt into copy semantics).
  • Escaping closuresescaping functions emitted as heap-allocated struct instances and passed as runtime values.
  • Unified closures — declared with the unified keyword, with explicit capture lists (e.g. unified {var x, mut y}), and passed as both a type parameter and a runtime value.

To ease usage of closures and simplify the API the team is collapsing these variants into a single representation.

In the latest nightly, only unified closures exist and they are the default. There is no unified keyword to distinguish them anymore.
More detailed documentation is coming and we are actively working on bug fixes.

Before this change, legacy closures were expressed in the following way:

@always_inline
@parameter
def init_value[width: Int]() capturing -> SIMD[DType.float32, width]:
    return 0.0

@always_inline
@__copy_capture(output, scale)
@parameter
def write_result[
    width: Int
](idx: Int, val: SIMD[DType.float32, width]) capturing:
    output.unsafe_ptr().store(idx, val * scale)

compute_grid[width, init_value, write_result](shape)

Now, closures will look a little different. The same code above is now expressed:

def init_value[width: Int]() -> SIMD[DType.float32, width]:
    return 0.0

def write_result[
    width: Int
](idx: Int, val: SIMD[DType.float32, width]) {
    var output, var scale
}:
    output.unsafe_ptr().store(idx, val * scale)

compute_grid[width](init_value, write_result, shape)

Notice how the closures move from the type parameter list ([...]) into the runtime argument list ((...)) — that’s the headline change.

The recipe in one breath:

  1. Drop @parameter and @__copy_capture(...) from your closures.
  2. Add an explicit { ... } capture list.
  3. Pass closures as value arguments (first positional args), not as explicit type parameters — the compiler infers the types. Keep only the non-closure params (width, dtype, etc.) explicit.

I am happy to answer any questions and discuss the change. Please file bug reports for any unexpected behavior you may encounter.

Having updated some closures in the Mojo GPU Puzzles for the latest nightly releases, this is a helpful simplification. Thanks sharing the guidance and rationale!

This is very helpful and I’m very excited by this change.

I think it would be useful to have a brief intro to Mojo closures for users used to thinking about C++ lambdas. For me, the subtle differences have caused a significant amount of confusion.

Great work! I know this has been long in the making so I’m glad to see it start to come to fruition. What is the current purpose/future of the thin keyword?

Having closures that aren’t a big hole in the type system is fantastic and I’m very happy this made it for 1.0!

One of my big questions is how I describe a closure in an API.

For instance, if I want to implement InlineArray.foreach, which runs a closure with each member as an argument, I want to be able to specify a few things:

  1. The closure must remain valid for however long it takes me to make the function call (which means I need to be able to name the origin of its borrows)
  2. The closure must be sound to call more than once (it cannot destroy a capture without replacing it)
  3. If I want to use parallelize, the closure must be safe to send between threads (depends on the threading model, so an item for later discussion), and it must be safe to have multiple threads calling the closure at once.
  4. If I want something which is a pure function pointer (possibly due to needing to pass it to a C API), I need to be able to describe that.

Rust does this by having fn(T) -> K be the type of a function pointer, and then it has distinct traits that coroutines implement.

  • Fn(T) -> K, a function which either has no captures or a closure that immutably borrows its captures.
  • FnMut(T) -> K, a closure which is either Fn(T) -> K or mutably borrows its captures.
  • FnOnce(T) -> K, a closure which destroys its captures without replacing them, or a closure which implements FnMut(T) -> K.

Would it be possible to have something similar for Mojo so that we can make more explicit APIs? Without this, I’m concerned that we will end up in a place where everything that takes a capture will need to write a fairly extensive amount of documentation about what kinds of closures are allowed, with the same kind of consequences that always brings.

Also, is there some way we can name the struct backing the closure separately from the function signature that a closure implements and the overall closure?

Treat def(T) → R like a trait. So I might write def foo[T:def(Int) → Int](impl:T) to express that foo is specialized on a closure type that conforms to trait that requires a call function that accepts an int and returns an int.

Treat def(T) thin → T as a type with a symbol as its sole state. I might write def foo[value: def(Int) thin → Int]() to express that foo is specialized on a function pointer or def foo(value: def(Int) thin → Int) to express that foo takes a runtime argument of function pointer type.

  1. the closure has an origin and an origin for each value it captured by reference so the lifetimes of the referenced values will persist with the closure. It does not require origin naming but perhaps there is a specific use case you can provide that will illustrate why implicit origin capture is not sufficient to accomplish the goal
  2. A closure is always callable multiple times. We would like to support a call once closure but we do not have that yet
  3. There is no compiler checked notion of state that is safe to share across threads. The best option is to use the copy/move capture convention or register_passable convention to create self contained closures, but that alone is not sufficient as it depends on the type copied into the closure.
  4. thin effect marks a function type as a function pointer

To mutably capture a value, use the mut capture convention. There is no way to express mutability on non-referenced captures. If a closure owns a value it is allowed to mutate it. We can consider changing this. We do not have an analog to FnOnce either but it is something we want to add.

I’m not sure I understand your final question. The closure struct type can be accessed and it has metadata like name but this metadata is currently immutable. Is there an example you can provide to illustrate the utility of this feature?

All sounds good.

I’m not sure I understand your final question. The closure struct type can be accessed and it has metadata like name but this metadata is currently immutable. Is there an example you can provide to illustrate the utility of this feature?

It being immutable is fine. The primary use-case would be creating arenas of closures with a shared function pointer and set of capture type, but different captures. It’s also helpful if you want to serialize a closure, since a given closure type will always have the same function pointer, which means that if you can access the backing struct you can serialize closures to disk.

I’ve managed to make the compiler crash when the captured type is defined in a *.mojopkg file and reported the bug here: [BUG] Compiler crash: unified closure move-capture of value from `.mojopkg` type · Issue #6478 · modular/modular · GitHub

Under what circumstances can closures be inlined by the compiler? Is the following correct?

  • unified closure: yes
  • thin closure passed as parameter: yes
  • thin closures passed as values: no

Is it important to put @always_inline on closures or will this happen anyway?

thank you for reporting! I have a fix that will land with the next release

inlining is not a guaranteed optimization unless @always_inline is used on the closure/function definition site and the callsite is a direct call. As a result, function pointers passed as runtime values will never be inlined

Thanks! “Direct call” - does this mean that even with @always_inline there is no guarantee for inlining even if a closure is passed as a parameter? (My interest is in whether vectorize and friends are guaranteed to yield the same IR code / optimizations as equivalent for loops.)

Would it make sense to have some sort of Callable trait in the former case? Having the thin keyword be a toggle between “function-like trait” and “function pointer” seems a bit unintuitive.

This would need to be a parametrized trait, since it must express the call signature. I find the current syntax with def()->Int quite convenient. The role as trait is also clear from colon. (Not saying that there can’t be a clearer syntax, though …)

If a closure is passed as a parameter and marked as always inline it is guaranteed to be inlined

Is there a way to pass a unified closure as an argument to a device function?

From my tests, it looks like unified closures do not conform to DevicePassable, so they can’t be passed to DeviceContext.enqueue_function.

This seems to block using closures to create performance portability abstractions in Mojo. The use case is to provide helpers for library users to iterative over our data structures on device, analogous to CUDA’s extended lambdas. Is there another / a better way to accomplish this?