Closure: Lambda Functions of Rust

SiddharthSiddharth
11 min read

A closure is an anonymous function that can capture variables from its environment. At a high level, Rust closures allow you to write small, concise functions inline, without having to formally define a new fn with a name. They’re particularly handy when you want to pass a piece of functionality around, such as to iterators or threads.

Capturing variables from its enclosing scope means you don’t have to manually pass those variables as parameters; the compiler implicitly makes them accessible inside the closure.

let x = 10;
// `double_x` is a closure that captures `x` from the environment
let double_x = || x * 2;

Closure Syntax

Parameters in pipes (|)

let closure = |param1, param2| {
    // body
};

Optional curly braces if the body is a single expression:

let closure = |x| x + 1;

Often, you don't have to specify types for the parameters, but if you'd like to be clear, you certainly can.

let add = |a, b| a + b;
let add = |a: i32, b: i32| -> i32 { a + b };

Capturing Variables

Unlike standard functions, closures have a special talent for capturing variables from the scope where they are created. Rust closures can capture:

  1. By reference (&T)

  2. By mutable reference (&mut T)

  3. By value / move (T)

How Rust Decides the Capture Method

Rust uses capture inference to figure out the best way to capture each variable. It decides this based on how the closure uses the variable:

  1. If the closure just needs to read the variable, it will capture it by immutable reference.

  2. If the closure needs to change the variable, it will capture it by mutable reference.

  3. If the closure needs to take full ownership of the variable (like when it moves it or keeps it somewhere that lasts longer than the current scope), it will capture it by value (ownership).

fn main() {
    let mut s = String::from("hello");
    let print_s = || println!("{}", s); // 1) Capture by reference

    print_s(); // Prints "hello"
    // This closure modifies `s`, so it captures `s` by &mut.
    let mut append_exclamation = || {    // 2) Capture by mutable reference
        s.push('!');
    };

    append_exclamation();
    println!("{}", s); // "hello!"

    // 3) Capture by value (move) Using move explicitly forces ownership capture.
    let take_ownership = move || {
        println!("Moved: {}", s);
    }; 
    take_ownership();
}

Lets try to print s after running take_ownerhship closure

println!("{}", s); 

error[E0382]: borrow of moved value: `s`
  --> src/main.rs:20:20
   |
2  |     let mut s = String::from("hello");
   |         ----- move occurs because `s` has type `String`, which does not implement the `Copy` trait
