Closures

Back

Loading concept...

🎒 Rust Closures: Your Pocket-Sized Helper Functions

The Story of the Magic Backpack

Imagine you have a magic backpack. This backpack is special because:

  1. You can put instructions inside it
  2. It can grab things from your room and carry them along
  3. You can give it to someone else, and they can follow the instructions using what’s inside

That’s exactly what a closure is in Rust!

A closure is a tiny function you can:

  • Write quickly (short syntax)
  • Store in a variable
  • Pass around like a toy
  • It can “grab” things from its surroundings

1. Closure Syntax: Writing Your First Magic Note

Regular functions in Rust look like this:

fn add_one(x: i32) -> i32 {
    x + 1
}

But closures are shorter and sweeter:

let add_one = |x| x + 1;

The Parts of a Closure

let greet = |name| {
    println!("Hello, {}!", name);
};
Part What It Means
|name| The input (like a door for things to enter)
{ ... } The instructions inside
let greet = Give the closure a name

Even Simpler!

If your closure has just ONE expression, skip the curly braces:

let double = |x| x * 2;

Example: Using a Closure

let square = |n| n * n;
println!("{}", square(5));  // Prints: 25

2. Closure Type Inference: Rust Figures It Out!

Here’s something magical: you don’t always need to tell Rust the types!

Rust looks at how you USE the closure and figures out the types automatically.

let add = |a, b| a + b;

// Rust sees you're adding numbers
let result = add(3, 4);  // result = 7

But Here’s the Rule

Once Rust decides the types, they’re locked in:

let echo = |x| x;

let s = echo("hello");  // x is now &str
// let n = echo(5);     // ERROR! Can't use i32

Think of it like this: The first time you put something in a box, the box takes that shape forever.

When You WANT to Specify Types

let multiply: fn(i32, i32) -> i32 = |a, b| {
    a * b
};

Or inside the closure:

let divide = |a: f64, b: f64| -> f64 {
    a / b
};

3. Capturing Environment: The Backpack Grabs Things!

This is where closures become really special.

Closures can reach outside and grab variables from their surroundings:

let name = String::from("Alice");

let greet = || {
    println!("Hello, {}!", name);
};

greet();  // Prints: Hello, Alice!

The closure “grabbed” the name variable!

How Does Rust Grab Things?

Rust has three ways to grab things:

graph TD A["Closure Captures Variable"] --> B{How?} B --> C["📖 Borrow<br>Just look at it"] B --> D["✏️ Mutable Borrow<br>Look and change it"] B --> E["📦 Take Ownership<br>Take it completely"]

Example: Borrowing (just looking)

let message = String::from("Hi!");

let print_it = || {
    println!("{}", message);  // Just reading
};

print_it();
println!("{}", message);  // Still works!

Example: Mutable Borrow (changing)

let mut count = 0;

let mut increment = || {
    count += 1;  // Changing the value
};

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

4. The Fn Trait: “I Promise to Only Look!”

When a closure only reads from its environment (doesn’t change anything), it implements the Fn trait.

let name = String::from("Bob");

let say_hi = || {
    println!("Hi, {}!", name);
};

// This closure implements Fn
// It only READS name, never changes it

Why Does This Matter?

You can call Fn closures as many times as you want:

fn call_twice<F: Fn()>(f: F) {
    f();
    f();
}

let greeting = || println!("Hello!");
call_twice(greeting);  // Works perfectly!

Think of Fn like a library book: You can read it over and over, but you can’t write in it.


5. The FnMut Trait: “I Might Change Things!”

When a closure modifies something it captured, it implements FnMut.

let mut total = 0;

let mut add_to_total = |x| {
    total += x;  // Changing total!
};

add_to_total(5);
add_to_total(10);
println!("{}", total);  // Prints: 15

The Rules

  • The closure must be declared mut
  • You can still call it multiple times
  • But only ONE mutable reference at a time!
fn call_with_one<F: FnMut(i32)>(mut f: F) {
    f(1);
}

let mut sum = 0;
call_with_one(|x| sum += x);

Think of FnMut like a notebook: You can write in it, erase, and write again!


6. The FnOnce Trait: “One-Time Magic!”

Some closures can only be called once because they consume what they captured.

let name = String::from("Charlie");

let consume_name = || {
    drop(name);  // name is gone forever!
};

consume_name();
// consume_name();  // ERROR! Can't call again

When Does FnOnce Happen?

When your closure moves something out:

let data = vec![1, 2, 3];

let consume_data = || {
    let _moved = data;  // data moves INTO closure
    println!("Data consumed!");
};

consume_data();  // Works once
// consume_data();  // ERROR!

The Trait Hierarchy

graph TD A["FnOnce"] --> B["FnMut"] B --> C["Fn"] style A fill:#ffcccc style B fill:#ffffcc style C fill:#ccffcc

Every Fn is also FnMut and FnOnce!

Trait Can Call Modifies? Consumes?
Fn Many times No No
FnMut Many times Yes No
FnOnce Once Maybe Yes

7. Move Closures: “Take Everything With Me!”

Sometimes you WANT the closure to own its captured values, even if it doesn’t need to.

Use the move keyword:

let name = String::from("Dana");

let greet = move || {
    println!("Hello, {}!", name);
};

greet();
// println!("{}", name);  // ERROR! name was moved

Why Use Move?

Perfect for threads! When you spawn a new thread, it needs to OWN its data:

use std::thread;

let message = String::from("Hi from thread!");

let handle = thread::spawn(move || {
    println!("{}", message);
});

handle.join().unwrap();

Without move, the thread might outlive the data!

Move + Copy Types

For types that implement Copy (like numbers), move makes a copy:

let x = 42;

let print_x = move || {
    println!("{}", x);
};

print_x();
println!("{}", x);  // Still works! x was copied

🎯 Putting It All Together

fn main() {
    // 1. Simple closure syntax
    let add = |a, b| a + b;

    // 2. Type inference works!
    let result = add(2, 3);  // Rust knows: i32

    // 3. Capturing environment
    let multiplier = 10;
    let multiply = |x| x * multiplier;

    // 4. Fn - just reading
    println!("{}", multiply(5));  // 50

    // 5. FnMut - changing things
    let mut count = 0;
    let mut counter = || { count += 1; count };
    println!("{}", counter());  // 1
    println!("{}", counter());  // 2

    // 6. FnOnce - consuming
    let data = vec![1, 2, 3];
    let consume = || drop(data);
    consume();

    // 7. Move closure
    let name = String::from("Rust");
    let greet = move || println!("Hello, {}!", name);
    greet();
}

🚀 Quick Reference

Concept What It Does Example
Basic Syntax |params| body |x| x + 1
Type Inference Rust figures out types |a, b| a + b
Environment Capture Grabs nearby variables || println!("{}", name)
Fn Read-only access Can call many times
FnMut Can modify captured vars Needs mut
FnOnce Consumes captured vars Call once only
move Takes ownership move || ...

🌟 Remember This!

Closures are like magic backpacks:

  • Quick to make (short syntax)
  • Can grab things from nearby (capture)
  • Can be passed around (first-class functions)
  • Rust decides how they grab things (Fn, FnMut, FnOnce)
  • Use move when you want them to OWN everything inside

You’ve got this! Closures might seem tricky at first, but they’re just tiny, portable functions with superpowers. 🎒✨

Loading story...

Story - Premium Content

Please sign in to view this story and start learning.

Upgrade to Premium to unlock full access to all stories.

Stay Tuned!

Story is coming soon.

Story Preview

Story - Premium Content

Please sign in to view this concept and start learning.

Upgrade to Premium to unlock full access to all content.