Understanding Rust Closures: A Deep Dive into Anonymous Functions

Introduction

After compilation, closures are transformed into standalone functions by the compiler. This is essentially a syntactic sugar provided by the language. Understanding closures is crucial for writing idiomatic Rust code.

What Are Closures?

A closure (also called an anonymous functon) is a block of code defined within a function body that has parameters (optional) and a body, but lacks a name. While this feature exists in many modern languages for convenience, it's particularly important in Rust due to the ownership and borrowing system.

Definition Syntax

Rust provides two primary ways to define closures:

Assigning to a Variable

let processor = |input| { 
    println!("Processing: {}", input) 
};

Direct Usage as Arguments

fn select_color(preference: Option<ShirtColor>) -> ShirtColor {
    preference.unwrap_or_else(|| default_color())
}

In this example, the argument to unwrap_or_else is a closure.

Comparison with Other Languages

Other languages like Java and JavaScript use more traditional syntax:

// Java
Isort sort = (a, b) -> a + b;

// JavaScript
const add = (a, b) => a + b;

Rust uses || instead of ->, which can be confusing for beginners. However, this syntax doesn't compromise safety or performance.

Variable Capture and Borrowing

Closures can capture variables from their surrounding scope in three ways:

Capturing by Reference (Immutable)

let base = 10;
let calculator = |increment| base + increment;
let result = calculator(5);
println!("Result: {}", result); // base is still accessible

Capturing by Mutable Reference

let mut counter = 0;
let mut increment = || {
    counter += 1;
    println!("Counter: {}", counter);
};

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

Capturing by Ownership (move)

let data = vec![1, 2, 3];
let consume = move || {
    println!("Consumed: {:?}", data);
    data
};
consume();

Important Rules for Mutable Capture

When using mutable capture, you cannot use the captured variable in the outer scope while the closure exists. The compiler will prevent this with errors like "immutable borrow occurs here" or "cannot borrow as mutable more then once at a time".

Fn Traits Explained

Rust colsures implement one of three traits based on how they capture variables:

  • FnOnce: Closures that can only be called once. This applies when the closure moves captured values out of its body.
  • FnMut: Closures that can be called multiple times and may modify captured values.
  • Fn: Closures that can be called multiple times without modifying captured values or moving them.

Trait Hierarchy

FnOnce → FnMut → Fn

All closures implement FnOnce (since all can be called at least once). A closure that implements FnMut also implements FnOnce, and a closure that implements Fn also implements both FnMut and FnOnce.

Using Traits as Bounds

impl<T> Option<T> {
    pub fn unwrap_or_else<F>(self, f: F) -> T
    where
        F: FnOnce() -> T
    {
        match self {
            Some(value) => value,
            None => f(),
        }
    }
}

This implementation requires a closure that implements FnOnce, meaning it can only be called once.

Complete Examples

Basic Closure Usage

fn main() {
    // Immutable capture
    let value = 10;
    let add_ten = |num| value + num;
    println!("Before: {}", value); // value is still accessible
    let result = add_ten(5);
    println!("{} + 5 = {}, value: {}", value, result, value);
    
    // Mutable capture
    let mut count = 20;
    println!("Before mutation: {}", count);
    
    let mut modify = || {
        count += 1;
    };
    
    modify();
    modify();
    println!("After mutation: {}", count);
    
    // Ownership transfer
    let mut collection = vec![1, 2, 3];
    println!("Before ownership transfer: {:?}", collection);
    
    let mut take_ownership = move || {
        collection.push(4);
        println!("Inside closure: {:?}", collection);
    };
    
    take_ownership();
    take_ownership();
}

Testing Fn Traits

#[derive(Debug)]
struct Item {
    name: String,
    quantity: u32,
}

impl Item {
    fn new(name: &str, qty: u32) -> Self {
        Item { 
            name: name.to_string(), 
            quantity: qty 
        }
    }
    
    fn display(&self) {
        println!("{} (qty: {})", self.name, self.quantity);
    }
}

fn test_once<T: FnOnce()>(f: T) {
    println!("Testing FnOnce - single execution");
    f();
}

fn test_mut<T: FnMut()>(mut f: T) {
    println!("Testing FnMut - multiple executions");
    f();
    f();
}

fn test_immutable<T: Fn()>(f: T) {
    println!("Testing Fn - multiple executions");
    f();
    f();
}

fn main() {
    // Test FnOnce
    let item1 = Item::new("Book", 5);
    let closure1 = || {
        item1.display();
    };
    test_once(closure1);
    item1.display(); // Still usable after FnOnce call
    
    // Test FnMut
    let mut item2 = Item::new("Laptop", 10);
    let mut closure2 = move || {
        item2.quantity += 1;
        println!("Modified item: {:?}", item2);
    };
    test_mut(closure2);
    
    // Test Fn
    let item3 = Item::new("Phone", 15);
    let closure3 = || {
        println!("Reading item: {:?}", item3);
    };
    test_immutable(closure3);
    println!("Item3 still accessible: {:?}", item3);
}

Summary

Closures in Rust provide powerful functionality but require understanding the ownership system. Key takeaways:

  1. Closures capture variables from their enclosing scope automatically
  2. Use mut when defining closures that need mutable capture
  3. Use the move keyword to transfer ownership of captured variables
  4. Mutable capture prevents using the variable in the outer scope while the closure exists
  5. Understanding Fn, FnMut, and FnOnce traits helps in writing generic code

Tags: rust closures anonymous-functions fn-trait fnmut

Posted on Wed, 13 May 2026 13:38:26 +0000 by Dilb