├── 02-hello-cargo.md ├── 01-setting-up-rust.md ├── README.md ├── 03-variables-data-types-mutability.md ├── 05-functions-modules.md └── 04-control-flow.md /02-hello-cargo.md: -------------------------------------------------------------------------------- 1 | # 2. Hello, Cargo! 2 | 3 | Cargo is Rust's build system and package manager. It helps you create, build, and manage Rust projects easily. 4 | 5 | ## 2.1 Create a New Project 6 | 7 | Open your terminal and run: 8 | 9 | ```sh 10 | cargo new hello_cargo 11 | cd hello_cargo 12 | ``` 13 | 14 | This creates a new directory called `hello_cargo` with the following structure: 15 | 16 | ``` 17 | hello_cargo/ 18 | ├── Cargo.toml 19 | └── src 20 | └── main.rs 21 | ``` 22 | 23 | - `Cargo.toml`: Project metadata and dependencies. 24 | - `src/main.rs`: Main source file. 25 | 26 | ## 2.2 Build and Run Your Project 27 | 28 | To build your project: 29 | 30 | ```sh 31 | cargo build 32 | ``` 33 | 34 | To run it: 35 | 36 | ```sh 37 | cargo run 38 | ``` 39 | 40 | You should see: 41 | ``` 42 | Compiling hello_cargo v0.1.0 (...) 43 | Finished dev [unoptimized + debuginfo] target(s) in ... 44 | Running `target/debug/hello_cargo` 45 | Hello, world! 46 | ``` 47 | 48 | ## 2.3 Explore the Project Structure 49 | 50 | - `src/main.rs` contains your program's entry point: 51 | 52 | ```rust 53 | fn main() { 54 | println!("Hello, world!"); 55 | } 56 | ``` 57 | 58 | - `Cargo.toml` looks like this: 59 | 60 | ```toml 61 | [package] 62 | name = "hello_cargo" 63 | version = "0.1.0" 64 | edition = "2021" 65 | 66 | [dependencies] 67 | ``` 68 | 69 | You can add dependencies under `[dependencies]`. 70 | 71 | ## 2.4 Clean and Build Release 72 | 73 | To clean build artifacts: 74 | 75 | ```sh 76 | cargo clean 77 | ``` 78 | 79 | To build an optimized release version: 80 | 81 | ```sh 82 | cargo build --release 83 | ``` 84 | 85 | This creates a faster executable in `target/release/`. 86 | 87 | --- 88 | 89 | Now you know how to use Cargo to manage your Rust projects! -------------------------------------------------------------------------------- /01-setting-up-rust.md: -------------------------------------------------------------------------------- 1 | # 1. Setting Up Your Rust Development Environment 2 | 3 | Welcome to Rust! Let's get your environment ready so you can start coding right away. 4 | 5 | ## 1.1 Install Rust 6 | 7 | Rust provides an installer called `rustup` that sets up everything you need. 8 | 9 | ### On macOS/Linux: 10 | ```sh 11 | curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh 12 | ``` 13 | 14 | ### On Windows: 15 | Download and run [rustup-init.exe](https://rustup.rs/). 16 | 17 | Follow the prompts to complete the installation. After installation, restart your terminal and check your Rust version: 18 | 19 | ```sh 20 | rustc --version 21 | ``` 22 | 23 | You should see something like: 24 | ``` 25 | rustc 1.XX.X (xxxxxxx YYYY-MM-DD) 26 | ``` 27 | 28 | ## 1.2 Set Up Your Editor 29 | 30 | Rust works well with many editors. Popular choices: 31 | - **VS Code**: Install the [rust-analyzer](https://marketplace.visualstudio.com/items?itemName=rust-lang.rust-analyzer) extension. 32 | - **IntelliJ IDEA**: Use the [Rust plugin](https://plugins.jetbrains.com/plugin/8182-rust). 33 | - **Vim/Neovim**: Use plugins like [rust.vim](https://github.com/rust-lang/rust.vim) and [coc-rust-analyzer](https://github.com/fannheyward/coc-rust-analyzer). 34 | 35 | ## 1.3 Run Your First Rust Program 36 | 37 | Let's make sure everything works by running a simple program. 38 | 39 | 1. Open a terminal. 40 | 2. Run: 41 | 42 | ```sh 43 | rustc --version 44 | ``` 45 | 46 | 3. Create a file called `main.rs` with this content: 47 | 48 | ```rust 49 | fn main() { 50 | println!("Hello, Rust!"); 51 | } 52 | ``` 53 | 54 | 4. Compile and run it: 55 | 56 | ```sh 57 | rustc main.rs 58 | ./main 59 | ``` 60 | 61 | You should see: 62 | ``` 63 | Hello, Rust! 64 | ``` 65 | 66 | Congratulations! Your Rust environment is ready. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Learn Rust by Doing: An Applied, Hands-On Roadmap 2 | 3 | Welcome! This guide is designed to make you proficient in Rust through practical, project-based learning. Each topic includes hands-on exercises and real-world applications. 4 | 5 | ## Learning Plan 6 | 7 | 1. **Setting Up Your Rust Development Environment** 8 | - Install Rust, set up your editor, and run your first program. 9 | 10 | 2. **Hello, Cargo!** 11 | - Use Cargo to create, build, and run projects. Explore project structure. 12 | 13 | 3. **Variables, Data Types, and Mutability** 14 | - Practice with variables, constants, shadowing, and type inference. 15 | 16 | 4. **Control Flow in Action** 17 | - Write programs using `if`, `match`, loops, and pattern matching. 18 | 19 | 5. **Functions and Modular Code** 20 | - Build reusable functions and split code into modules. 21 | 22 | 6. **Ownership, Borrowing, and References** 23 | - Experiment with Rust's memory safety model through small programs. 24 | 25 | 7. **Structs and Enums: Modeling Data** 26 | - Design and use custom data types in practical mini-projects. 27 | 28 | 8. **Collections and Iterators** 29 | - Manipulate vectors, strings, hash maps, and use iterators in real tasks. 30 | 31 | 9. **Error Handling: Result and Option** 32 | - Handle errors and absence of values in file and user input operations. 33 | 34 | 10. **File I/O and the Filesystem** 35 | - Read from and write to files, and explore directories. 36 | 37 | 11. **Testing Your Code** 38 | - Write and run unit and integration tests for your projects. 39 | 40 | 12. **Building a Command-Line Tool** 41 | - Create a useful CLI application from scratch. 42 | 43 | 13. **Using External Crates** 44 | - Add dependencies, use popular crates, and publish your own. 45 | 46 | 14. **Concurrency: Threads and Channels** 47 | - Write concurrent programs using threads and message passing. 48 | 49 | 15. **Networking: HTTP Client/Server** 50 | - Build a simple web client or server to interact with the internet. 51 | 52 | 16. **Interacting with JSON and APIs** 53 | - Parse JSON and call real-world APIs. 54 | 55 | 17. **Debugging and Profiling** 56 | - Use Rust's debugging and profiling tools to improve your code. 57 | 58 | 18. **Capstone Project** 59 | - Apply everything by building a complete, real-world application. 60 | 61 | --- 62 | 63 | Each topic is hands-on and project-driven. By the end, you'll be able to build, test, and deploy robust Rust applications. -------------------------------------------------------------------------------- /03-variables-data-types-mutability.md: -------------------------------------------------------------------------------- 1 | # 3. Variables, Data Types, and Mutability 2 | 3 | Rust's approach to variables isn't just different—it prevents entire categories of bugs at compile time. 4 | 5 | ## 3.1 Variable Declaration and Core Types 6 | 7 | Understanding Rust's type system is crucial for writing safe, efficient code. 8 | 9 | ### Variable Declaration Patterns 10 | ```rust 11 | let name = "Alice"; // Type inferred as &str 12 | let age: u32 = 25; // Explicit type annotation 13 | let mut score = 0; // Mutable variable 14 | let (x, y) = (10, 20); // Destructuring assignment 15 | ``` 16 | 17 | ### Scalar Types: The Building Blocks 18 | 19 | **Integers** - Choose based on your domain: 20 | ```rust 21 | let user_id: u32 = 12345; // Always positive, up to 4.2 billion 22 | let temperature: i16 = -40; // Can be negative, -32k to +32k 23 | let file_size: u64 = 1_048_576; // Large positive numbers 24 | let offset: isize = -10; // Pointer-sized, platform dependent 25 | ``` 26 | 27 | **Floating Point** - When precision matters: 28 | ```rust 29 | let price: f32 = 19.99; // 32-bit, sufficient for most decimals 30 | let scientific: f64 = 6.022e23; // 64-bit, high precision calculations 31 | ``` 32 | 33 | **Booleans and Characters**: 34 | ```rust 35 | let is_authenticated: bool = true; 36 | let grade: char = 'A'; // Unicode scalar, 4 bytes 37 | let emoji: char = '🦀'; // Rust crab! 38 | ``` 39 | 40 | ### Compound Types: Grouping Data 41 | 42 | **Tuples** - Fixed-size, mixed types: 43 | ```rust 44 | let person: (String, u32, bool) = ("Bob".to_string(), 30, true); 45 | let rgb: (u8, u8, u8) = (255, 0, 128); // Color values 0-255 46 | let coordinates = (12.5, -3.8); // Inferred as (f64, f64) 47 | ``` 48 | 49 | **Arrays** - Fixed-size, same type: 50 | ```rust 51 | let weekdays: [&str; 5] = ["Mon", "Tue", "Wed", "Thu", "Fri"]; 52 | let buffer: [u8; 1024] = [0; 1024]; // Initialize 1024 zeros 53 | let primes = [2, 3, 5, 7, 11]; // Inferred as [i32; 5] 54 | ``` 55 | 56 | ### String Types: Text Handling Done Right 57 | 58 | ```rust 59 | let literal: &str = "Hello"; // String slice, immutable 60 | let owned: String = "World".to_string(); // Owned string, growable 61 | let formatted = format!("{}!", owned); // Create new String 62 | ``` 63 | 64 | ### Collection Types: Dynamic Data 65 | 66 | ```rust 67 | let mut numbers: Vec = vec![1, 2, 3]; // Growable array 68 | let mut scores = Vec::new(); // Empty vector, type inferred later 69 | scores.push(95); // Now Vec 70 | 71 | use std::collections::HashMap; 72 | let mut users: HashMap = HashMap::new(); 73 | users.insert(1, "Alice".to_string()); 74 | ``` 75 | 76 | ### Reference Types: Borrowing Without Owning 77 | 78 | ```rust 79 | let data = vec![1, 2, 3, 4, 5]; 80 | let slice: &[i32] = &data[1..4]; // Borrow part of the vector 81 | let first: &i32 = &data[0]; // Borrow single element 82 | ``` 83 | 84 | ## 3.2 Immutability by Default: Why It Matters 85 | 86 | ```rust 87 | let connection_count = 0; 88 | // connection_count = 1; // ❌ Compile error! 89 | ``` 90 | 91 | This isn't restrictive—it's **protective**. Immutable variables prevent accidental modifications that cause bugs in concurrent code, make reasoning about program state easier, and enable compiler optimizations. 92 | 93 | **When you need change, be explicit:** 94 | ```rust 95 | let mut active_users = Vec::new(); 96 | active_users.push("alice"); // ✅ Clear intent 97 | ``` 98 | 99 | ## 3.3 Shadowing vs Mutation: Choose Your Tool 100 | 101 | Shadowing creates a **new variable** with the same name: 102 | ```rust 103 | let data = "42"; // String 104 | let data = data.parse::().unwrap(); // i32 105 | // Type transformation without mutation 106 | ``` 107 | 108 | Mutation changes an **existing variable**: 109 | ```rust 110 | let mut counter = 0; 111 | counter += 1; // Same variable, new value 112 | ``` 113 | 114 | **Rule of thumb:** Use shadowing for type/value transformations, mutation for accumulating changes. 115 | 116 | ## 3.4 Smart Type System in Action 117 | 118 | Rust infers types but catches mismatches: 119 | ```rust 120 | let user_id = 42; // i32 by default 121 | let scores = vec![85, 92, 78]; // Vec 122 | 123 | // This won't compile - prevents subtle bugs 124 | // scores.push("invalid"); // ❌ Type mismatch 125 | ``` 126 | 127 | **Be explicit when precision matters:** 128 | ```rust 129 | let temperature: f64 = 98.6; // Medical precision 130 | let timeout_ms: u64 = 5000; // No negative timeouts 131 | ``` 132 | 133 | ## 3.5 Zero-Cost Abstractions with Compound Types 134 | 135 | **Tuples** for related data without overhead: 136 | ```rust 137 | fn get_user_info() -> (String, u32, bool) { 138 | ("Alice".to_string(), 25, true) // name, age, is_active 139 | } 140 | 141 | let (name, age, _) = get_user_info(); // Destructure what you need 142 | ``` 143 | 144 | **Arrays** for known-size collections: 145 | ```rust 146 | let daily_temps: [f64; 7] = [20.1, 22.5, 19.8, 21.2, 23.0, 24.1, 20.9]; 147 | // Stack-allocated, no heap allocation overhead 148 | ``` 149 | 150 | ## 3.6 Constants: More Than Just Immutable 151 | 152 | Constants are computed at **compile time** and have global scope: 153 | ```rust 154 | const MAX_CONNECTIONS: usize = 1000; 155 | const ERROR_MESSAGE: &str = "Connection failed"; 156 | 157 | // Use for configuration, lookup tables, magic numbers 158 | const HTTP_OK: u16 = 200; 159 | ``` 160 | 161 | ## 3.7 Practical Pattern: Safe State Management 162 | 163 | ```rust 164 | struct Config { 165 | max_retries: u32, 166 | timeout_ms: u64, 167 | } 168 | 169 | impl Config { 170 | fn new() -> Self { 171 | Self { 172 | max_retries: 3, // Immutable after creation 173 | timeout_ms: 5000, 174 | } 175 | } 176 | 177 | // Controlled mutation through methods 178 | fn with_timeout(mut self, ms: u64) -> Self { 179 | self.timeout_ms = ms; 180 | self 181 | } 182 | } 183 | ``` 184 | 185 | ## 3.8 Putting It All Together: A Shopping Cart Example 186 | 187 | Let's see how data types and mutability work in a practical scenario: 188 | 189 | ```rust 190 | fn main() { 191 | // Immutable user data - won't change during session 192 | let user_name = "Alice"; 193 | let user_id: u32 = 12345; 194 | let is_premium: bool = true; 195 | 196 | // Mutable shopping cart - will change as user shops 197 | let mut cart_items: Vec = Vec::new(); 198 | let mut total_price: f64 = 0.0; 199 | let mut item_count: u32 = 0; 200 | 201 | // Adding items to cart (mutation) 202 | cart_items.push("Laptop".to_string()); 203 | cart_items.push("Mouse".to_string()); 204 | cart_items.push("Keyboard".to_string()); 205 | 206 | // Updating totals (mutation) 207 | total_price += 999.99; // Laptop 208 | total_price += 25.50; // Mouse 209 | total_price += 79.99; // Keyboard 210 | item_count = cart_items.len() as u32; 211 | 212 | // Apply discount using immutable data 213 | let discount_rate: f64 = if is_premium { 0.10 } else { 0.05 }; 214 | let discount_amount = total_price * discount_rate; 215 | let final_price = total_price - discount_amount; 216 | 217 | // Display order summary (using compound types) 218 | let order_summary = (user_id, item_count, final_price); 219 | println!("User: {} (ID: {})", user_name, order_summary.0); 220 | println!("Items: {}", order_summary.1); 221 | println!("Total: ${:.2}", order_summary.2); 222 | 223 | // Shadowing for data transformation 224 | let final_price = format!("${:.2}", final_price); // f64 → String 225 | println!("Formatted price: {}", final_price); 226 | } 227 | ``` 228 | 229 | **What this example shows:** 230 | - **Immutable variables** (`user_name`, `user_id`) for data that shouldn't change 231 | - **Mutable variables** (`cart_items`, `total_price`) for state that evolves 232 | - **Different data types** working together (strings, numbers, booleans, vectors) 233 | - **Type annotations** where clarity matters (`u32`, `f64`) 234 | - **Compound types** (tuple) for grouping related data 235 | - **Shadowing** for safe type transformations 236 | - **Safe arithmetic** with explicit type handling 237 | 238 | The compiler ensures you can't accidentally modify `user_name` or forget to mark `cart_items` as mutable—preventing bugs before they happen! 239 | 240 | ## Key Takeaways 241 | 242 | 1. **Immutability prevents bugs** - especially in concurrent code 243 | 2. **Explicit mutability** makes change obvious in code reviews 244 | 3. **Type inference** reduces boilerplate while maintaining safety 245 | 4. **Shadowing** enables clean data transformations 246 | 5. **Choose the right type** for your domain (u32 for counts, f64 for precision) 247 | 248 | The compile-time guarantees aren't restrictions—they're your safety net for building reliable software. -------------------------------------------------------------------------------- /05-functions-modules.md: -------------------------------------------------------------------------------- 1 | # 5. Functions and Modular Code 2 | 3 | Functions in Rust are more than just code blocks—they're the building blocks of safe, reusable, and maintainable software. 4 | 5 | ## 5.1 Function Fundamentals: Beyond the Basics 6 | 7 | ### Function Declaration and Return Types 8 | ```rust 9 | // Explicit return type 10 | fn calculate_tax(amount: f64, rate: f64) -> f64 { 11 | amount * rate // No semicolon = implicit return 12 | } 13 | 14 | // Unit type (returns nothing) 15 | fn log_message(message: &str) { 16 | println!("[LOG] {}", message); 17 | } 18 | 19 | // Multiple parameters with different types 20 | fn format_user_display(name: &str, age: u32, is_active: bool) -> String { 21 | format!("User: {} ({}), Status: {}", 22 | name, 23 | age, 24 | if is_active { "Active" } else { "Inactive" }) 25 | } 26 | ``` 27 | 28 | ### Early Returns and Error Handling 29 | ```rust 30 | use std::fs::File; 31 | 32 | fn validate_and_process_age(input: &str) -> Result { 33 | // Early return on parse failure 34 | let age: u32 = input.parse() 35 | .map_err(|_| "Invalid number format".to_string())?; 36 | 37 | // Early return on business logic failure 38 | if age > 150 { 39 | return Err("Age seems unrealistic".to_string()); 40 | } 41 | 42 | // Success case 43 | Ok(format!("Age {} is valid", age)) 44 | } 45 | ``` 46 | 47 | ## 5.2 Advanced Function Patterns 48 | 49 | ### Function Parameters: Ownership and Borrowing 50 | ```rust 51 | // Taking ownership - use when you need to consume the value 52 | fn process_and_consume(data: Vec) -> i32 { 53 | data.into_iter().sum() // data is consumed 54 | } 55 | 56 | // Borrowing immutably - use for read-only access 57 | fn calculate_average(numbers: &[i32]) -> f64 { 58 | if numbers.is_empty() { 59 | return 0.0; 60 | } 61 | numbers.iter().sum::() as f64 / numbers.len() as f64 62 | } 63 | 64 | // Borrowing mutably - use when you need to modify 65 | fn normalize_scores(scores: &mut [f64]) { 66 | let max_score = scores.iter() 67 | .max_by(|a, b| a.partial_cmp(b).unwrap()) 68 | .copied() 69 | .unwrap_or(1.0); 70 | 71 | for score in scores.iter_mut() { 72 | *score /= max_score; 73 | } 74 | } 75 | ``` 76 | 77 | ### Generic Functions: Code Reuse Done Right 78 | ```rust 79 | use std::fmt::Display; 80 | 81 | // Generic function with trait bounds 82 | fn print_and_return(value: T) -> T { 83 | println!("Processing: {}", value); 84 | value.clone() 85 | } 86 | 87 | // Multiple generic parameters 88 | fn find_max(a: T, b: T) -> T { 89 | if a > b { a } else { b } 90 | } 91 | 92 | // Generic with where clause for complex bounds 93 | fn process_items(items: Vec, processor: F) -> Vec 94 | where 95 | T: Display, 96 | F: Fn(&T) -> String, 97 | { 98 | items.iter() 99 | .map(|item| processor(item)) 100 | .collect() 101 | } 102 | ``` 103 | 104 | ### Higher-Order Functions and Closures 105 | ```rust 106 | // Function that takes another function 107 | fn apply_operation(values: &[i32], operation: F) -> Vec 108 | where 109 | F: Fn(i32) -> i32, 110 | { 111 | values.iter().map(|&x| operation(x)).collect() 112 | } 113 | 114 | // Using closures with captured variables 115 | fn create_multiplier(factor: i32) -> impl Fn(i32) -> i32 { 116 | move |x| x * factor // 'move' transfers ownership to closure 117 | } 118 | 119 | // Practical example: Configuration-driven processing 120 | fn process_requests(requests: Vec, validator: F) -> Vec> 121 | where 122 | F: Fn(&Request) -> Result<(), String>, 123 | { 124 | requests.into_iter() 125 | .map(|req| { 126 | validator(&req) 127 | .and_then(|_| process_request(req)) 128 | }) 129 | .collect() 130 | } 131 | ``` 132 | 133 | ## 5.3 Module System: Organizing Code for Scale 134 | 135 | ### Basic Module Structure 136 | ```rust 137 | // main.rs or lib.rs 138 | mod network { 139 | pub mod http { 140 | pub fn get(url: &str) -> Result { 141 | // HTTP GET implementation 142 | Ok(format!("Response from {}", url)) 143 | } 144 | 145 | pub fn post(url: &str, data: &str) -> Result { 146 | // HTTP POST implementation 147 | Ok(format!("Posted {} to {}", data, url)) 148 | } 149 | } 150 | 151 | pub mod tcp { 152 | pub struct Connection { 153 | address: String, 154 | } 155 | 156 | impl Connection { 157 | pub fn new(address: String) -> Self { 158 | Self { address } 159 | } 160 | 161 | pub fn send(&self, data: &str) -> Result<(), String> { 162 | println!("Sending '{}' to {}", data, self.address); 163 | Ok(()) 164 | } 165 | } 166 | } 167 | } 168 | 169 | // Usage 170 | use network::http; 171 | use network::tcp::Connection; 172 | 173 | fn main() { 174 | let response = http::get("https://api.example.com").unwrap(); 175 | 176 | let conn = Connection::new("127.0.0.1:8080".to_string()); 177 | conn.send("Hello server!").unwrap(); 178 | } 179 | ``` 180 | 181 | ### File-Based Module Organization 182 | ``` 183 | src/ 184 | ├── main.rs 185 | ├── lib.rs 186 | ├── auth/ 187 | │ ├── mod.rs 188 | │ ├── login.rs 189 | │ └── permissions.rs 190 | ├── database/ 191 | │ ├── mod.rs 192 | │ ├── connection.rs 193 | │ └── migrations.rs 194 | └── api/ 195 | ├── mod.rs 196 | ├── handlers.rs 197 | └── middleware.rs 198 | ``` 199 | 200 | ```rust 201 | // auth/mod.rs 202 | pub mod login; 203 | pub mod permissions; 204 | 205 | pub use login::authenticate_user; 206 | pub use permissions::{Role, check_permission}; 207 | 208 | // auth/login.rs 209 | use crate::database; 210 | 211 | pub fn authenticate_user(username: &str, password: &str) -> Result { 212 | // Authentication logic 213 | database::find_user_by_credentials(username, password) 214 | } 215 | 216 | // main.rs 217 | mod auth; 218 | mod database; 219 | mod api; 220 | 221 | use auth::{authenticate_user, Role}; 222 | 223 | fn main() { 224 | match authenticate_user("alice", "secret123") { 225 | Ok(user) => println!("Welcome, {}!", user.name), 226 | Err(e) => println!("Authentication failed: {:?}", e), 227 | } 228 | } 229 | ``` 230 | 231 | ## 5.4 Error Handling in Functions 232 | 233 | ### Custom Error Types 234 | ```rust 235 | #[derive(Debug)] 236 | enum ProcessingError { 237 | InvalidInput(String), 238 | NetworkError(String), 239 | DatabaseError(String), 240 | } 241 | 242 | impl std::fmt::Display for ProcessingError { 243 | fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result { 244 | match self { 245 | ProcessingError::InvalidInput(msg) => write!(f, "Invalid input: {}", msg), 246 | ProcessingError::NetworkError(msg) => write!(f, "Network error: {}", msg), 247 | ProcessingError::DatabaseError(msg) => write!(f, "Database error: {}", msg), 248 | } 249 | } 250 | } 251 | 252 | impl std::error::Error for ProcessingError {} 253 | 254 | // Functions that return custom errors 255 | fn validate_email(email: &str) -> Result<(), ProcessingError> { 256 | if email.contains('@') { 257 | Ok(()) 258 | } else { 259 | Err(ProcessingError::InvalidInput( 260 | format!("'{}' is not a valid email", email) 261 | )) 262 | } 263 | } 264 | 265 | fn save_user(user: &User) -> Result { 266 | validate_email(&user.email)?; // Propagate validation error 267 | 268 | // Simulated database save 269 | database::save(user) 270 | .map_err(|e| ProcessingError::DatabaseError(e.to_string())) 271 | } 272 | ``` 273 | 274 | ### Result Combinators and Error Chaining 275 | ```rust 276 | fn process_user_registration(data: UserRegistrationData) -> Result { 277 | validate_email(&data.email)?; 278 | 279 | let user = User::new(data.name, data.email); 280 | let user_id = save_user(&user)?; 281 | 282 | send_welcome_email(&user.email) 283 | .map_err(|e| ProcessingError::NetworkError(e.to_string()))?; 284 | 285 | Ok(User { id: Some(user_id), ..user }) 286 | } 287 | 288 | // Chaining operations 289 | fn get_user_profile(user_id: u32) -> Result { 290 | database::find_user(user_id) 291 | .map_err(|e| ProcessingError::DatabaseError(e.to_string()))? 292 | .and_then(|user| { 293 | get_user_preferences(user.id) 294 | .map(|prefs| UserProfile::new(user, prefs)) 295 | }) 296 | } 297 | ``` 298 | 299 | ## 5.5 Testing Functions and Modules 300 | 301 | ### Unit Testing 302 | ```rust 303 | pub fn fibonacci(n: u32) -> u64 { 304 | match n { 305 | 0 => 0, 306 | 1 => 1, 307 | _ => fibonacci(n - 1) + fibonacci(n - 2), 308 | } 309 | } 310 | 311 | #[cfg(test)] 312 | mod tests { 313 | use super::*; 314 | 315 | #[test] 316 | fn test_fibonacci_base_cases() { 317 | assert_eq!(fibonacci(0), 0); 318 | assert_eq!(fibonacci(1), 1); 319 | } 320 | 321 | #[test] 322 | fn test_fibonacci_sequence() { 323 | assert_eq!(fibonacci(2), 1); 324 | assert_eq!(fibonacci(3), 2); 325 | assert_eq!(fibonacci(4), 3); 326 | assert_eq!(fibonacci(5), 5); 327 | } 328 | 329 | #[test] 330 | #[should_panic] 331 | fn test_invalid_input() { 332 | validate_email("invalid-email").unwrap(); 333 | } 334 | } 335 | ``` 336 | 337 | ### Integration Testing 338 | ```rust 339 | // tests/integration_test.rs 340 | use myapp::auth; 341 | use myapp::database; 342 | 343 | #[test] 344 | fn test_user_authentication_flow() { 345 | // Setup test database 346 | let db = database::setup_test_db(); 347 | 348 | // Create test user 349 | let user = database::create_test_user("test@example.com", "password123"); 350 | 351 | // Test authentication 352 | let result = auth::authenticate_user("test@example.com", "password123"); 353 | assert!(result.is_ok()); 354 | 355 | // Cleanup 356 | database::cleanup_test_db(db); 357 | } 358 | ``` 359 | 360 | ## 5.6 Performance and Optimization Patterns 361 | 362 | ### Zero-Cost Abstractions 363 | ```rust 364 | // Generic function that compiles to specific implementations 365 | fn process_data(data: &[T], processor: F) -> Vec 366 | where 367 | T: Clone, 368 | F: Fn(&T) -> T, 369 | { 370 | data.iter() 371 | .map(|item| processor(item)) 372 | .collect() 373 | } 374 | 375 | // Usage - the compiler optimizes this to direct operations 376 | let numbers = vec![1, 2, 3, 4, 5]; 377 | let doubled = process_data(&numbers, |&x| x * 2); 378 | ``` 379 | 380 | ### Avoiding Unnecessary Allocations 381 | ```rust 382 | // Instead of returning String, return &str when possible 383 | fn get_error_message(code: u32) -> &'static str { 384 | match code { 385 | 404 => "Not Found", 386 | 500 => "Internal Server Error", 387 | _ => "Unknown Error", 388 | } 389 | } 390 | 391 | // Use string formatting only when necessary 392 | fn format_response(status: u32, data: &str) -> String { 393 | if status == 200 { 394 | data.to_string() // Only allocate when needed 395 | } else { 396 | format!("Error {}: {}", status, data) 397 | } 398 | } 399 | ``` 400 | 401 | ## 5.7 Real-World Application Architecture 402 | 403 | ### Layered Architecture Example 404 | ```rust 405 | // Domain layer 406 | pub mod domain { 407 | pub struct User { 408 | pub id: Option, 409 | pub email: String, 410 | pub name: String, 411 | } 412 | 413 | pub trait UserRepository { 414 | fn save(&self, user: &User) -> Result; 415 | fn find_by_email(&self, email: &str) -> Result, String>; 416 | } 417 | } 418 | 419 | // Infrastructure layer 420 | pub mod infrastructure { 421 | use crate::domain::{User, UserRepository}; 422 | 423 | pub struct DatabaseUserRepository { 424 | connection: DatabaseConnection, 425 | } 426 | 427 | impl UserRepository for DatabaseUserRepository { 428 | fn save(&self, user: &User) -> Result { 429 | // Database-specific implementation 430 | Ok(123) 431 | } 432 | 433 | fn find_by_email(&self, email: &str) -> Result, String> { 434 | // Database query implementation 435 | Ok(None) 436 | } 437 | } 438 | } 439 | 440 | // Application layer 441 | pub mod application { 442 | use crate::domain::{User, UserRepository}; 443 | 444 | pub struct UserService { 445 | repository: R, 446 | } 447 | 448 | impl UserService { 449 | pub fn new(repository: R) -> Self { 450 | Self { repository } 451 | } 452 | 453 | pub fn register_user(&self, email: String, name: String) -> Result { 454 | // Business logic 455 | let user = User { id: None, email, name }; 456 | let id = self.repository.save(&user)?; 457 | Ok(User { id: Some(id), ..user }) 458 | } 459 | } 460 | } 461 | ``` 462 | 463 | ## Key Takeaways 464 | 465 | 1. **Functions are building blocks** - design them for reuse and clarity 466 | 2. **Use borrowing wisely** - prefer references over ownership when possible 467 | 3. **Generics enable code reuse** - write once, use with many types 468 | 4. **Modules provide organization** - separate concerns and control visibility 469 | 5. **Error handling is part of design** - use custom error types for clarity 470 | 6. **Test your functions** - unit tests catch bugs early 471 | 7. **Layer your architecture** - separate business logic from infrastructure 472 | 473 | ## Practice Projects 474 | 475 | 1. **Calculator Library**: Build a module with different mathematical operations 476 | 2. **Configuration Parser**: Create functions to read and validate config files 477 | 3. **HTTP Client**: Design a modular API client with error handling 478 | 4. **Task Manager**: Build a todo app with proper separation of concerns 479 | 480 | Functions and modules in Rust help you build software that scales—both in terms of performance and maintainability! -------------------------------------------------------------------------------- /04-control-flow.md: -------------------------------------------------------------------------------- 1 | # 4. Control Flow in Action 2 | 3 | Control flow determines how your program makes decisions and repeats actions. In Rust, control flow isn't just about branching and looping—it's about expressing logic safely, handling all possible cases, and letting the compiler catch your mistakes before they become bugs. 4 | 5 | ## What Makes Rust's Control Flow Special? 6 | 7 | Unlike many languages, Rust's control flow constructs are: 8 | - **Exhaustive**: The compiler ensures you handle every possible case 9 | - **Expression-based**: Most control flow returns values, not just executes statements 10 | - **Memory-safe**: No null pointer dereferences or buffer overflows 11 | - **Zero-cost**: The abstractions compile down to efficient machine code 12 | 13 | Think of Rust's control flow as having a very strict, helpful teacher who won't let you turn in incomplete work—and that's exactly what makes your programs robust. 14 | 15 | ## The Control Flow Mindset 16 | 17 | Before diving into syntax, understand the philosophy: 18 | 19 | 1. **Make invalid states unrepresentable** - Use enums and match to model your problem domain 20 | 2. **Let the compiler catch your mistakes** - Embrace exhaustive matching instead of fighting it 21 | 3. **Express intent, not implementation** - Use iterators to say "what" not "how" 22 | 4. **Handle errors explicitly** - No hidden nulls or exceptions to surprise you later 23 | 24 | These principles will guide you to writing Rust code that's not just correct, but maintainable and bug-free. 25 | 26 | ## 4.1 Conditional Logic: Beyond Simple If/Else 27 | 28 | ### The Foundation: If Expressions 29 | ```rust 30 | // In Rust, if is an expression that returns a value 31 | let temperature = 25; 32 | let clothing = if temperature > 20 { "t-shirt" } else { "jacket" }; 33 | ``` 34 | 35 | The key insight: Rust's `if` always returns a value, and both branches must return the same type. This prevents entire classes of bugs. 36 | 37 | ### Pattern-Based Conditions 38 | ```rust 39 | let user_role = "admin"; 40 | let access_level = if user_role == "admin" { 10 } else { 1 }; 41 | 42 | // Better: Express intent clearly 43 | let access_level = match user_role { 44 | "admin" => 10, 45 | "moderator" => 5, 46 | "user" => 1, 47 | _ => 0, // Default for unknown roles 48 | }; 49 | ``` 50 | 51 | ### Conditional Assignment with Safety 52 | ```rust 53 | let temperature = 25; 54 | let status = if temperature > 30 { 55 | "hot" 56 | } else if temperature < 10 { 57 | "cold" 58 | } else { 59 | "comfortable" 60 | }; 61 | 62 | // Rust ensures all branches return the same type 63 | ``` 64 | 65 | ## 4.2 Match: Rust's Superpower 66 | 67 | The `match` expression is where Rust truly shines. It's not just a switch statement—it's a powerful pattern matching system that: 68 | - Forces you to handle every possible case (exhaustive matching) 69 | - Extracts data from complex types safely 70 | - Compiles to highly efficient code 71 | - Makes impossible states unrepresentable 72 | 73 | ### Why Match Instead of If/Else Chains? 74 | 75 | ```rust 76 | // Fragile: Easy to forget cases or make logic errors 77 | let status_message = if user_role == "admin" { 78 | "Full access" 79 | } else if user_role == "moderator" { 80 | "Limited access" 81 | } else if user_role == "user" { 82 | "Basic access" 83 | } else { 84 | "No access" // What if we add a new role and forget to update this? 85 | }; 86 | 87 | // Robust: Compiler ensures we handle every case 88 | enum UserRole { 89 | Admin, 90 | Moderator, 91 | User, 92 | Guest, 93 | } 94 | 95 | let status_message = match user_role { 96 | UserRole::Admin => "Full access", 97 | UserRole::Moderator => "Limited access", 98 | UserRole::User => "Basic access", 99 | UserRole::Guest => "Read-only access", 100 | // If we add a new role, this won't compile until we handle it! 101 | }; 102 | ``` 103 | 104 | ### Exhaustive Pattern Matching 105 | ```rust 106 | enum ConnectionState { 107 | Connected, 108 | Disconnected, 109 | Reconnecting, 110 | Error(String), 111 | } 112 | 113 | fn handle_connection(state: ConnectionState) -> String { 114 | match state { 115 | ConnectionState::Connected => "All good!".to_string(), 116 | ConnectionState::Disconnected => "Attempting to reconnect...".to_string(), 117 | ConnectionState::Reconnecting => "Please wait...".to_string(), 118 | ConnectionState::Error(msg) => format!("Connection failed: {}", msg), 119 | } 120 | // Compiler guarantees we handle every case! 121 | } 122 | ``` 123 | 124 | ### Match Guards and Complex Patterns 125 | ```rust 126 | fn categorize_score(score: u32) -> &'static str { 127 | match score { 128 | 90..=100 => "Excellent", 129 | 80..=89 => "Good", 130 | 70..=79 => "Average", 131 | 60..=69 => "Below Average", 132 | n if n < 60 => "Needs Improvement", 133 | _ => "Invalid Score", 134 | } 135 | } 136 | 137 | // Destructuring in match 138 | fn process_user_data(data: (String, Option, bool)) -> String { 139 | match data { 140 | (name, Some(age), true) => format!("{} is {} years old and active", name, age), 141 | (name, Some(age), false) => format!("{} is {} years old but inactive", name, age), 142 | (name, None, active) => format!("{} age unknown, active: {}", name, active), 143 | } 144 | } 145 | ``` 146 | 147 | ## 4.3 Loops: Iteration Done Right 148 | 149 | Rust's approach to loops prioritizes safety and expressiveness. Instead of manual index manipulation (a common source of bugs), Rust encourages iterator-based loops that are both safer and often faster. 150 | 151 | ### The Rust Way: Iterators Over Indices 152 | 153 | ```rust 154 | let numbers = vec![1, 2, 3, 4, 5]; 155 | 156 | // ❌ C-style: Error-prone, verbose 157 | for i in 0..numbers.len() { 158 | println!("Number: {}", numbers[i]); // Could panic if index is wrong! 159 | } 160 | 161 | // ✅ Rust-style: Safe, clear intent 162 | for num in &numbers { 163 | println!("Number: {}", num); // Cannot go out of bounds 164 | } 165 | ``` 166 | 167 | The iterator approach eliminates: 168 | - Index out of bounds errors 169 | - Off-by-one mistakes 170 | - Complexity of manual index management 171 | 172 | ### For Loops: The Preferred Choice 173 | ```rust 174 | // Iterate over collections 175 | let numbers = vec![1, 2, 3, 4, 5]; 176 | for num in &numbers { // Borrow, don't move 177 | println!("Number: {}", num); 178 | } 179 | 180 | // Iterate with indices when needed 181 | for (index, value) in numbers.iter().enumerate() { 182 | println!("Index {}: {}", index, value); 183 | } 184 | 185 | // Range-based iteration 186 | for i in 0..10 { 187 | println!("Count: {}", i); 188 | } 189 | ``` 190 | 191 | ### While Loops: Condition-Based Iteration 192 | ```rust 193 | let mut retry_count = 0; 194 | let max_retries = 3; 195 | 196 | while retry_count < max_retries { 197 | match attempt_connection() { 198 | Ok(_) => break, 199 | Err(e) => { 200 | retry_count += 1; 201 | println!("Attempt {} failed: {}", retry_count, e); 202 | } 203 | } 204 | } 205 | ``` 206 | 207 | ### Loop with Break Values 208 | ```rust 209 | let mut counter = 0; 210 | let result = loop { 211 | counter += 1; 212 | if counter == 5 { 213 | break counter * 2; // Return value from loop 214 | } 215 | }; 216 | println!("Result: {}", result); // Result: 10 217 | ``` 218 | 219 | ## 4.4 Advanced Pattern Matching Techniques 220 | 221 | ### Option and Result Handling 222 | ```rust 223 | fn safe_divide(a: f64, b: f64) -> Option { 224 | if b != 0.0 { Some(a / b) } else { None } 225 | } 226 | 227 | // Pattern matching with Option 228 | match safe_divide(10.0, 2.0) { 229 | Some(result) => println!("Result: {}", result), 230 | None => println!("Cannot divide by zero"), 231 | } 232 | 233 | // Chaining with if let 234 | if let Some(result) = safe_divide(10.0, 2.0) { 235 | println!("Division successful: {}", result); 236 | } 237 | ``` 238 | 239 | ### Nested Pattern Matching 240 | ```rust 241 | struct User { 242 | name: String, 243 | email: Option, 244 | age: Option, 245 | } 246 | 247 | fn describe_user(user: &User) -> String { 248 | match (&user.email, &user.age) { 249 | (Some(email), Some(age)) => { 250 | format!("{} ({}) - Contact: {}", user.name, age, email) 251 | } 252 | (Some(email), None) => { 253 | format!("{} - Contact: {}", user.name, email) 254 | } 255 | (None, Some(age)) => { 256 | format!("{} ({})", user.name, age) 257 | } 258 | (None, None) => user.name.clone(), 259 | } 260 | } 261 | ``` 262 | 263 | ## 4.5 Error Handling Patterns 264 | 265 | ### Early Returns with Question Mark 266 | ```rust 267 | use std::fs::File; 268 | use std::io::Read; 269 | 270 | fn read_config_file() -> Result { 271 | let mut file = File::open("config.txt")?; // Early return on error 272 | let mut contents = String::new(); 273 | file.read_to_string(&mut contents)?; // Another early return 274 | Ok(contents) 275 | } 276 | 277 | // Handling the result 278 | match read_config_file() { 279 | Ok(config) => println!("Config loaded: {}", config), 280 | Err(e) => eprintln!("Failed to load config: {}", e), 281 | } 282 | ``` 283 | 284 | ### Match vs If Let: Choosing Your Tool 285 | ```rust 286 | // Use match for exhaustive handling 287 | match user_input { 288 | "start" => start_process(), 289 | "stop" => stop_process(), 290 | "pause" => pause_process(), 291 | _ => show_help(), 292 | } 293 | 294 | // Use if let for single-case handling 295 | if let Some(first_item) = items.first() { 296 | process_item(first_item); 297 | } 298 | ``` 299 | 300 | ## 4.6 Practical Patterns: State Machines 301 | 302 | ```rust 303 | #[derive(Debug)] 304 | enum TaskState { 305 | Pending, 306 | InProgress { started_at: u64 }, 307 | Completed { duration: u64 }, 308 | Failed { error: String }, 309 | } 310 | 311 | impl TaskState { 312 | fn transition(&mut self, event: TaskEvent) { 313 | *self = match (self, event) { 314 | (TaskState::Pending, TaskEvent::Start(time)) => { 315 | TaskState::InProgress { started_at: time } 316 | } 317 | (TaskState::InProgress { started_at }, TaskEvent::Complete(end_time)) => { 318 | TaskState::Completed { duration: end_time - started_at } 319 | } 320 | (TaskState::InProgress { .. }, TaskEvent::Fail(error)) => { 321 | TaskState::Failed { error } 322 | } 323 | _ => return, // Invalid transition, ignore 324 | }; 325 | } 326 | } 327 | 328 | enum TaskEvent { 329 | Start(u64), 330 | Complete(u64), 331 | Fail(String), 332 | } 333 | ``` 334 | 335 | ## 4.7 Performance Considerations 336 | 337 | ### Iterator Chains vs Loops 338 | ```rust 339 | let numbers = vec![1, 2, 3, 4, 5, 6, 7, 8, 9, 10]; 340 | 341 | // Functional approach - often faster due to optimizations 342 | let sum: i32 = numbers 343 | .iter() 344 | .filter(|&&x| x % 2 == 0) 345 | .map(|&x| x * x) 346 | .sum(); 347 | 348 | // Traditional loop approach 349 | let mut sum = 0; 350 | for &num in &numbers { 351 | if num % 2 == 0 { 352 | sum += num * num; 353 | } 354 | } 355 | ``` 356 | 357 | ## 4.8 Real-World Example: Simple Todo App 358 | 359 | Let's build a basic todo application that demonstrates the key control flow concepts in a practical way. 360 | 361 | ```rust 362 | // 1. Model the problem with enums - making invalid states impossible 363 | #[derive(Debug, Clone, PartialEq)] 364 | enum TaskStatus { 365 | Todo, 366 | Done, 367 | } 368 | 369 | #[derive(Debug)] 370 | struct Task { 371 | id: u32, 372 | title: String, 373 | status: TaskStatus, 374 | } 375 | 376 | // 2. Simple error handling 377 | #[derive(Debug)] 378 | enum TodoError { 379 | TaskNotFound(u32), 380 | EmptyTitle, 381 | } 382 | 383 | // 3. Main todo manager 384 | struct TodoApp { 385 | tasks: Vec, 386 | next_id: u32, 387 | } 388 | 389 | impl TodoApp { 390 | fn new() -> Self { 391 | Self { 392 | tasks: Vec::new(), 393 | next_id: 1, 394 | } 395 | } 396 | 397 | // Creating tasks with validation 398 | fn add_task(&mut self, title: String) -> Result { 399 | // Early return pattern - check for empty title 400 | if title.trim().is_empty() { 401 | return Err(TodoError::EmptyTitle); 402 | } 403 | 404 | let task = Task { 405 | id: self.next_id, 406 | title, 407 | status: TaskStatus::Todo, 408 | }; 409 | 410 | self.tasks.push(task); 411 | let id = self.next_id; 412 | self.next_id += 1; 413 | Ok(id) 414 | } 415 | 416 | // Pattern matching for state changes 417 | fn toggle_task(&mut self, task_id: u32) -> Result<(), TodoError> { 418 | // Find the task using iterator patterns 419 | let task = self.tasks 420 | .iter_mut() 421 | .find(|task| task.id == task_id) 422 | .ok_or(TodoError::TaskNotFound(task_id))?; 423 | 424 | // Match on current status to toggle 425 | task.status = match task.status { 426 | TaskStatus::Todo => TaskStatus::Done, 427 | TaskStatus::Done => TaskStatus::Todo, 428 | }; 429 | 430 | Ok(()) 431 | } 432 | 433 | // Using iterators to filter and count 434 | fn get_summary(&self) -> String { 435 | let total = self.tasks.len(); 436 | let completed = self.tasks 437 | .iter() 438 | .filter(|task| task.status == TaskStatus::Done) 439 | .count(); 440 | let remaining = total - completed; 441 | 442 | // Conditional logic for user-friendly messages 443 | match (total, remaining) { 444 | (0, _) => "No tasks yet! Add some to get started.".to_string(), 445 | (_, 0) => "🎉 All tasks completed! Great job!".to_string(), 446 | (_, 1) => "1 task remaining".to_string(), 447 | (_, n) => format!("{} tasks remaining", n), 448 | } 449 | } 450 | 451 | // Display all tasks using iteration and pattern matching 452 | fn display_tasks(&self) { 453 | if self.tasks.is_empty() { 454 | println!("No tasks to display."); 455 | return; 456 | } 457 | 458 | println!("📋 Your Tasks:"); 459 | for task in &self.tasks { 460 | let status_icon = match task.status { 461 | TaskStatus::Todo => "⏳", 462 | TaskStatus::Done => "✅", 463 | }; 464 | println!(" {} {} (ID: {})", status_icon, task.title, task.id); 465 | } 466 | println!("\n{}", self.get_summary()); 467 | } 468 | } 469 | 470 | // 4. Demo function showing everything working together 471 | fn demo_todo_app() { 472 | let mut app = TodoApp::new(); 473 | 474 | // Add some tasks 475 | match app.add_task("Learn Rust".to_string()) { 476 | Ok(id) => println!("✅ Added task with ID: {}", id), 477 | Err(e) => println!("❌ Failed to add task: {:?}", e), 478 | } 479 | 480 | app.add_task("Write code".to_string()).unwrap(); 481 | app.add_task("Deploy app".to_string()).unwrap(); 482 | 483 | // Display initial state 484 | app.display_tasks(); 485 | 486 | // Complete some tasks 487 | println!("\n📝 Completing first task..."); 488 | if let Err(e) = app.toggle_task(1) { 489 | println!("Error: {:?}", e); 490 | } 491 | 492 | app.display_tasks(); 493 | 494 | // Try to complete non-existent task (error handling) 495 | println!("\n🚫 Trying to toggle non-existent task..."); 496 | match app.toggle_task(999) { 497 | Ok(_) => println!("Task toggled successfully"), 498 | Err(TodoError::TaskNotFound(id)) => println!("Task {} not found!", id), 499 | Err(e) => println!("Other error: {:?}", e), 500 | } 501 | } 502 | 503 | // Usage: demo_todo_app(); 504 | ``` 505 | 506 | ### What This Simplified Example Shows: 507 | 508 | 1. **Enum for State**: `TaskStatus` prevents invalid states (no "maybe done" tasks!) 509 | 2. **Pattern Matching**: 510 | - `match` for toggling task status 511 | - `match` for generating user-friendly messages 512 | 3. **Error Handling**: Simple `Result` types with meaningful errors 513 | 4. **Iterator Patterns**: Using `.filter()` and `.find()` instead of manual loops 514 | 5. **Conditional Logic**: `if/else` and `match` for different scenarios 515 | 516 | ### Key Control Flow Patterns: 517 | 518 | - **Early Returns**: Check for errors first, continue with valid data 519 | - **Exhaustive Matching**: Handle every possible task status 520 | - **Safe Iteration**: No index errors, just expressive operations 521 | - **Error Propagation**: Use `?` and `match` to handle failures gracefully 522 | 523 | This example is much simpler but still demonstrates how Rust's control flow patterns work together to create reliable, maintainable code. The compiler ensures we handle all cases and catch errors early! 524 | 525 | ## Key Takeaways 526 | 527 | 1. **Match is exhaustive** - the compiler ensures you handle all cases 528 | 2. **Pattern matching prevents bugs** - no forgotten edge cases 529 | 3. **Early returns** with `?` make error handling clean 530 | 4. **For loops are preferred** over manual indexing 531 | 5. **State machines** with enums create robust, maintainable code 532 | 6. **Iterator chains** are both expressive and performant 533 | 534 | Control flow in Rust guides you toward writing code that's both safe and expressive—the compiler won't let you forget edge cases! 535 | 536 | ## Practice Exercises 537 | 538 | 1. **Traffic Light State Machine**: Create an enum for traffic light states and implement transitions 539 | 2. **User Authentication Flow**: Use match to handle different authentication states 540 | 3. **File Processing Pipeline**: Chain iterators to process and filter file data 541 | 4. **Retry Logic**: Implement exponential backoff using loops and error handling 542 | 543 | Try building these patterns—they're the foundation of robust Rust applications! --------------------------------------------------------------------------------