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:

  1. How do closures differ from regular functions in terms of variable capture?
  2. What are the advantages of lazy evaluation using closures over eager evaluation?
  3. How does Rust's type inference work with closure parameters and return types?
  4. When would you choose a closure over a function pointer for API design?
  5. 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() and unwrap_or_else() are methods on Option and Result
  • unwrap_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| expression or |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:

  1. A closure that takes two integers and returns their maximum
  2. A closure that takes a string slice and returns its length
  3. A closure that captures a tax_rate variable 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):

  1. When did you need explicit type annotations vs. relying on inference?
  2. In Part 2, what's the practical difference in performance between eager and lazy evaluation?
  3. Can you think of other scenarios where lazy evaluation with closures would be beneficial?
  4. 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 max closure uses an if expression to return the larger value
  • The str_len closure needs a type annotation &str because Rust needs to know it's a string slice (not a String)
  • The calculate_total closure captures tax_rate from 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 have Some("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 F with a Fn(i32) -> i32 trait bound
  • This allows any closure (or function) that takes an i32 and returns an i32
  • The mutable closure requires mut on both count and increment
  • This demonstrates closure flexibility: they can be immutable (like double) or mutable (like increment)