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:
#![allow(unused)] fn main() { fn add_one_v1 (x: u32) -> u32 { x + 1 } // function println!("{}", add_one_v1(5)); let add_one_v2 = |x: u32| -> u32 { x + 1 }; // closures... println!("{}", add_one_v2(6)); let add_one_v3 = |x| { x + 1 }; // ... remove types println!("{}", add_one_v3(4)); let add_one_v4 = |x| x + 1 ; // ... remove brackets println!("{}", add_one_v4(2)); }
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 // Hint: what method call returns the length? // Hint 2: you'll get an error: "type must be known..." 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()); // 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 below to only query when needed // Scenario 2: No cached user let cached_user2: Option<String> = Some("Bob".to_string()); 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 let mut count = 0; // YOUR CODE HERE - a closure that captures and modifies the count variable let mut increment = ... 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?