IS4010: AI-Enhanced Application Development

Week 11: Structuring Code and Data in Rust

Brandon M. Greenwell

Session 1: Organizing Code and Custom Types

Housekeeping: Lab 10 check-in

  • Lab 10 (Ownership & Borrowing) was due last Sunday
  • How did the borrow checker game go?
  • This week builds on those concepts - structs and enums follow ownership rules
  • Lab 11 due Sunday, November 16 - data modeling with structs/enums/collections

From small scripts to real programs

  • So far, we’ve written small Rust programs in a single main.rs file
  • Real applications need organization: separate files, logical grouping, reusable components
  • Rust’s module system provides powerful tools for structuring code
  • Today we learn how professionals organize Rust projects

The Rust module system hierarchy

Three levels of organization:

  1. Packages - A Cargo feature that builds, tests, and shares crates
  2. Crates - A tree of modules that produces a library or executable
  3. Modules - Let you control organization, scope, and privacy

Think of it like: Package = project, Crate = building, Modules = rooms

Creating modules with mod

// src/main.rs
mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {
            println!("Adding to waitlist");
        }
    }

    mod serving {
        fn take_order() {
            println!("Taking order");
        }
    }
}

fn main() {
    front_of_house::hosting::add_to_waitlist();
    // front_of_house::serving::take_order(); // ❌ ERROR: private!
}

Try it in Rust Playground

Privacy in Rust: default private

  • Everything is private by default in Rust modules
  • Use pub keyword to make items public
  • Privacy rules:
    • Parent modules can’t see into child private items
    • Child modules can see everything in parent modules
    • Sibling modules can see each other’s public items
  • This is defensive design - explicit about your public API

The use keyword: bringing paths into scope

mod front_of_house {
    pub mod hosting {
        pub fn add_to_waitlist() {
            println!("Added!");
        }
    }
}

// Bring the module into scope
use front_of_house::hosting;

fn main() {
    hosting::add_to_waitlist(); // Much cleaner!
    // No need for full path: front_of_house::hosting::add_to_waitlist()
}

Try it in Rust Playground

Separating modules into files

Project structure:

src/
├── main.rs
├── front_of_house.rs
└── front_of_house/
    ├── hosting.rs
    └── serving.rs

In main.rs:

mod front_of_house; // Tells Rust to look for front_of_house.rs

use front_of_house::hosting;

fn main() {
    hosting::add_to_waitlist();
}

🤖 AI co-pilot technique: module organization

When organizing code into modules, ask your AI assistant:

Effective prompts: - “How should I organize this Rust project into modules? [describe project]” - “What should be public vs private in this module? [paste code]” - “Explain the difference between ‘use’ and ‘mod’ in Rust” - “Help me split this large main.rs into separate modules”

Pro tip: Ask AI to explain the module tree structure - visualizations help!

Defining custom data with struct

  • A struct (structure) groups related data into a single, meaningful type
  • Like Python classes but data only (methods come separately)
  • Three kinds:
    1. Named-field structs - most common
    2. Tuple structs - fields accessed by position
    3. Unit structs - no fields (rare)
struct User {
    username: String,
    email: String,
    active: bool,
    sign_in_count: u64,
}

Creating struct instances

struct User {
    username: String,
    email: String,
    active: bool,
}

fn main() {
    let user1 = User {
        email: String::from("ada@example.com"),
        username: String::from("ada_lovelace"),
        active: true,
    };

    println!("Username: {}", user1.username);

    // Make mutable to change fields
    let mut user2 = User {
        email: String::from("grace@example.com"),
        username: String::from("grace_hopper"),
        active: true,
    };
    user2.email = String::from("new_email@example.com");
}

Try it in Rust Playground

Adding behavior with impl blocks

struct Rectangle {
    width: u32,
    height: u32,
}

impl Rectangle {
    // Method (takes &self)
    fn area(&self) -> u32 {
        self.width * self.height
    }

    // Associated function (no self) - like a static method
    fn square(size: u32) -> Rectangle {
        Rectangle {
            width: size,
            height: size,
        }
    }
}

fn main() {
    let rect = Rectangle { width: 30, height: 50 };
    println!("Area: {}", rect.area());

    let sq = Rectangle::square(10);
}

Try it in Rust Playground

