Lecture 21 - Pattern Matching and Review

Logistics

  • HW4 due Friday night
  • HW3 corrections due Sunday night
  • Feedback on pre-task value: mixed
  • After oral exams and corrections, 25/50/75 %iles: 86%, 92%, 95%
  • Our next midterm is in 2 weeks
  • Changes to corrections procedure for midterm 2

Presenting Struct Design from Monday

See PDFs - 10 min

Learning Objectives

By the end of today, you should be able to:

  • Use pattern matching to extract data from structs and enums
  • Apply simple pattern guards for conditional matching
  • Review ownership concepts from L14-L20

Part 1 - Pattern Matching with Structs

Getting data out of enums and structs

The same match statements we saw for enums works for structs:

#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Book {
    title: String,
    year: u32,
    rating: f64,
    genre: Genre,
}

enum Genre {
    Fiction,
    NonFiction,
    Mystery,
    SciFi,
}

// Extract data from enums
fn describe_genre(genre: &Genre) -> &str {
    match genre {
        Genre::Fiction => "Literary fiction",
        Genre::NonFiction => "Factual content",
        Genre::Mystery => "Mystery and suspense",
        Genre::SciFi => "Science fiction",
    }
}

// Extract data from structs
fn get_rating(book: &Book) -> f64 {
    match book {
        Book { rating, .. }  => *rating,
    }
}

fn check_highly_rated(book: &Book) -> bool {
    match book {
        Book { rating, .. } if *rating >= 4.5 => true,
        _ => false,
    }
}
}

Pattern Guards for Complex Conditions

#![allow(unused)]
fn main() {
#[derive(Debug)]
struct Book {
    title: String,
    rating: f64,
    pages: u32,
}
}
#![allow(unused)]
fn main() {
fn classify_book(book: &Book) -> &'static str {
    match book {
        Book { rating, pages, .. } if rating >= 4.5 && pages >= 400 => {
            "Epic Bestseller"
        }
        Book { rating, pages, .. } if rating >= 4.5 => {
            "Highly Rated"
        }
        Book { pages, .. } if pages >= 600 => "Epic",
        Book { pages, .. } if pages >= 300 => "Standard Novel",
        Book { rating, .. } if rating < 2.0 => "Needs Review",
        _ => "Short Read",
    }
}
}

Destructuring in Let Bindings

#[derive(Debug)]
struct DataPoint {
    x: f64,
    y: f64,
    label: String,
    confidence: f64,
}

let point1 = DataPoint {
    x: 1.5, y: 2.3,
    label: "Positive".to_string(),
    confidence: 0.95,
};

let point2 = DataPoint {
    x: 5.0, y: -1.1,
    label: "Negative".to_string(),
    confidence: 0.70,
};
#![allow(unused)]
fn main() {
// Destructure in let binding
let DataPoint { x, y, label, confidence } = point1;
println!("({}, {}) - {} ({:.1}%)", x, y, label, confidence * 100.0);

// Partial destructuring
let DataPoint { label, confidence, .. } = point2;  // Ignore x, y
println!("We only learned: {} ({:.1}%)", label, confidence * 100.0);

// In function parameters
fn print_coords(DataPoint { x, y, .. }: &DataPoint) {
    println!("Point at ({:.2}, {:.2})", x, y);
}
}

Ownership Interlude: Destructuring Moves Quiz

Question: After this destructuring, what can we still use?

#![allow(unused)]
fn main() {
let point = DataPoint {
    x: 1.0, y: 2.0,
    label: "test".to_string(),
    confidence: 0.9,
};

let DataPoint { x, label, .. } = point;
}

What's still usable: point, point.x, point.y, point.label, point.confidence?

Part 2: Review of L14-L20

Stack vs. Heap (Lecture 14)

  • Stack: Fast, fixed-size, automatic cleanup (LIFO - "stack of plates")
    • Each function call gets a stack frame
    • Stores simple types: i32, bool, char, arrays, tuples
    • Variables cleaned up when function ends
  • Heap: Flexible size, manual management (Rust helps!)
    • For data that can grow/shrink or are very large
    • Types like String, Vec, HashMap, Box store data here
    • Stack holds pointers to heap data
  • Memory addresses: Every location has a unique address to a physical part of RAM (like 0x7fff5fbff6bc)

Ownership Rules (Lecture 15)

The three fundamental ownership rules:

  1. Each value has an owner
  2. Only one owner at a time
  3. When owner goes out of scope, value is dropped

Key concepts:

  • Move semantics: let s2 = s1; moves ownership for heap types (String, Vec)
  • Copy semantics: let y = x; copies value for stack types (i32, bool)
  • Clone: .clone() creates explicit copy of heap data (and copies stack data)
  • Function calls: Passing to function moves or copies (same rules)
  • Return values: Transfer ownership back to caller

Borrowing and References (Lecture 16)

  • Creating references: & operator borrows without taking ownership
    • let data_ref = &data; - both data and data_ref usable
  • Functions with references: fn process(data: &Vec<i32>) - can use data without moving it
  • Dereferencing: * operator accesses value through reference
    • *x_ref > *y_ref to compare values, and for use in match
    • Often auto-dereferenced (eg println! and arithmetic)
  • Pattern matching: for &num in numbers.iter() extracts values (Copy types only)

