DaleSchool

Closures: Anonymous Functions

Intermediate15min

Learning Objectives

  • Understand and use closure syntax (|x| x + 1)
  • Explain how closures capture their environment
  • Distinguish between Fn / FnMut / FnOnce at a basic level

Working Code

Rust lets you create functions without names. These are called closures.

fn main() {
    let add_one = |x: i32| x + 1;
    println!("{}", add_one(5)); // 6

    // Multi-line closures work too
    let add_and_print = |a: i32, b: i32| -> i32 {
        let sum = a + b;
        println!("{a} + {b} = {sum}");
        sum
    };
    add_and_print(3, 4); // 3 + 4 = 7
}

|parameters| body — that's the basic closure form. Compare it to a regular function:

// Regular function
fn add_one_fn(x: i32) -> i32 {
    x + 1
}

// Closure
let add_one = |x: i32| x + 1;

Closures can also omit type annotations — the compiler infers them:

fn main() {
    let numbers = vec![1, 2, 3, 4, 5];
    let doubled: Vec<i32> = numbers.iter().map(|x| x * 2).collect();
    println!("{:?}", doubled); // [2, 4, 6, 8, 10]
}

Try It Yourself

  1. Create a closure |x| x * x to compute squares.
  2. Create a closure |a, b| if a > b { a } else { b } that returns the larger of two values.

"Why?" — Closures Remember Their Surroundings

The key difference between closures and regular functions is environment capture. Closures can "grab" variables from the scope where they're defined:

fn main() {
    let threshold = 10;

    // The closure captures threshold from the outer scope!
    let is_big = |x: i32| x > threshold;

    println!("{}", is_big(5));   // false
    println!("{}", is_big(15));  // true
}

Regular functions can't do this — you'd have to pass threshold explicitly as a parameter.

Three Capture Modes

How a closure captures variables determines its trait. This ties directly to the ownership rules!

| Capture mode | Trait | Analogy | Example | | ---------------- | -------- | ----------------------- | ------- | --- | ------------------ | | Immutable borrow | Fn | Just reading a book | | x | x + threshold | | Mutable borrow | FnMut | Writing notes in a book | | x | { count += 1; x } | | Takes ownership | FnOnce | Taking the book away | move | x | { drop(name); x } |

fn main() {
    // Fn — captures by immutable reference
    let name = String::from("Rust");
    let greet = || println!("Hello, {name}!");
    greet();
    greet(); // can call multiple times
    println!("{name}"); // name still usable

    // FnMut — captures by mutable reference
    let mut count = 0;
    let mut increment = || {
        count += 1;
        println!("count: {count}");
    };
    increment(); // count: 1
    increment(); // count: 2

    // FnOnce — takes ownership
    let message = String::from("bye");
    let consume = move || {
        println!("{message}");
        // message's ownership moved into the closure
    };
    consume();
    // println!("{message}"); // error! message was moved
}

Tip: In most cases, Rust decides the capture mode automatically. It tries Fn first, then FnMut, then FnOnce — picking the most restrictive option that works. You rarely need to specify it yourself!

The move Keyword

Adding move forces the closure to take ownership of every captured variable:

fn main() {
    let name = String::from("Alice");

    // Without move: borrows name
    let greet = || println!("Hello, {name}!");
    greet();
    println!("Still usable: {name}");

    // With move: ownership transfers to the closure
    let greet_move = move || println!("Hello, {name}!");
    greet_move();
    // println!("{name}"); // error! ownership moved
}

move is especially important when sending data to threads — a topic for later!

Passing Closures to Functions

You can pass closures as arguments using trait bounds:

fn apply_twice<F: Fn(i32) -> i32>(f: F, x: i32) -> i32 {
    f(f(x))
}

fn main() {
    let double = |x| x * 2;
    let result = apply_twice(double, 3);
    println!("{result}"); // 12 (3 -> 6 -> 12)

    // Regular functions work too
    fn add_ten(x: i32) -> i32 { x + 10 }
    let result2 = apply_twice(add_ten, 5);
    println!("{result2}"); // 25 (5 -> 15 -> 25)
}

The generics and trait bounds from Lesson 21 are at work here! F: Fn(i32) -> i32 means "a function (or closure) that takes an i32 and returns an i32."

Real-World Example: A Cacher

Combining closures with structs, you can build a caching (memoization) pattern:

struct Cacher<F: Fn(i32) -> i32> {
    calculation: F,
    value: Option<i32>,
}

impl<F: Fn(i32) -> i32> Cacher<F> {
    fn new(calculation: F) -> Cacher<F> {
        Cacher {
            calculation,
            value: None,
        }
    }

    fn get(&mut self, arg: i32) -> i32 {
        match self.value {
            Some(v) => v,
            None => {
                let v = (self.calculation)(arg);
                self.value = Some(v);
                v
            }
        }
    }
}

fn main() {
    let mut expensive = Cacher::new(|num| {
        println!("Computing...");
        num * 2
    });

    println!("Result: {}", expensive.get(5)); // "Computing..." printed
    println!("Result: {}", expensive.get(5)); // cached value used, no print
}

Run an expensive computation once and cache the result!

Exercise 1. Complete the function. Apply a closure to every element of a vector and return a new vector.

fn transform<F: Fn(i32) -> i32>(numbers: &[i32], f: F) -> Vec<i32> {
    // Complete this
    // Hint: create an empty Vec and push f(n) in a for loop
}

fn main() {
    let nums = vec![1, 2, 3, 4, 5];
    let squared = transform(&nums, |x| x * x);
    let tripled = transform(&nums, |x| x * 3);
    println!("Squared: {:?}", squared);  // [1, 4, 9, 16, 25]
    println!("Tripled: {:?}", tripled);  // [3, 6, 9, 12, 15]
}

Exercise 2. Complete the filter_by function. Return only elements that satisfy the predicate.

fn filter_by<F: Fn(&i32) -> bool>(numbers: &[i32], predicate: F) -> Vec<i32> {
    // Complete this
}

fn main() {
    let nums = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
    let evens = filter_by(&nums, |x| x % 2 == 0);
    let big = filter_by(&nums, |x| *x > 5);
    println!("Evens: {:?}", evens);  // [2, 4, 6, 8, 10]
    println!("> 5: {:?}", big);      // [6, 7, 8, 9, 10]
}

Q1. What is the biggest difference between closures and regular functions?

  • A) Closures can't return values
  • B) Closures can capture variables from their surrounding environment
  • C) Closures can't accept parameters
  • D) Regular functions are faster

Q2. What is the capture mode of this closure?

let mut total = 0;
let mut add = |x: i32| { total += x; };
add(5);
  • A) Fn — captures by immutable reference
  • B) FnMut — captures by mutable reference
  • C) FnOnce — takes ownership
  • D) No capture

Q3. What happens when you put move before a closure?

  • A) The closure runs faster
  • B) Captured variables' ownership transfers to the closure
  • C) The closure can't be called more than once
  • D) The closure can't return values