IS4010: AI-Enhanced Application Development

Week 11: Structuring Code and Data in Rust

Brandon M. Greenwell

Session 1: Organizing Code and Custom Types

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 math {  // Define a module named 'math'

    pub fn add(a: i32, b: i32) -> i32 {  // 'pub' makes this visible outside the module
        a + b  // Return the sum
    }

    fn subtract(a: i32, b: i32) -> i32 {  // No 'pub' = private to this module only
        a - b  // Return the difference
    }
}

fn main() {
    println!("2 + 3 = {}", math::add(2, 3));  // ✅ Works! add() is public
    // println!("5 - 2 = {}", math::subtract(5, 2));  // ❌ ERROR: subtract() is 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

Quick rule of thumb: when do you need pub? 🤔

✅ Use pub when accessed from:

  • A parent module
  • A sibling’s child module
  • Outside the crate

Example: Public API functions, structs used by other modules

❌ No pub needed when accessed from:

  • The same module
  • Its child modules

Example: Helper functions, internal implementation details

Example: pub visibility in action

mod calculator {  // Parent module

    fn validate_inputs(a: i32, b: i32) -> bool {  // ❌ No pub - internal helper only
        b != 0  // Make sure we're not dividing by zero
    }

    pub mod operations {  // ✅ pub - main() needs to access this module

        pub fn divide(a: i32, b: i32) -> i32 {  // ✅ pub - public API function
            if super::validate_inputs(a, b) {  // ✅ Child can access parent's private!
                a / b  // Do the division
            } else {
                0  // Return 0 for invalid inputs (simplified error handling)
            }
        }

        fn log_operation(msg: &str) {  // ❌ Private - internal to operations only
            println!("LOG: {}", msg);  // Internal logging
        }
    }
}

fn main() {
    println!("10 / 2 = {}", calculator::operations::divide(10, 2));  // ✅ Works! Both pub
    // calculator::validate_inputs(10, 2);  // ❌ Error! validate_inputs is private
    // calculator::operations::log_operation("test");  // ❌ Error! log_operation is private
}

Try it in Rust Playground

The use keyword: bringing paths into scope

mod math {  // Parent module

    pub mod operations {  // Public submodule
        pub fn add(a: i32, b: i32) -> i32 {  // Public function
            a + b  // Return the sum
        }

        pub fn multiply(a: i32, b: i32) -> i32 {  // Public function
            a * b  // Return the product
        }
    }
}

// Bring the operations module into scope with 'use'
use math::operations;

fn main() {
    println!("2 + 3 = {}", operations::add(2, 3));  // ✅ Shorter! Just operations::add
    println!("2 * 3 = {}", operations::multiply(2, 3));  // Instead of math::operations::multiply
    // Without 'use', we'd need: math::operations::add(2, 3)
}

Try it in Rust Playground

Separating modules into files

Project structure:

src/
├── main.rs
├── math.rs
└── math/
    ├── operations.rs
    └── constants.rs

In main.rs:

mod math;  // Tells Rust to look for math.rs (no braces = load from file)

use math::operations;  // Import the operations submodule

fn main() {
    println!("{}", operations::add(2, 3));  // Use the add function
}

In math.rs:

// src/math.rs
// This file declares what submodules exist under 'math'
pub mod operations;  // Makes operations public and tells Rust to load math/operations.rs
pub mod constants;   // Makes constants public and tells Rust to load math/constants.rs

In math/operations.rs:

// src/math/operations.rs
// This file contains the actual implementation of math operations
pub fn add(a: i32, b: i32) -> i32 {  // Public function
    a + b  // Return sum
}

pub fn multiply(a: i32, b: i32) -> i32 {  // Public function
    a * b  // Return product
}

In math/constants.rs:

// src/math/constants.rs
// This file defines mathematical constants
pub const PI: f64 = 3.14159;  // Approximation of pi
pub const E: f64 = 2.71828;   // Approximation of Euler's number

🤖 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 Shape {  // Define an enum to represent different geometric shapes
    Circle(f64),              // Circle variant holds radius
    Rectangle(f64, f64),      // Rectangle holds width and height
    Triangle(f64, f64),       // Triangle holds base and height
}

// Create instances of different shapes
let circle = Shape::Circle(5.0);                    // Circle with radius 5.0
let rectangle = Shape::Rectangle(4.0, 6.0);         // Rectangle 4.0 × 6.0
let triangle = Shape::Triangle(3.0, 8.0);           // Triangle with base 3.0, height 8.0

Try it in Rust Playground

Pattern matching with match

enum Shape {  // Define different geometric shapes
    Circle(f64),              // Circle holds radius
    Rectangle(f64, f64),      // Rectangle holds width and height
    Triangle(f64, f64),       // Triangle holds base and height
}

fn area(shape: Shape) -> f64 {  // Calculate area for any shape
    match shape {  // Pattern match on the shape variant
        Shape::Circle(r) => {  // Extract radius from Circle
            println!("Calculating area of circle with radius {}", r);
            3.14159 * r * r  // Formula: πr²
        }
        Shape::Rectangle(w, h) => {  // Extract width and height
            w * h  // Formula: width × height
        }
        Shape::Triangle(b, h) => {  // Extract base and height
            0.5 * b * h  // Formula: ½ × base × height
        }
    }  // ✅ Compiler ensures we handle ALL variants!
}

fn main() {
    let circle = Shape::Circle(5.0);  // Create a circle with radius 5.0
    println!("Area: {:.2} square units", area(circle));  // Output: 78.54

    let rect = Shape::Rectangle(4.0, 6.0);  // Create a 4×6 rectangle
    println!("Area: {:.2} square units", area(rect));  // Output: 24.00
}

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: week11/lab11.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”

Preview: You’ve Been Using Generics!

Notice the angle brackets?

let mut numbers: Vec<i32> = Vec::new();
let result: Result<String, io::Error> = read_file();
let maybe_value: Option<&str> = map.get("key");
let mut scores: HashMap<String, i32> = HashMap::new();

What <T> means: - Vec<i32> = “a vector of i32 values” - Result<String, io::Error> = “success gives String, error gives io::Error” - Option<&str> = “maybe a &str, maybe nothing” - HashMap<String, i32> = “map from String keys to i32 values”

You’ve been using generic types all along!

Next Week: Writing Your Own Generics

Week 12 preview - Generics and Traits:

You’ll learn to: - Write generic functions that work with any type - Create generic structs like Stack<T> or Cache<K, V> - Define traits (Rust’s version of interfaces) - Implement traits like Display, Iterator, Clone - Use trait bounds to constrain generic types

Why it matters: - Write reusable code without duplication - Zero runtime cost (Rust generates specialized code) - Type-safe abstractions - Professional Rust development pattern

Teaser: Lab 12 will have you build a generic Stack<T> that works with any type!

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!