Modeling choices with enum

  • An enum (enumeration) represents a value that can be one of several variants
  • Much more powerful than enums in C, Java, or Python
  • Each variant can hold different types of data
  • Perfect for modeling “this OR that OR that” situations
enum IpAddr {
    V4(u8, u8, u8, u8),
    V6(String),
}

let home = IpAddr::V4(127, 0, 0, 1);
let loopback = IpAddr::V6(String::from("::1"));

Pattern matching with match

enum Coin {
    Penny,
    Nickel,
    Dime,
    Quarter(String), // Quarter holds state name
}

fn value_in_cents(coin: Coin) -> u8 {
    match coin {
        Coin::Penny => 1,
        Coin::Nickel => 5,
        Coin::Dime => 10,
        Coin::Quarter(state) => {
            println!("Quarter from {}!", state);
            25
        }
    }
}

fn main() {
    let my_coin = Coin::Quarter(String::from("Alaska"));
    println!("Value: {} cents", value_in_cents(my_coin));
}

Try it in Rust Playground

🤖 AI co-pilot technique: designing with structs and enums

When modeling data, ask your AI assistant:

Effective prompts: - “Should I use a struct or enum for this? [describe data]” - “Help me model a [domain concept] in Rust with structs and enums” - “What fields should this struct have? [describe requirements]” - “Show me how to use match to handle all cases of this enum”

Example: “I need to model a blog post that can be Draft, Published, or Archived. Each state has different data. How should I structure this in Rust?”

Session 2: Error Handling and Collections

The problem with null

Option<T>: handling optional values

enum Option<T> {
    Some(T),
    None,
}

fn find_user(id: u32) -> Option<String> {
    if id == 1 {
        Some(String::from("Alice"))
    } else {
        None
    }
}

fn main() {
    match find_user(1) {
        Some(name) => println!("Found: {}", name),
        None => println!("User not found"),
    }

    // Shorthand with if let
    if let Some(name) = find_user(2) {
        println!("Found: {}", name);
    } else {
        println!("User not found");
    }
}

Try it in Rust Playground

Result<T, E>: recoverable errors

enum Result<T, E> {
    Ok(T),
    Err(E),
}

use std::fs::File;
use std::io::ErrorKind;

fn open_config() -> Result<File, std::io::Error> {
    File::open("config.txt")
}

fn main() {
    match open_config() {
        Ok(file) => println!("Opened file successfully"),
        Err(error) => match error.kind() {
            ErrorKind::NotFound => println!("File not found!"),
            other => println!("Error opening file: {:?}", other),
        },
    }
}

Try it in Rust Playground

The ? operator: error propagation

use std::fs::File;
use std::io::{self, Read};

fn read_username_from_file() -> Result<String, io::Error> {
    let mut file = File::open("username.txt")?; // ❓ propagates error if Err
    let mut username = String::new();
    file.read_to_string(&mut username)?;        // ❓ propagates error if Err
    Ok(username)                                  // ✅ returns Ok if success
}

// Without ? operator, this would be:
// match File::open("username.txt") {
//     Ok(mut file) => match file.read_to_string(&mut username) {
//         Ok(_) => Ok(username),
//         Err(e) => Err(e),
//     },
//     Err(e) => Err(e),
// }

🤖 AI co-pilot technique: error handling patterns

When handling errors, ask your AI assistant:

Effective prompts: - “When should I use Option vs Result in Rust?” - “Help me handle this error idiomatically: [paste code]” - “Explain the ? operator and when I can use it” - “Convert this match error handling to use ? operator”

Pro tip: Ask AI to show both verbose (match) and concise (? operator) versions to understand what’s happening under the hood.

Common collections: Vec<T>

  • Vec<T> (vector) is a growable array - like Python’s list
  • Stores values of the same type
  • Allocated on the heap (flexible size)
  • Most common collection in Rust
fn main() {
    // Creating vectors
    let mut v: Vec<i32> = Vec::new();
    let mut v2 = vec![1, 2, 3]; // vec! macro for initial values

    // Adding elements
    v.push(5);
    v.push(6);

    // Accessing elements
    let third = &v2[2];         // Panics if out of bounds
    let maybe_third = v2.get(2); // Returns Option<&T>

    // Iterating
    for i in &v2 {
        println!("{}", i);
    }
}

Try it in Rust Playground

