DEV Community

Subesh Yadav
Subesh Yadav

Posted on

Day 22 of #100DaysOfRust: Closures in Rust

Today, I took a deep dive into closures in Rust. These are powerful, flexible constructs that enable functional-style programming and can be used to make code more concise and expressive. Here's everything I explored today:


🧠 What Are Closures?

Closures are anonymous functions that can capture values from their environment. They can be saved into variables, passed to functions, or returned from them.

They differ from regular functions in key ways:

  • Can capture from the surrounding scope.
  • Type inference for parameters and return values.
  • Can implement Fn traits depending on how they use captured values.

📦 Capturing the Environment

Closures can capture variables from their scope:

#[derive(Debug, PartialEq, Copy, Clone)]
enum ShirtColor {
    Red,
    Blue,
}

struct Inventory {
    shirts: Vec<ShirtColor>,
}

impl Inventory {
    fn giveaway(&self, user_preference: Option<ShirtColor>) -> ShirtColor {
        user_preference.unwrap_or_else(|| self.most_stocked())
    }

    fn most_stocked(&self) -> ShirtColor {
        let (mut red, mut blue) = (0, 0);
        for color in &self.shirts {
            match color {
                ShirtColor::Red => red += 1,
                ShirtColor::Blue => blue += 1,
            }
        }
        if red > blue { ShirtColor::Red } else { ShirtColor::Blue }
    }
}

fn main() {
    let store = Inventory {
        shirts: vec![ShirtColor::Blue, ShirtColor::Red, ShirtColor::Blue],
    };

    let pref1 = Some(ShirtColor::Red);
    let pref2 = None;

    println!("User 1 gets {:?}", store.giveaway(pref1));
    println!("User 2 gets {:?}", store.giveaway(pref2));
}
Enter fullscreen mode Exit fullscreen mode

This uses a closure inside unwrap_or_else() that captures self.


✍ Closure Syntax and Type Inference

Closures are concise and types are often inferred:

fn add_fn(x: u32) -> u32 { x + 1 }
let add_closure1 = |x: u32| -> u32 { x + 1 };
let add_closure2 = |x| { x + 1 };
let add_closure3 = |x| x + 1;
Enter fullscreen mode Exit fullscreen mode

Rust infers parameter and return types based on context. However, closures can only use one inferred type per invocation.

let example = |x| x;
let a = example(String::from("hello"));
let b = example(5); // ❌ ERROR: mismatched types
Enter fullscreen mode Exit fullscreen mode

🔄 Borrowing, Mutability, and Moving

Closures can capture by:

  • Immutable borrow
  • Mutable borrow
  • Move (ownership)
let list = vec![1, 2, 3];
let print_list = || println!("{list:?}");

let mut list = vec![1, 2, 3];
let mut modify_list = || list.push(4);
modify_list();

let list = vec![1, 2, 3];
thread::spawn(move || println!("From thread: {list:?}"))
    .join()
    .unwrap();
Enter fullscreen mode Exit fullscreen mode

🧬 Fn, FnMut, FnOnce Traits

Closures implement one or more traits depending on how they use the environment:

  • FnOnce: Can be called once. Moves values.
  • FnMut: Mutates but doesn’t move.
  • Fn: Does not mutate or move. Callable multiple times.

Example: unwrap_or_else

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

f is called at most once — hence FnOnce is required.

Example: sort_by_key

list.sort_by_key(|r| r.width);
Enter fullscreen mode Exit fullscreen mode

Requires FnMut because the closure runs multiple times.


❌ Using FnOnce Where FnMut Is Needed

This code won’t compile:

let value = String::from("captured");
list.sort_by_key(|r| {
    sort_operations.push(value); // ❌ Moves value
    r.width
});
Enter fullscreen mode Exit fullscreen mode

Instead, capture mutable reference:

let mut counter = 0;
list.sort_by_key(|r| {
    counter += 1;
    r.width
});
Enter fullscreen mode Exit fullscreen mode

✅ Summary

  • Closures are powerful tools for functional-style programming.
  • Can capture variables by reference, mutable reference, or move.
  • Type inference makes them concise.
  • Trait bounds (Fn, FnMut, FnOnce) define closure capabilities.
  • Useful in APIs like unwrap_or_else, sort_by_key, threading, etc.

Tomorrow, I’ll explore iterators — another key functional concept in Rust.


🚀 Follow me for more on my #100DaysOfRust journey!

Top comments (0)