...
15 |     let take_ownership = move || {
   |                          ------- value moved into closure here
16 |         // We won't be able to use `s` after this closure is created if it truly takes it by value.
17 |         println!("Moved: {}", s);
   |                               - variable moved due to use in closure
...
20 |     println!("{}", s); // Error: `s` was moved
   |                    ^ value borrowed here after move

The move Keyword

By default, the compiler uses the least restrictive capture mode it can. But if you want to change that, you can use move. This makes sure all captures in that closure take ownership, using move semantics.

Closure Traits: Fn, FnMut, and FnOnce

Closures can implement one or more of these function traits, based on how they capture their environment:

Fn: A closure that only needs an immutable reference to its environment.

FnMut: A closure that only needs a mutable reference to its environment.

FnOnce: A closure that can be called at least once. It may consume (move) variables it captures, so it can only be called once if it moves.

Conceptually, you can think of them as:

  • FnOnce: uses self

  • FnMut: uses &mut self

  • Fn: uses &self

Let do a bit of deep dive on these traits.

Fn trait

As described in Rust documentation, Fn is the trait for function-like types that can be called repeatedly without needing to change anything in their environment. Put simply, if a piece of code can be called multiple times and doesn’t need to mutate (change) any variables it has captured, it usually implements Fn. This trait (Fn) should not be confused with function pointers (fn).

Fn vs. fn (Function Pointers)

  • fn (lowercase) refers to a function pointer type. For example, fn(i32) -> i32 is a pointer to a function that takes an i32 and returns an i32.

  • Fn (uppercase) is a trait, which can be implemented by any callable type, such as:

    • Closures (anonymous functions that capture variables),

    • Function pointers (fn(...)) themselves,

    • or even your own custom types that implement the Fn trait.

To illustrate, consider a function that takes an integer as input and returns its square. In Rust, a function pointer has the type fn(…) -> …. For example, here is a simple function that squares an integer:

fn square(x: i32) -> i32 {
    x * x
}

We can create a function that accepts a function pointer of this type. apply_square takes a parameter f: fn(i32) -> i32. This means the caller needs to pass a pointer to a function with that exact signature (no captures, no closures). We can pass square (a regular function) to it, and it works perfectly.

// Accepts a function pointer "fn(i32) -> i32".
fn apply_square(f: fn(i32) -> i32, value: i32) -> i32 {
    f(value)
}

fn main() {
    let result = apply_square(square, 4);
    println!("Result using function pointer: {}", result); 
}

If we try to pass a closure instead of a function pointer, you may expect a compilation error or warning.

let closure = |x: i32| x * x;
let result = apply_square(closure, 4);

Wait, it worked perfectly as well and we know Closures in Rust are not the same type as fn(…) -> ….

In this case, a closure does not capture anything from its environment (i.e., it doesn’t use any local variables from the enclosing scope) is effectively equivalent to a plain function. Rust can coerce (convert) that closure into a function pointer. This is a special-case automatic conversion allowed by the compiler.

Lets capture some variable from its environment.

let x = 4;
let closure = || x * x;
let result = apply_square(closure, x);

error[E0308]: mismatched types
  --> src/main.rs:24:40
   |
22 |     let closure = || x * x;
   |                   -- the found closure
23 |     
24 |     let result = apply_square(closure, x);
   |                  --------------------- ^^^^^^^ expected fn pointer, found closure
   |                  |
   |                  arguments to this function are incorrect
   |
   = note: expected fn pointer `fn(i32) -> i32`
                 found closure `{closure@src/main.rs:22:19: 22:21}`
note: closures can only be coerced to `fn` types if they do not capture any variables
  --> src/main.rs:22:22
   |
22 |     let closure = || x * x;
   |                      ^ `x` captured here
note: function defined here
  --> src/main.rs:7:4

To allow a capturing closure to pass, you need to use a trait bound like Fn

fn apply_square_fn<F>(f: F) -> i32
where
    F: Fn() -> i32,
{
    f()
}
let x = 4;
let closure = || x * x;
let result = apply_square_fn(closure);

The main thing to remember is that fn(i32) -> i32 is a function pointer type. It's only for real functions or some non-capturing closures that can be turned into a fn pointer. If you set your parameter as a function pointer type, you can't pass a capturing closure. It does not capture any state.

FnMut Trait

A closure that only needs a mutable reference to its environment. It can be called repeatedly, but it might mutate captured variables. FnMut is implemented automatically by closures which take mutable references to captured variables, as well as all types that implement Fn. Any FnMut is automatically also an FnOnce, because if you can call something multiple times, you can at least call it once.

In the below example, the closure changes count. Therefore, it needs a mutable borrow of count. That means it implements at least FnMut.

let mut count = 0;
// This closure captures `count` by mutable reference,
let mut increment = || { count += 1  }; // compiler infers `increment: FnMut() -> ()`.

increment(); //"count: 1"
increment(); //"count: 2"
println!("Final count: {}", count); // 2

Using FnMut as a Trait Bound

Often, you’ll write generic functions that accept any closure (or function) with certain capabilities. If your function needs to call a closure multiple times and allow it to mutate captured state, you should use an FnMut bound.

// It requires that `f` can be called multiple times AND can mutate state.
fn call_multiple_times<F>(mut f: F, times: usize)
where
    F: FnMut(),
{
    for _ in 0..times {
        f();
    }
}

fn main() {
    let mut counter = 0;

    let mut increment = || {
        counter += 1;
        println!("counter: {}", counter);
    };
    // The closure implements `FnMut`, so it's valid here.
    call_multiple_times(increment, 3);
    // Output:
    // counter: 1
    // counter: 2
    // counter: 3

    println!("Final counter: {}", counter); // 3
}

FnMut is the trait for callables that may mutate captured state and can be called multiple times.

FnOnce Trait

A closure that can be called at least once. It may consume (move) variables it captures, so it can only be called once if it moves something out of the environment. Every closure implements FnOnce because, at the bare minimum, you can call it once.

In simple terms, if a closure takes ownership of data from its environment, it's at least FnOnce. It could also be FnMut or Fn, based on whether it changes or just reads that data.

Every closure implements at least one of these traits, and sometimes more, depending on how it handles variables.

Essentially:

FnOnce <= FnMut <= Fn

(FnOnce is the “weakest” requirement; Fn is the “strongest”)

Manually Implementing Fn

In most real-world Rust code, you do not manually implement Fn, because closures and function pointers automatically implement it. However, if you want a custom struct to behave like a function, you need to implement Fn, FnMut, and FnOnce by hand.

Important: This is only possible with nightly Rust, using special features. You can manually implement Fn, FnMut, or FnOnce by enabling unboxed_closures and fn_traits, using the extern "rust-call" syntax.

Below is a nightly-only example. It shows a struct MultiplyBy that holds a multiplier. We implement all three traits so that it can be called like a function.

#![feature(fn_traits)]
#![feature(unboxed_closures)]
use std::ops::{Fn, FnMut, FnOnce};

struct MultiplyBy(i32); // A struct that holds a multiplier.

//1) Implement FnOnce for MultiplyBy. Notice the signature uses `call_once(self, (arg,): (i32,))`.
impl FnOnce<(i32,)> for MultiplyBy {
    type Output = i32;

    extern "rust-call" fn call_once(self, (arg,): (i32,)) -> Self::Output {
        self.0 * arg
    }
}
//2)Implement FnMut for MultiplyBy. This time the method is `call_mut(&mut self, (arg,): (i32,))`.
impl FnMut<(i32,)> for MultiplyBy {
    extern "rust-call" fn call_mut(&mut self, (arg,): (i32,)) -> Self::Output {
        self.0 * arg
    }
}
// 3) Finally, implement Fn for MultiplyB. Now we use `&self`.
impl Fn<(i32,)> for MultiplyBy {
    extern "rust-call" fn call(&self, (arg,): (i32,)) -> Self::Output {
        self.0 * arg
    }
}
fn main() {
    let multiply_by_10 = MultiplyBy(10);
    // Because we've implemented Fn, we can call our struct like a function:
    println!("{}", multiply_by_10(5));  // 50
    println!("{}", multiply_by_10(12)); // 120
}

Why implement all three?
In Rust, Fn is a supertrait of FnMut, which is a supertrait of FnOnce. So you need to implement them in ascending order if you want a type to fully behave like a closure that can be called multiple times without mutation.

  • FnOnce says “I can be called once.”

  • FnMut says “I can be called multiple times, mutably.”

  • Fn says “I can be called multiple times, immutably.”

Under the hood: Anonymous Struct Holding Captures

When you write a closure, Rust will create a hidden struct (let’s call it the closure object) that:

  1. Has fields for each variable it captures.

  2. Implements one or more of the FnOnce, FnMut, and/or Fn traits.

  3. Implements a “call” method (i.e. call, call_mut, or call_once) that executes the closure body, referencing or consuming those fields as needed.

Compiler generates this struct with name something like closure@ID.

fn main() {
    let mut count = 0;
    let mut increment = || {
        count += 1;
        println!("count: {}", count);
    };

    increment();
    increment();
}

Conceptually, Rust might generate a struct like:

struct Closure_CountMut<'a> {
    count_ref: &'a mut i32,
}