Strings in Rust: String vs &str

  • Rust has two main string types (not one!):
  • String - Owned, growable, heap-allocated (like Vec<u8>)
  • &str - String slice, borrowed, immutable view
  • All strings are UTF-8 encoded
fn main() {
    // String - owned
    let mut s = String::from("hello");
    s.push_str(" world"); // Can modify

    // &str - borrowed string slice
    let slice: &str = &s[0..5]; // "hello"

    // String literals are &str
    let literal = "I'm a &str";

    // Converting
    let s2: String = literal.to_string();
    let slice2: &str = &s2;
}

Try it in Rust Playground

HashMap<K, V>: key-value storage

use std::collections::HashMap;

fn main() {
    // Creating
    let mut scores = HashMap::new();

    // Inserting
    scores.insert(String::from("Blue"), 10);
    scores.insert(String::from("Yellow"), 50);

    // Accessing
    let team_name = String::from("Blue");
    let score = scores.get(&team_name);  // Returns Option<&V>

    match score {
        Some(&s) => println!("Score: {}", s),
        None => println!("Team not found"),
    }

    // Iterating
    for (key, value) in &scores {
        println!("{}: {}", key, value);
    }
}

Try it in Rust Playground

Choosing the right collection

Vec<T> - Use when: - You need an ordered list - You want to access by index - You’ll add/remove from the end

String - Use when: - You need owned, growable text - Building strings dynamically

HashMap<K, V> - Use when: - You need key-value lookups - Order doesn’t matter - Fast access by key is important

Real-world example: building a phone book

use std::collections::HashMap;

struct Contact {
    name: String,
    phone: String,
    email: Option<String>, // Email is optional
}

fn main() {
    let mut phone_book: HashMap<String, Contact> = HashMap::new();

    phone_book.insert(
        String::from("Alice"),
        Contact {
            name: String::from("Alice Smith"),
            phone: String::from("555-1234"),
            email: Some(String::from("alice@example.com")),
        }
    );

    // Look up by name
    if let Some(contact) = phone_book.get("Alice") {
        println!("Phone: {}", contact.phone);
        if let Some(email) = &contact.email {
            println!("Email: {}", email);
        }
    }
}

Try it in Rust Playground

🤖 AI co-pilot technique: working with collections

When using collections, ask your AI assistant:

Effective prompts: - “Which Rust collection should I use for [describe use case]?” - “Help me convert this Python list comprehension to Rust: [paste code]” - “Show me how to iterate over a HashMap in Rust” - “How do I handle the Option returned by HashMap.get()?” - “What’s the difference between Vec::push and Vec::append?”

Example: “I have a list of user IDs and need fast lookup. Should I use Vec or HashMap?”

Introducing Lab 11: data modeling challenge

This week’s lab has three parts:

  1. Structs and Enums
    • Model a library system with custom types
    • Use structs for Book/DVD data
    • Use enums for ItemStatus (Available/CheckedOut)
  2. Error Handling
    • Return Result from functions that can fail
    • Use Option for optional data
    • Practice the ? operator
  3. Collections in Action
    • Store items in Vec<T>
    • Build HashMap<K, V> for fast lookups
    • Iterate and search

Full instructions: labs/lab11/README.md

Career relevance: production Rust patterns

What you learned today is used daily in industry:

  • Discord - Structs for message data, enums for event types
  • Dropbox - Result for error handling, HashMap for file tracking
  • Cloudflare - Vec for request queues, modules for organization

Interview topics: - “How does Rust handle errors differently than exceptions?” - “When would you use an enum instead of inheritance?” - “Explain Option vs Result”

Resources for going deeper

Official Rust documentation: - The Rust Book - Chapter 7: Modules - The Rust Book - Chapter 5: Structs - The Rust Book - Chapter 6: Enums and Pattern Matching - The Rust Book - Chapter 9: Error Handling - The Rust Book - Chapter 8: Collections

Interactive practice: - Rust by Example - Modules - Rust by Example - Error Handling - Rustlings - Structs Exercises

Questions?

Remember: - Modules organize code, structs/enums organize data - Option<T> eliminates null pointer errors - Result<T, E> makes error handling explicit - Collections (Vec, String, HashMap) follow ownership rules - Use AI to explore design patterns and learn idioms

Lab 11 due: Sunday, November 16 at 11:59 PM

Office hours: Available on Microsoft Teams - reach out anytime!