Week 13: Idiomatic Rust
Session 1: Iterators and Closures
.map(), .filter(), .fold(), .collect())Fn, FnMut, FnOnceSession 2: Smart Pointers and Error Handling
Box<T>, Rc<T>, RefCell<T>Result<T, E>? operator and custom error typesReal-world impact:
Industry examples:
Traditional loop-based approach:
Problems:
result)Functional approach with iterator chains:
Benefits:
Transformation:
// map: transform each element
vec![1, 2, 3].iter().map(|x| x * 2) // [2, 4, 6]
// filter: keep elements matching condition
vec![1, 2, 3, 4].iter().filter(|&&x| x % 2 == 0) // [2, 4]
// filter_map: transform and filter in one step
vec!["1", "two", "3"]
.iter()
.filter_map(|s| s.parse::<i32>().ok()) // [1, 3]Aggregation:
Count words, find average length, get longest word:
fn analyze_text(text: &str) -> (usize, f64, String) {
let words: Vec<&str> = text
.split_whitespace()
.collect();
if words.is_empty() {
return (0, 0.0, String::new());
}
let word_count = words.len();
// Average word length using iterator methods
let total_length: usize = words
.iter()
.map(|word| word.len())
.sum();
let average_length = total_length as f64 / word_count as f64;
// Find longest word
let longest_word = words
.iter()
.max_by_key(|word| word.len())
.unwrap_or(&"")
.to_string();
(word_count, average_length, longest_word)
}Challenge: Filter and transform numbers
// Given this vector:
let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Use iterator methods to:
// 1. Keep only even numbers
// 2. Square each number
// 3. Sum the results
// Expected: 220 (2² + 4² + 6² + 8² + 10² = 4 + 16 + 36 + 64 + 100)
let result: i32 = numbers
.iter()
.filter(|&&n| n % 2 == 0)
.map(|&n| n * n)
.sum();Try on Rust Playground
Closures are functions that can capture their environment:
Closures can capture variables:
Rust has three closure traits based on how they capture variables:
Fn - Borrows immutably:
FnMut - Borrows mutably:
FnOnce - Takes ownership:
Create a closure that tracks state:
fn make_counter() -> impl FnMut() -> i32 {
let mut count = 0;
move || {
count += 1;
count
}
}
// Use it:
let mut counter = make_counter();
println!("{}", counter()); // 1
println!("{}", counter()); // 2
println!("{}", counter()); // 3
// Each counter is independent!
let mut counter2 = make_counter();
println!("{}", counter2()); // 1Key concepts:
move keyword transfers ownership into closureFnMut because closure mutates captured countProcessing collections with closures:
#[derive(Debug)]
struct Student {
name: String,
grade: f64,
}
let students = vec![
Student { name: "Alice".to_string(), grade: 92.0 },
Student { name: "Bob".to_string(), grade: 78.0 },
Student { name: "Charlie".to_string(), grade: 85.0 },
];
// Find honor roll students (grade >= 80)
let honor_roll: Vec<&str> = students
.iter()
.filter(|student| student.grade >= 80.0)
.map(|student| student.name.as_str())
.collect();
// honor_roll = ["Alice", "Charlie"]Career relevance: This pattern appears in every Rust codebase!
What we covered:
✅ Iterator methods (.map(), .filter(), .sum(), .collect()) ✅ Building data processing pipelines ✅ Closures and capture semantics ✅ Closure traits (Fn, FnMut, FnOnce) ✅ Real-world patterns combining iterators + closures
Next session: Smart pointers and error handling
Rust’s ownership rules are strict:
Solution: Smart pointers!
Three essential smart pointers:
| Type | Purpose | Use When |
|---|---|---|
Box<T> |
Heap allocation | Recursive types, large data |
Rc<T> |
Reference counting | Multiple owners (single-threaded) |
RefCell<T> |
Interior mutability | Runtime borrowing checks |
Key insight: Smart pointers provide additional capabilities while maintaining Rust’s safety guarantees.
Common combinations:
Box<T> alone: Recursive data structuresRc<T> alone: Shared read-only dataRc<RefCell<T>>: Shared mutable data (single-threaded)Box<T> stores data on the heap with known size:
// Simple heap allocation
let boxed_int = Box::new(5);
println!("{}", *boxed_int); // 5 (dereference with *)
// Main use case: recursive types
#[derive(Debug)]
enum BinaryTree<T> {
Empty,
Node {
value: T,
left: Box<BinaryTree<T>>, // Box enables recursion!
right: Box<BinaryTree<T>>,
},
}
// Create a tree
let tree = BinaryTree::Node {
value: 5,
left: Box::new(BinaryTree::Empty),
right: Box::new(BinaryTree::Empty),
};Why it works: Box<T> has fixed size (just a pointer), even though T might be recursive.
Building a binary tree with Box:
impl<T> BinaryTree<T> {
/// Creates a new empty tree
fn new() -> Self {
BinaryTree::Empty
}
/// Creates a leaf node (no children)
fn leaf(value: T) -> Self {
BinaryTree::Node {
value,
left: Box::new(BinaryTree::Empty),
right: Box::new(BinaryTree::Empty),
}
}
/// Creates a node with children
fn node(value: T, left: BinaryTree<T>, right: BinaryTree<T>) -> Self {
BinaryTree::Node {
value,
left: Box::new(left),
right: Box::new(right),
}
}
}
// Usage:
let tree = BinaryTree::node(
10,
BinaryTree::leaf(5),
BinaryTree::leaf(15),
);Rc<T> enables multiple owners of the same data:
use std::rc::Rc;
#[derive(Debug)]
struct SharedData {
value: i32,
}
let data = Rc::new(SharedData { value: 42 });
println!("Count: {}", Rc::strong_count(&data)); // 1
// Create additional owners by cloning the Rc
let owner1 = Rc::clone(&data); // Increments count
let owner2 = Rc::clone(&data); // Increments count
println!("Count: {}", Rc::strong_count(&data)); // 3
println!("Value: {}", owner1.value); // 42
// When all Rc's drop, data is freed
drop(owner1);
println!("Count: {}", Rc::strong_count(&data)); // 2Important: Rc::clone is cheap - it only increments a counter, doesn’t copy data!
RefCell<T> allows mutation through shared references (runtime borrow checking):
Key difference: Borrows checked at runtime instead of compile-time.
Panic if rules violated:
Pattern: Shared mutable data (single-threaded)
use std::rc::Rc;
use std::cell::RefCell;
#[derive(Debug)]
struct Counter {
value: i32,
}
let counter = Rc::new(RefCell::new(Counter { value: 0 }));
// Create multiple owners
let counter_ref1 = Rc::clone(&counter);
let counter_ref2 = Rc::clone(&counter);
// All can mutate through RefCell
counter_ref1.borrow_mut().value += 1;
counter_ref2.borrow_mut().value += 1;
println!("{}", counter.borrow().value); // 2Pattern breakdown:
Rc<T>: Multiple ownersRefCell<T>: Interior mutabilityRc<RefCell<T>>: Multiple owners can all mutateDecision tree:
Do you need multiple owners?
├─ NO → Use Box<T>
│ └─ For: Recursive types, large data on heap
│
└─ YES → Do you need mutation?
├─ NO → Use Rc<T>
│ └─ For: Shared read-only data
│
└─ YES → Use Rc<RefCell<T>>
└─ For: Shared mutable data (single-threaded)
Multi-threaded variants (covered next week):
Arc<T>: Thread-safe Rc<T> (Atomic Reference Counting)Mutex<T>: Thread-safe RefCell<T> (Mutual Exclusion)Arc<Mutex<T>>: Thread-safe shared mutable dataRust doesn’t have exceptions. It uses Result<T, E> for errors.
Why this is better:
Result is just an enumTwo approaches:
Result<T, E> (file not found, parse error)panic! (programming bugs, invariant violations)Result is an enum with two variants:
enum Result<T, E> {
Ok(T), // Success - contains value
Err(E), // Failure - contains error
}
// Example: division that can fail
fn divide(a: f64, b: f64) -> Result<f64, String> {
if b == 0.0 {
Err(String::from("Division by zero"))
} else {
Ok(a / b)
}
}
// Usage with match:
match divide(10.0, 2.0) {
Ok(result) => println!("Result: {}", result),
Err(e) => println!("Error: {}", e),
}
// Or with if let:
if let Ok(result) = divide(10.0, 2.0) {
println!("Result: {}", result);
}The ? operator makes error handling ergonomic:
use std::fs::File;
use std::io::{self, Read};
// Without ?
fn read_file_verbose(path: &str) -> io::Result<String> {
let file_result = File::open(path);
let mut file = match file_result {
Ok(f) => f,
Err(e) => return Err(e), // Early return on error
};
let mut contents = String::new();
match file.read_to_string(&mut contents) {
Ok(_) => Ok(contents),
Err(e) => Err(e),
}
}
// With ? (much cleaner!)
fn read_file(path: &str) -> io::Result<String> {
let mut file = File::open(path)?; // ? propagates error
let mut contents = String::new();
file.read_to_string(&mut contents)?; // ? propagates error
Ok(contents)
}? means: “If Ok, unwrap the value. If Err, return the error immediately.”
Create domain-specific errors:
use std::fmt;
#[derive(Debug, Clone)]
enum ParseError {
EmptyInput,
InvalidNumber(String),
OutOfRange(i32),
}
// Implement Display for pretty error messages
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ParseError::EmptyInput => write!(f, "Input string is empty"),
ParseError::InvalidNumber(s) => write!(f, "Invalid number: {}", s),
ParseError::OutOfRange(n) => write!(f, "Number {} out of range (1-100)", n),
}
}
}
// Use custom error type
fn parse_positive_number(input: &str) -> Result<i32, ParseError> {
if input.is_empty() {
return Err(ParseError::EmptyInput);
}
let num: i32 = input.trim().parse()
.map_err(|_| ParseError::InvalidNumber(input.to_string()))?;
if num < 1 || num > 100 {
return Err(ParseError::OutOfRange(num));
}
Ok(num)
}Challenge: Parse user input with good error messages
// Given this function signature:
fn parse_age(input: &str) -> Result<u8, String> {
// TODO: Parse age (0-120), return helpful errors
}
// Test cases:
assert_eq!(parse_age("25"), Ok(25));
assert!(parse_age("").is_err()); // Empty
assert!(parse_age("abc").is_err()); // Not a number
assert!(parse_age("150").is_err()); // Out of range
assert!(parse_age("-5").is_err()); // NegativeHint: Use ? operator and .map_err() for error conversion.
Try on Rust Playground
Combining iterators, smart pointers, and error handling:
use std::rc::Rc;
use std::cell::RefCell;
use std::fmt;
#[derive(Debug, Clone)]
struct Config {
min_length: usize,
max_length: usize,
}
#[derive(Debug)]
enum ProcessError {
LineTooShort(String),
LineTooLong(String),
}
impl fmt::Display for ProcessError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
ProcessError::LineTooShort(line) => write!(f, "Line too short: {}", line),
ProcessError::LineTooLong(line) => write!(f, "Line too long: {}", line),
}
}
}
fn process_lines(
lines: &[String],
config: Rc<RefCell<Config>>, // Smart pointer for shared config
) -> Result<Vec<String>, ProcessError> { // Result for error handling
lines
.iter() // Iterator
.map(|line| {
let cfg = config.borrow();
let len = line.len();
if len < cfg.min_length {
Err(ProcessError::LineTooShort(line.clone()))
} else if len > cfg.max_length {
Err(ProcessError::LineTooLong(line.clone()))
} else {
Ok(line.to_uppercase()) // Transform on success
}
})
.collect() // Collect into Result<Vec<_>, _>
}Where you’ll use these patterns:
Iterators:
Smart Pointers:
Error Handling:
Job interview topics: All three appear in Rust coding interviews!
Iterators:
❌ Collecting unnecessarily: .collect::<Vec<_>>() then iterate again ✅ Chain operations: .map().filter().sum() in one go
❌ Using clone() in iterators when reference works ✅ Use references or Copy types
Smart Pointers:
❌ Using Rc<RefCell<T>> when &mut T works ✅ Start simple, add smart pointers only when needed
❌ Creating reference cycles with Rc (memory leak!) ✅ Use Weak<T> for back-references
Error Handling:
❌ Using .unwrap() everywhere (panics in production!) ✅ Use ? operator and handle errors properly
❌ String errors: Err("something failed".to_string()) ✅ Custom error enums with specific variants
If you want to go deeper:
Custom Iterators:
Iterator traitfilter and mapMore Smart Pointers:
Weak<T>: Non-owning references (break cycles)Cow<T>: Clone-on-writePin<T>: Prevent moves (async/await)Error Libraries:
None of these are required for Lab 13, but great for projects!
This week’s lab covers everything we discussed:
Part 1: Iterators and Closures (30%)
Part 2: Smart Pointers (30%)
Box<T>Rc<T>RefCell<T>Part 3: Error Handling (30%)
Result? operatorPart 4: Integrative Exercise (10%)
What makes Rust code “idiomatic”:
✅ Iterators: Functional data processing ✅ Closures: Functions that capture environment ✅ Smart pointers: Flexible memory management ✅ Error handling: Explicit, recoverable errors
Key takeaways:
Result<T, E> makes errors part of your APINext week: Multithreaded applications with Lab 14!
Official Documentation:
Practice:
Community:
Office hours: See syllabus
AI assistance: GitHub Copilot, ChatGPT, Claude, Gemini CLI
Lab 13: Due Sunday at 11:59 PM
Good luck and have fun with idiomatic Rust! 🦀
IS4010: App Development with AI