// This closure implements FnMut, because it mutates its captures.
impl<'a> FnMut(()) for Closure_CountMut<'a> {
    extern "rust-call" fn call_mut(&mut self, _args: ()) {
        *self.count_ref += 1;
        println!("count: {}", *self.count_ref);
    }
}

fn main() {
    let mut count = 0;
    // The closure is constructed
    let mut increment = Closure_CountMut {
        count_ref: &mut count,
    };

    // When we call it, it calls `call_mut`
    increment.call_mut(());
    increment.call_mut(());
}

You can say that closure is syntactic sugar for an anonymous struct that holds the captured variables.

This is all done automatically by the compiler, but understanding this model helps clarify why closures have their particular capturing rules and trait bounds.

Summary

In summary, I would say if you need a small piece of logic that depends on local variables, but you don’t want to define a whole new named function. That’s exactly where closures come in. A closure is essentially an anonymous function that automatically captures variables from the scope in which it’s defined. Sometimes it merely borrows these variables (immutably or mutably), and other times it takes full ownership—Rust decides which strategy to use based on how you interact with those variables.

Once created, the closure acts like a callable object, implementing one or more of the function traits—Fn, FnMut, or FnOnce—depending on whether it only reads, mutates, or consumes its captured data. In cases where a closure doesn’t capture anything at all, the compiler can even coerce it into a plain function pointer.

Under the hood, Rust constructs an anonymous struct to hold whatever variables the closure captures, then it implements the relevant trait methods for that struct. Most of the time, these traits are derived automatically by the compiler.

Ultimately, this allows you to write concise, powerful inline functions that interact seamlessly with their surrounding environment—without ever losing track of Rust’s safety and performance guarantees. When it comes to performance, the use of closures in Rust is truly remarkable. Closures are designed to be efficient and fast, allowing you to write code that executes quickly without sacrificing safety or functionality. The Rust compiler is highly optimized for closures.

3
Subscribe to my newsletter

Read articles from Siddharth directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Siddharth
Siddharth

I am Quantitative Developer, Rust enthusiast and passionate about Algo Trading.