Mutable References (Lecture 17)

Borrowing rules (the borrow checker):

  • Rule 1: Many immutable references OR one mutable reference (not both!)
  • Rule 2: References must not outlive what they point to

Mutable references (&mut T):

  • fn modify(data: &mut Vec<i32>) - can change borrowed data
  • Must declare variable as mut to create mutable reference
  • Can't have other references (mutable or immutable) at same time
  • Use * to modify through reference: *x = 10;

Reference timing:

  • Rust tracks when references are last used
  • Can create new references after previous ones stop being used

Strings and Slices (Lecture 18)

  • String types:
    • String: Owned, growable string on heap
    • &str: String slice - reference to string data (points to the heap)
      • string literals point to the binary itself
      • string slices built on owned strings point to the heap
      • they have the same type/structure (a "fat pointer")
    • &String: Reference to a String (points to the stack)

Slices:

  • Syntax: &data[start..end], &data[..3], &data[2..]
  • &[T]: Slice of array/Vec elements (pointer + length)
  • Slices are references - don't take ownership

UTF-8 encoding:

  • Characters can be 1-4 bytes
  • .len() returns bytes, not character count
  • Use .chars() to iterate over characters
  • No text[0] indexing - would fail to compile

Iterators:

  • .iter(): Generates immutable references &T
  • .iter_mut(): Generates mutable references &mut T
  • .iter().enumerate(): Generates pairs (i32, &T)
  • .iter_mut().enumerate(): Generates pairs (i32, &mut T)
  • .collect(): Collapse iterators into target type

HashMap and HashSet (Lecture 19)

Hash functions:

  • Convert any input to a number (deterministic, fast, uniform distribution)
  • Used to determine bucket index: hash % capacity

HashMap<K, V>:

  • Hashes the key to get a hash value
  • Creates a bucket array that stores hash values and pointers to (key, value) pairs
  • Create: HashMap::new() or collect from iterator
  • Insert: .insert(key, value) - overwrites if key exists
  • Get: .get(&key) returns Option<&V>
  • Check: .contains_key(&key) returns bool
  • Iterate: for (key, value) in map.iter()
  • Pattern: .entry(key).or_insert(value) for counting/defaults

HashSet:

  • Stores unique values (no duplicates)
  • .insert(value), .contains(&value)
  • Create from Vec: vec.iter().cloned().collect()
  • Fast membership testing (better than Vec for large data)

Structs and Methods (Lecture 20)

Defining structs:

#![allow(unused)]
fn main() {
struct Customer {
    name: String,
    age: u32,
    member: bool,
}
}
  • Group related data together
  • Access fields with dot notation: customer.name
  • Tuple structs: struct Point(f64, f64, f64);
  • Update syntax: Customer { name: "Bob".to_string(), ..alice }

Methods (impl blocks):

#![allow(unused)]
fn main() {
impl Customer {
    fn new(name: String) -> Customer { ... }  // Constructor
    fn display(&self) { ... }                 // Read-only
    fn update_age(&mut self, age: u32) { ... } // Modify
    fn into_name(self) -> String { ... }      // Consume
}
}

Some function signature best practices

#![allow(unused)]
fn main() {
// ❌ BAD: Unnecessary ownership transfer
fn bad_process(key: String, value: f64) -> f64 {
    value * 2.0  // Doesn't need to own `key`!
}

// ✅ GOOD: Flexible parameter
fn good_process(key: &str, value: f64) -> f64 {
    value * 2.0  // Accepts both String and &str
}

// ❌ BAD: Dangerous unwrap
fn bad_lookup(map: &HashMap<String, i32>, key: &str) -> i32 {
    map.get(key).unwrap()  // Panics if key missing!
}

// ✅ GOOD: Safe Option handling
fn good_lookup(map: &HashMap<String, i32>, key: &str) -> Option<i32> {
    map.get(key).copied()  // Returns Option for safety
}
}

Debug Quiz

Question: What's wrong with this code?

#![allow(unused)]
fn main() {
fn get_first_line(text: String) -> &str {
    let lines: Vec<&str> = text.lines().collect();
    if lines.is_empty() {
        ""
    } else {
        lines[0]
    }
}
}

A) Should return Option<&str> for safety
B) Can't return reference to text which goes out of scope
C) collect() is unnecessary here

A few common ownership issues

#![allow(unused)]
fn main() {
#[derive(Debug, Clone)]
struct Data { value: i32, label: String }

// Issue 1: Unnecessary ownership
fn process_label(label: String) -> String {  // Should be &str?
    label.to_uppercase()
}

// Issue 2: Lifetime problem
fn get_first_item(items: Vec<String>) -> &String {  // ERROR - Can't return ref to owned data!
    &items[0]
}

// Issue 3: Move in loop
fn analyze_data(data_list: Vec<Data>) {
    for data in data_list {  // Moves each Data
        println!("{:?}", data);
    }
    println!("Count: {}", data_list.len());  // ERROR - data_list moved!
}

// Issue 4: Borrowing conflict
fn modify_and_read(items: &mut Vec<i32>) {
    let first = &items[0];  // Immutable borrow
    items.push(42);         // ERROR - Mutable borrow while immutable exists!
    println!("First: {}", first);
}
}