Week 11: Structuring code and data in Rust
main.rs fileThree levels of organization:
Think of it like: Package = project, Crate = building, Modules = rooms
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!
}pub keyword to make items publicpub?✅ Use pub when accessed from:
Example: Public API functions, structs used by other modules
❌ No pub needed when accessed from:
Example: Helper functions, internal implementation details
pub visibility in actionmod 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
}use keyword: bringing paths into scopemod 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)
}Project structure:
src/
├── main.rs
├── math.rs
└── math/
├── operations.rs
└── constants.rs
In main.rs:
In math.rs:
In math/operations.rs:
In math/constants.rs:
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!
structstruct (structure) groups related data into a single, meaningful typestruct 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");
}impl blocksstruct 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);
}enumenum (enumeration) represents a value that can be one of several variantsenum 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.0matchenum 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
}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?”
nullOption<T>: handling optional valuesenum 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");
}
}Result<T, E>: recoverable errorsenum 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),
},
}
}? operator: error propagationuse 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),
// }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.
Vec<T>Vec<T> (vector) is a growable array - like Python’s listfn 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);
}
}String and &strString - Owned, growable, heap-allocated (like Vec<u8>)&str - String slice, borrowed, immutable viewfn 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;
}HashMap<K, V>: key-value storageuse 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);
}
}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
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);
}
}
}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?”
Implement the supplied types in src/student.rs:
Student methods
Grade and CourseGrade methods
StudentDatabase methods
HashMap, reject duplicate IDs, search, count, and summarize GPADo not change the supplied types or test module. Rename placeholder parameters by dropping their leading underscore.
Full instructions: week11/lab11.md
Examples to investigate:
Questions for review: - “How does Rust handle errors differently than exceptions?” - “When would you use an enum instead of inheritance?” - “Explain Option vs Result”
Notice the angle brackets?
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!
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 - Static dispatch can generate specialized code; dynamic dispatch remains available when needed - Trait bounds state which operations a generic implementation requires - Patterns used throughout Rust libraries and applications
Teaser: Lab 12 will have you build a generic Stack<T> that works with any type!
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
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 workflow:
A green Week 11 badge represents the complete 10-point lab.
Office hours: Available on Microsoft Teams - reach out anytime!
IS 4010: App Dev with AI