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 vs &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?”
This week’s lab has three parts:
Result from functions that can failOption for optional data? operatorVec<T>HashMap<K, V> for fast lookupsFull instructions: week11/lab11.md
What you learned today is used daily in industry:
Interview topics: - “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 - 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!
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 due: Sunday, November 16 at 11:59 PM
Office hours: Available on Microsoft Teams - reach out anytime!