Closures (Anonymous Functions) in Rust
About This Module
This module introduces Rust closures - anonymous functions that can capture variables from their environment. Closures are powerful tools for functional programming patterns, lazy evaluation, and creating flexible APIs. Unlike regular functions, closures can capture variables from their surrounding scope, making them ideal for customizing behavior and implementing higher-order functions.
Prework
Prework Reading
Read the following sections from "The Rust Programming Language" book:
Pre-lecture Reflections
Before class, consider these questions:
- How do closures differ from regular functions in terms of variable capture?
- What are the advantages of lazy evaluation using closures over eager evaluation?
- How does Rust's type inference work with closure parameters and return types?
- When would you choose a closure over a function pointer for API design?
- How do closures enable functional programming patterns in systems programming?
Learning Objectives
By the end of this module, you should be able to:
- Define and use closures with various syntactic forms
- Understand how closures capture variables from their environment
- Implement lazy evaluation patterns using closures
- Use closures with Option and Result methods like unwrap_or_else
- Apply closures for HashMap entry manipulation and other standard library methods
- Choose between closures and function pointers based on use case
Closures (Anonymous Functions)
- Closures are anonymous functions you can:
- save in a variable, or
- pass as arguments to other functions
In Python they are called lambda functions:
>>> x = lambda a, b: a * b
>>> print(x(5,6))
30
In Rust syntax (with implicit or explicit type specification):
|a, b| a * b
|a: i32, b: i32| -> i32 {a * b}
Basic Closure Syntax
- types are inferred
#![allow(unused)] fn main() { // Example 1: Basic closure syntax let add = |x, y| x + y; println!("Basic closure: 5 + 3 = {}", add(5, 3)); }
Can't change types
- Once inferred, the type cannot change.
#![allow(unused)] fn main() { let example_closure = |x| x; let s = example_closure(String::from("hello")); let n = example_closure(5); }
Basic Closure Syntax with Explicit Types
- Type annotations in closures are optional unlike in functions.
- Required in functions because those are interfaces exposed to users.
For comparison:
fn add_one_v1 (x: u32) -> u32 { x + 1 } // function
let add_one_v2 = |x: u32| -> u32 { x + 1 }; // closures...
let add_one_v3 = |x| { x + 1 }; // ... remove types
let add_one_v4 = |x| x + 1 ; // ... remove brackets
Another example:
#![allow(unused)] fn main() { let add = |x: i32, y: i32| -> i32 {x + y}; println!("Basic closure: 5 + 3 = {}", add(5, 3)); }
Closure Capturing a Variable from the Environment
Note how multiplier is used from the environment.
#![allow(unused)] fn main() { let multiplier = 2; let multiply = |x| x * multiplier; println!("Closure with captured variable: 4 * {} = {}", multiplier, multiply(4)); }
Closure with Multiple Statements
#![allow(unused)] fn main() { let process = |x: i32| { let doubled = x * 2; doubled + 1 }; println!("Multi-statement closure: process(3) = {}", process(3)); }
Digression
- You can assign regular functions to variables as well
#![allow(unused)] fn main() { fn median2(arr: &mut [i32]) -> i32 { arr.sort(); println!("{}", arr[2]); arr[2] } let f = median2; f(&mut [1,4,5,6,4]); }
- but you can't capture variables from the environment.
Lazy Evaluation
Closures enable lazy evaluation: delaying computation until the result is actually needed.
unwrap_or()andunwrap_or_else()are methods onOptionandResultunwrap_or_else()takes a closure and only executes on else case.
// Expensive computation function // What is this computing??? fn expensive_computation(n: i32) -> i32 { println!("Computing expensive result..."); if n <= 1 { 1 } else { expensive_computation(n-1) + expensive_computation(n-2) } } fn main() { let x = Some(5); // EAGER evaluation - always computed, even if not needed! println!("EAGER evaluation"); let result1 = x.unwrap_or(expensive_computation(5)); println!("Result 1: {}", result1); // LAZY evaluation - only computed if needed println!("\nLAZY evaluation"); let result2 = x.unwrap_or_else(|| expensive_computation(5)); // <-- note the closure! println!("Result 2: {}", result2); // When x is None, the closure is called println!("\nNone evaluation"); let y: Option<i32> = None; let result3 = y.unwrap_or_else(|| expensive_computation(5)); println!("Result 3: {}", result3); }
Key insight: unwrap_or_else takes a closure, delaying execution until needed.
Recap
- Closures are anonymous functions that can be saved in variables or passed as arguments
- Syntax:
|params| expressionor|params| { statements }- type annotations are optional - Type inference: Closure types are inferred from first use and cannot change afterward
- Environment capture: Unlike regular functions, closures can capture variables from their surrounding scope
- Flexibility: Closures are more flexible than functions, but functions can also be assigned to variables
- Closures enable lazy evaluation, functional programming patterns, and flexible API design
In-Class Activity
Exercise: Mastering Closures (10 minutes)
Setup: Work individually or in pairs. Open the Rust Playground or your local editor.
Paste your solutions in GradeScope.
Part 1: Basic Closure Practice (3 minutes)
Create closures for the following tasks. Try to use the most concise syntax possible:
- A closure that takes two integers and returns their maximum
- A closure that takes a string slice and returns its length
- A closure that captures a
tax_ratevariable from the environment and calculates the total price (price + tax)
fn main() { // TODO 1: Write a closure that returns the maximum of two integers let max = // YOUR CODE HERE println!("Max of 10 and 15: {}", max(10, 15)); // TODO 2: Write a closure that returns the length of a string slice let str_len = // YOUR CODE HERE println!("Length of 'hello': {}", str_len("hello")); // TODO 3: Write a closure that captures tax_rate and calculates total let tax_rate = 0.08; let calculate_total = // YOUR CODE HERE println!("Price $100 with {}% tax: ${:.2}", tax_rate * 100.0, calculate_total(100.0)); }
Part 2: Lazy vs Eager Evaluation (4 minutes)
Fix the following code by converting eager evaluation to lazy evaluation where appropriate:
fn expensive_database_query(id: i32) -> String { println!("Querying database for id {}...", id); // Simulate expensive operation format!("User_{}", id) } fn main() { // Scenario 1: We have a cached user let cached_user = Some("Alice".to_string()); // BUG: This always queries the database, even when we have a cached value! let user1 = cached_user.unwrap_or(expensive_database_query(42)); println!("User 1: {}", user1); // TODO: Fix the above to only query when needed // Scenario 2: No cached user let cached_user2: Option<String> = None; let user2 = // YOUR CODE HERE - use lazy evaluation println!("User 2: {}", user2); }
Part 3: Counter using a mutable closure
Create a closure that captures and modifies a variable and assigns
it to a variable called increment.
fn main() { // Create a counter using a mutable closure // This closure captures and modifies a variable // Your code here. println!("Count: {}", increment()); println!("Count: {}", increment()); println!("Count: {}", increment()); }
Bonus: Challenge - Functions That Accept Closures (3 minutes)
Write a function that takes a closure as a parameter and uses it:
// TODO: Complete this function that applies an operation to a number // only if the number is positive. Otherwise returns None. fn apply_if_positive<F>(value: i32, operation: F) -> Option<i32> where F: Fn(i32) -> i32 // F is a closure that takes i32 and returns i32 { // YOUR CODE HERE } fn main() { // Test with different closures let double = |x| x * 2; let square = |x| x * x; println!("Double 5: {:?}", apply_if_positive(5, double)); println!("Square 5: {:?}", apply_if_positive(5, square)); println!("Double -3: {:?}", apply_if_positive(-3, double)); }
Discussion Questions (during/after activity):
- When did you need explicit type annotations vs. relying on inference?
- In Part 2, what's the practical difference in performance between eager and lazy evaluation?
- Can you think of other scenarios where lazy evaluation with closures would be beneficial?
- What happens if you try to use a closure after the captured variable has been moved?
Solutions
Part 1 Solutions:
fn main() { // Solution 1: Maximum of two integers let max = |a, b| if a > b { a } else { b }; println!("Max of 10 and 15: {}", max(10, 15)); // Solution 2: Length of a string slice let str_len = |s: &str| s.len(); println!("Length of 'hello': {}", str_len("hello")); // Solution 3: Calculate total with captured tax_rate let tax_rate = 0.08; let calculate_total = |price| price + (price * tax_rate); println!("Price $100 with {}% tax: ${:.2}", tax_rate * 100.0, calculate_total(100.0)); }
Key Points:
- The
maxclosure uses an if expression to return the larger value - The
str_lenclosure needs a type annotation&strbecause Rust needs to know it's a string slice (not aString) - The
calculate_totalclosure capturestax_ratefrom the environment automatically
Part 2 Solutions:
fn expensive_database_query(id: i32) -> String { println!("Querying database for id {}...", id); format!("User_{}", id) } fn main() { // Scenario 1: We have a cached user let cached_user = Some("Alice".to_string()); // FIXED: Use unwrap_or_else with a closure for lazy evaluation let user1 = cached_user.unwrap_or_else(|| expensive_database_query(42)); println!("User 1: {}", user1); // Scenario 2: No cached user let cached_user2: Option<String> = None; let user2 = cached_user2.unwrap_or_else(|| expensive_database_query(99)); println!("User 2: {}", user2); }
Key Points:
- In Scenario 1, with
unwrap_or_else, the database query is NOT executed because we haveSome("Alice") - In Scenario 2, the closure IS executed because we have
None - Notice the closure syntax:
|| expensive_database_query(42)- no parameters needed - The lazy evaluation saves expensive computation when the value is already available
Part 3 Solutions:
fn main() { // Create a counter using a mutable closure // This closure captures and modifies a variable let mut count = 0; let mut increment = || { count += 1; count }; println!("Count: {}", increment()); println!("Count: {}", increment()); println!("Count: {}", increment()); }
- The closure mutates the captured variable each time it's called
Bonus: Challenge Solutions:
// Solution: Complete function that applies operation only to positive numbers fn apply_if_positive<F>(value: i32, operation: F) -> Option<i32> where F: Fn(i32) -> i32 { if value > 0 { Some(operation(value)) } else { None } } fn main() { // Test with different closures let double = |x| x * 2; let square = |x| x * x; println!("Double 5: {:?}", apply_if_positive(5, double)); // Some(10) println!("Square 5: {:?}", apply_if_positive(5, square)); // Some(25) println!("Double -3: {:?}", apply_if_positive(-3, double)); // None }
Key Points:
- The function uses a generic type parameter
Fwith aFn(i32) -> i32trait bound - This allows any closure (or function) that takes an
i32and returns ani32 - The mutable closure requires
muton bothcountandincrement - This demonstrates closure flexibility: they can be immutable (like
double) or mutable (likeincrement)