Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
64: Closures and the Fn Traits
I've seen a lot of developers hit a wall when they first try to pass closures into functions. Everything seems fine until you try to actually do something with the variables outside the closure, and suddenly the compiler starts screaming about trait bounds. Let's look at a piece of code that looks perfectly logical but won't compile.
fn process_data(data: Vec, action: F)
where
F: Fn(i32)
{
for val in data {
action(val);
}
}
fn main() {
let mut total_processed = 0;
let numbers = vec![10, 20, 30];
process_data(numbers, |x| {
total_processed += 1;
println!("Processing {}", x);
});
println!("Total: {}", total_processed);
}
The "cannot borrow as mutable" frustration
If you try to compile this, Rust will throw a fit. You'll see an error saying something like: cannot borrow `total_processed` as mutable, as it is a captured variable in an `Fn` closure.
Here is why this is happening: In the process_data function signature, I specified that F must implement the Fn trait. In Rust, Fn is the most restrictive trait regarding mutations. It implies that the closure can be called multiple times without mutating its environment. It only takes an immutable reference to the variables it captures.
But look at the closure we're passing: |x| { total_processed += 1; }. This closure is trying to mutate total_processed. Because it modifies its environment, Rust automatically classifies this closure as FnMut, not Fn. Since FnMut does not satisfy the Fn bound, the compiler stops you right there.
Matching the trait to the behavior
To fix this, we need to change the trait bound in our function signature to match what the closure is actually doing. We change Fn(i32) to FnMut(i32).
fn process_data(data: Vec, mut action: F)
where
F: FnMut(i32)
{
for val in data {
action(val);
}
}
Notice that I also added mut to the action parameter. Since FnMut requires a mutable reference to the closure itself to change its captured state, the variable holding the closure must be declared as mutable.
Deciding between Fn, FnMut, and FnOnce
You'll encounter these three traits constantly. I find it easiest to think about them in terms of how they handle the data they capture from the surrounding scope:
- Fn: The "Read-Only" closure. It captures values by immutable reference. You can call it as many times as you want, and it doesn't change anything.
- FnMut: The "Mutable" closure. It captures values by mutable reference. It can change its environment, and it can be called multiple times.
- FnOnce: The "Consuming" closure. It captures values by taking ownership. Because it moves the variables into the closure's own scope, it can only be called once. After that, the captured variables are gone.
A good rule of thumb: always start with the most restrictive trait you can get away with. If you don't need to mutate state, use Fn. If you're building a callback that updates a counter or a list, use FnMut. If you're passing a closure to a thread or a function that consumes the data, you're likely looking at FnOnce.
One last thing: if you see the move keyword before the |args| pipe, it doesn't necessarily change the Fn trait it implements, but it forces the closure to take ownership of the variables it uses rather than borrowing them. This is almost always required when returning a closure from a function or spawning a thread, because the compiler can't guarantee that the local variables will still exist when the closure is eventually called.
📋 Practical Task
Implementing a State-Tracking Discount Engine
You are building a checkout system. You need to write a function called apply_discounts that takes a list of product prices and a closure. This closure should calculate the discounted price and simultaneously track the total amount of money saved across all items.
Requirements:
- Create a variable
total_savingsin yourmainfunction. - Write the
apply_discountsfunction. It should accept aVec<f64>and a closure. - The closure must take a price (
f64) and return a discounted price (f64), while adding the difference tototal_savings. - Ensure you use the correct
Fntrait bound (Fn,FnMut, orFnOnce) so that the code compiles and correctly updates the savings tracker. - Print the final
total_savingsafterapply_discountshas finished executing.
There are no comments for now.