Post

03. Functions and Control Flow

🚀 Master the building blocks of robust Rust applications! This post guides you through defining functions, understanding control flow with if/match expressions and various loops, and effectively handling errors with Result and Option types. 💡

03. Functions and Control Flow

What we will learn in this post?

  • 👉 Defining Functions in Rust
  • 👉 Statements vs Expressions
  • 👉 If Expressions and Pattern Matching
  • 👉 Loops in Rust - loop, while, for
  • 👉 Match Expression and Pattern Matching
  • 👉 Error Handling with Result
  • 👉 Option Type for Nullable Values

Rust Functions: Your Code’s Building Blocks! 🚀

In Rust, functions are central to organizing your code. They help you perform specific tasks. Let’s break down their syntax.

Defining Functions with fn and Parameters ⚙️

You start defining a function with the fn keyword. After the function’s name, you list its parameters inside parentheses. Each parameter needs a name followed by a colon : and then its type. This tells Rust exactly what kind of input your function expects.

1
2
3
fn greet_person(name: &str, age: u32) { // 'name' is a string slice, 'age' is a 32-bit unsigned integer
    println!("Hello, {}! You are {} years old.", name, age);
}

Here, greet_person takes a name (a string slice) and an age (a 32-bit unsigned integer).

Return Types & Expression-Based Return ✨

To specify what a function will give back, use -> followed by the return type. What’s neat about Rust is its expression-based nature. The last expression in a function’s body (meaning, without a semicolon ; at the end) is automatically returned. This often means you don’t need a return keyword at the very end!

1
2
3
fn add_numbers(a: i32, b: i32) -> i32 { // This function explicitly returns an i32
    a + b // This is an expression. Its value (the sum) is automatically returned. No semicolon!
}
  • Why no return needed? The final expression’s value becomes the function’s output, promoting concise, clean code.
  • Note: You can still use the return keyword for early exits from a function.

Function Execution Flow 💡

graph TD
    A["🚀 Start Function Call"]:::style1 --> B{"✅ Parameters Valid?"}:::style2
    B -- "Yes" --> C["⚙️ Execute Body"]:::style3
    C --> D{"📝 Last Line Expression?"}:::style4
    D -- "Yes" --> E["🎯 Return Value"]:::style5
    D -- "No (has ;)" --> F["⭕ Return Unit ()"]:::style6
    E --> G["🏁 Function Ends"]:::style7
    F --> G
    
    classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style4 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style5 fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style6 fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style7 fill:#9e9e9e,stroke:#616161,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    
    linkStyle default stroke:#e67e22,stroke-width:3px;

Rust’s Power Duo: Statements vs. Expressions ✨

In Rust, think of it like cooking! Some steps just do something (statements), while others produce an ingredient or result (expressions).

Statements: Just Do It! 📝

These are actions that don’t return a value. They’re like an instruction: “Add salt to the pot.” They typically end with a semicolon ;.

  • let x = 5; (Assigns 5 to x, but doesn’t return the value 5 itself)
  • println!("Hello!"); (Prints, doesn’t return anything meaningful)

Expressions: They Give Back! 🎁

Expressions evaluate to a value. They’re like tasting the soup: “How salty is it?” — you get a taste (a value). They don’t end with a semicolon when their value is used.

  • 5 + 3 (evaluates to 8)
  • if condition { "good" } else { "bad" } (evaluates to "good" or "bad")

Impact on Code Magic 💡

This distinction makes Rust code concise and powerful:

  • Function Bodies: The last expression in a function (without a ;) is automatically its return value. E.g., fn get_five() -> i32 { 5 } returns 5.
  • if Blocks: if, match, and loop blocks can be expressions, allowing you to assign their outcome directly: let status = if passed { "Success" } else { "Failed" };.

This design encourages clear, functional code that directly produces values.

graph TD
    A[Code Line] --> B{Ends with ;?};
    B -- Yes --> C[Statement: Action, No Value];
    B -- No --> D[Expression: Evaluates to Value];
    D --> E[Can be Returned or Assigned];

Rust’s Superpower: if Expressions & if let! ✨

Rust’s if isn’t just a simple statement; it’s a powerful expression that can return a value! This makes your code cleaner and more functional. Let’s dive in!

if as a Value-Returning Expression 💡

In Rust, if blocks evaluate to a value, just like many other expressions. The value returned is the last expression in the if or else block (without a semicolon!). Crucially, both the if and else branches must return the same type.

1
2
3
4
5
6
7
let number = 7;
let description = if number % 2 == 0 {
    "Even" // This string slice is returned if true!
} else {
    "Odd"  // This string slice is returned if false!
};
println!("The number {} is {}", number, description); // Output: The number 7 is Odd

Here, description directly gets “Even” or “Odd” based on the condition.

if let for Slick Pattern Matching 🧐

if let is a concise way to handle a single pattern when you don’t need a full match statement. It’s often used to gracefully extract values from Option or Result types.

1
2
3
4
5
6
let config_value: Option<u8> = Some(42);
if let Some(value) = config_value {
    println!("Configuration found: {}", value);
} else { // You can add an optional 'else' for other cases
    println!("No configuration found.");
}

How it Differs from Traditional if 🛤️

Traditional if is primarily a statement that conditionally executes code. Rust’s if is an expression that evaluates to a value. This design promotes safer, more explicit code by requiring all branches to yield a value of the same type, preventing common bugs related to uninitialized variables or type mismatches.

graph TD
    A["🚀 Start If Expression"]:::style1 --> B{"❓ Condition True?"}:::style2
    B -- "Yes" --> C["✅ Execute 'if'"]:::style3
    B -- "No" --> D["❌ Execute 'else'"]:::style4
    C --> E["📦 Value from Block"]:::style5
    D --> E
    E --> F["💾 Assign to Variable"]:::style6
    F --> G["🏁 End"]:::style7
    
    classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style4 fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style5 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style6 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style7 fill:#9e9e9e,stroke:#616161,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    
    linkStyle default stroke:#e67e22,stroke-width:3px;

Rust’s Awesome Loop Types! 🚀

Rust offers three powerful loop types, each designed for specific scenarios, ensuring both flexibility and safety in your code. Let’s explore them!

1. The loop Loop (Infinite Power!) 💪

The loop keyword creates an infinite loop that runs forever unless you explicitly stop it.

  • When to use: Perfect for tasks that need to run continuously, like retrying an operation until it succeeds, or a main game loop that constantly updates.
  • Safety: You must use a break statement to exit, preventing truly endless execution. It can even break with a value, making it useful for simple match scenarios.
1
2
3
4
5
6
7
8
let mut count = 0;
let result = loop { // Starts an infinite loop
    count += 1;
    if count == 3 {
        break count * 2; // Exits loop and returns '6'
    }
}; // result will be 6
println!("Loop result: {}", result); 
graph TD
    A["🔄 Start Loop"]:::style1 --> B{"✅ Condition Met?"}:::style2
    B -- "No" --> A
    B -- "Yes" --> C["🛑 Break & Exit"]:::style3
    C --> D["➡️ Continue Program"]:::style4
    
    classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style4 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    
    linkStyle default stroke:#e67e22,stroke-width:3px;

More on `loop`

2. The while Loop (Conditional Guardian!) 🛡️

The while loop executes its block only as long as a specified boolean condition remains true.

  • When to use: Ideal when you don’t know the exact number of iterations, but you have a clear stopping condition (e.g., “keep processing while there are items,” or “run while x is less than y”).
  • Safety: The condition-driven nature makes it inherently safe, ensuring the loop terminates naturally once the condition becomes false, reducing error-prone manual index management.
1
2
3
4
5
6
let mut number = 3;
while number != 0 { // Loop as long as 'number' is not zero
    println!("{}!", number);
    number -= 1; // Decrement 'number' in each iteration
}
println!("LIFTOFF!!!");
graph TD
    A["🚀 Start"]:::style1 --> B{"❓ Condition True?"}:::style2
    B -- "Yes" --> C["⚙️ Execute Body"]:::style3
    C --> B
    B -- "No" --> D["🏁 Exit Loop"]:::style4
    
    classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style4 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    
    linkStyle default stroke:#e67e22,stroke-width:3px;

More on `while`

3. The for Loop (Iterator’s Friend!) 🤝

The for loop is Rust’s standard way to iterate safely over items in a collection or a range.

  • When to use: Your go-to for iterating over anything that implements the Iterator trait, including number ranges (1..5, 1..=5), vectors, arrays, hash maps, etc.
  • Safety: This is Rust’s safest loop for collections! It eliminates common programming errors like off-by-one mistakes or going out-of-bounds because it iterates directly over the items themselves, not indices.
1
2
3
4
5
6
7
8
9
let numbers = [10, 20, 30, 40];
for num in numbers.iter() { // Iterates over each number safely
    println!("The value is: {}", num);
}

// Or for a range of numbers (1 up to and including 3)
for i in 1..=3 { 
    println!("Range number: {}", i);
}
graph TD
    A["🚀 Start"]:::style1 --> B{"📋 More Items?"}:::style2
    B -- "Yes" --> C["📥 Get Next Item"]:::style3
    C --> D["⚙️ Process Item"]:::style4
    D --> B
    B -- "No" --> E["🏁 End Loop"]:::style5
    
    classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#ffd700,stroke:#d99120,color:#222,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style4 fill:#00bfae,stroke:#005f99,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style5 fill:#9e9e9e,stroke:#616161,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    
    linkStyle default stroke:#e67e22,stroke-width:3px;

Each loop type empowers you to write efficient, readable, and safe Rust code for various control flow needs!


🎯 Real-World Example: HTTP Request Router with Pattern Matching

Pattern matching is heavily used in web frameworks to route HTTP requests!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
enum HttpMethod {
    Get,
    Post,
    Put,
    Delete,
}

enum Route {
    Home,
    User(u64),
    Post { id: u64, slug: String },
    NotFound,
}

fn handle_request(method: HttpMethod, path: &str) -> String {
    let route = parse_route(path);
    
    match (method, route) {
        (HttpMethod::Get, Route::Home) => {
            "<html><h1>Welcome Home!</h1></html>".to_string()
        }
        (HttpMethod::Get, Route::User(id)) => {
            format!("<html>User Profile: {}</html>", id)
        }
        (HttpMethod::Get, Route::Post { id, slug }) => {
            format!("<html>Post #{}: {}</html>", id, slug)
        }
        (HttpMethod::Post, Route::User(_)) => {
            "User created successfully!".to_string()
        }
        (HttpMethod::Delete, Route::Post { id, .. }) => {
            format!("Post {} deleted", id)
        }
        _ => "404 Not Found".to_string(),
    }
}

fn parse_route(path: &str) -> Route {
    match path {
        "/" => Route::Home,
        path if path.starts_with("/user/") => {
            let id = path.strip_prefix("/user/")
                .and_then(|s| s.parse().ok())
                .unwrap_or(0);
            Route::User(id)
        }
        _ => Route::NotFound,
    }
}

fn main() {
    println!("{}", handle_request(HttpMethod::Get, "/"));
    println!("{}", handle_request(HttpMethod::Get, "/user/42"));
}

// This pattern is used in Actix-web, Rocket, and Axum!

Hey there, future Rustacean! 👋 Let’s dive into Rust’s super cool way of handling errors: the Result<T, E> type. It’s all about making your code robust and reliable without exceptions!

Understanding Rust’s Result ✨

Rust uses Result<T, E> to tell you if an operation succeeded or failed. It’s like a special box that either holds the successful value (T) or the error (E). This way, your code must acknowledge potential failures!

What is Result<T, E>? 🤔

Result is an enum with two variants:

  • Ok(value): Everything went well! Here’s your value (of type T).
  • Err(error): Something went wrong! Here’s the error (of type E).

Think of it as a function’s promise: “I’ll give you T if I succeed, or E if I don’t.”

1
2
3
4
5
6
7
8
// A function that might fail (e.g., division by zero)
fn safe_divide(numerator: f64, denominator: f64) -> Result<f64, String> {
    if denominator == 0.0 {
        Err("Cannot divide by zero!".to_string()) // Returns an Err variant
    } else {
        Ok(numerator / denominator) // Returns an Ok variant with the result
    }
}

The ? Operator for Error Propagation 🚀

The ? operator is your best friend for concisely passing errors up the call stack. If a Result is Err, ? immediately returns that Err from the current function. If it’s Ok, it unwraps the value and lets you continue!

1
2
3
4
fn process_and_display(a: f64, b: f64) -> Result<String, String> {
    let result = safe_divide(a, b)?; // If safe_divide returns Err, this function immediately returns Err
    Ok(format!("Result is: {}", result)) // Otherwise, continue with the Ok value
}

Here’s how ? works visually:

graph TD
    A["📦 Function Returns Result"]:::style1 --> B{"❓ Is Result Err?"}:::style2
    B -- "Yes" --> C["🔙 Return Err Immediately"]:::style3
    B -- "No (It's Ok)" --> D["✅ Unwrap Ok & Continue"]:::style4
    
    classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#6b5bff,stroke:#4a3f6b,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#ff9800,stroke:#f57c00,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style4 fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    
    linkStyle default stroke:#e67e22,stroke-width:3px;

unwrap() and expect() for Quick Prototyping 🧪

For quick prototyping or situations where you’re absolutely certain an error won’t happen (be careful!), unwrap() and expect() let you grab the Ok value.

  • unwrap(): Returns the Ok value or panics (crashes) if it’s Err.
  • expect("message"): Like unwrap(), but lets you provide a custom panic message.
1
2
3
4
5
6
7
8
fn main() {
    let success = safe_divide(10.0, 2.0).unwrap(); // This is Ok(5.0), so 'success' is 5.0
    println!("Success: {}", success);

    // This will cause the program to panic! (crash)
    // let failure = safe_divide(10.0, 0.0).expect("Oh no, division failed!"); 
    // println!("Failure: {}", failure); // This line will never be reached
}

Important: Avoid unwrap()/expect() in production code where errors are expected or could lead to crashes. Use pattern matching or the ? operator instead!

Bye-Bye Null Pointers! 👋

Ever encountered a program crashing because something was “null” or “empty” when it wasn’t supposed to be? That’s the infamous ‘billion dollar mistake’ – a common source of bugs! Rust tackles this head-on with its clever Option<T> enum.

Meet Rust’s Option Enum 🛡️</span>

Instead of null (or nil), Rust uses Option<T>, which explicitly tells you if a value might be missing. It can be one of two things:

  • Some(value): “Yes, there’s a value here!” For instance, Some("Alice") for a user’s name.
  • None: “Nope, nothing here!” Like when a user doesn’t have a middle name.

This design forces you, the programmer, to explicitly acknowledge and handle both possibilities. No more forgetting to check!

graph TD
    A["❓ Does Value Exist?"]:::style1 --> B["✅ Some(value)"]:::style2
    A --> C["❌ None"]:::style3
    
    classDef style1 fill:#ff4f81,stroke:#c43e3e,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style2 fill:#43e97b,stroke:#38f9d7,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    classDef style3 fill:#9e9e9e,stroke:#616161,color:#fff,font-size:16px,stroke-width:3px,rx:14,shadow:6px;
    
    linkStyle default stroke:#e67e22,stroke-width:3px;

Smart Ways to Handle Options ✨

Rust provides safe and expressive methods to work with Option<T>:

  • unwrap_or: Safely gets the value if Some, otherwise provides a default you specify.
    • Example: let user_name = maybe_name.unwrap_or("Guest");
  • map: Transforms the value inside Some, leaving None untouched. Great for changing types!
    • Example: let next_age = age_option.map(|a| a + 1);
  • and_then: Chains operations that also return an Option. If any step is None, the chain stops.
    • Example: user_id_option.and_then(|id| get_user_details(id))
  • Pattern Matching (match): The most robust way to handle Option<T> and different outcomes.
    1
    2
    3
    4
    
    match maybe_data {
        Some(data) => println!("Got: {}", data),
        None => println!("No data found!"),
    }
    

Preventing the “Billion Dollar Mistake” 💰

By making None a first-class citizen that must be handled, Rust’s Option<T> eliminates the possibility of unexpected null dereferences. You’re guaranteed at compile time that you’ve thought about the absence of a value, leading to more robust and crash-free software. It brings safety right into the type system!


🎯 Real-World Example: File Processing Pipeline with Error Handling

Production file processing with comprehensive error handling using Result and Option!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
use std::fs;
use std::io;
use std::path::Path;

#[derive(Debug)]
enum ProcessError {
    FileNotFound(String),
    InvalidFormat(String),
    EmptyContent,
    IoError(io::Error),
}

impl From<io::Error> for ProcessError {
    fn from(error: io::Error) -> Self {
        ProcessError::IoError(error)
    }
}

struct FileProcessor {
    input_path: String,
    min_lines: usize,
}

impl FileProcessor {
    fn new(path: &str, min_lines: usize) -> Self {
        FileProcessor {
            input_path: path.to_string(),
            min_lines,
        }
    }
    
    fn process(&self) -> Result<ProcessedData, ProcessError> {
        // Check if file exists
        if !Path::new(&self.input_path).exists() {
            return Err(ProcessError::FileNotFound(self.input_path.clone()));
        }
        
        // Read file content
        let content = fs::read_to_string(&self.input_path)?;
        
        // Validate content
        if content.trim().is_empty() {
            return Err(ProcessError::EmptyContent);
        }
        
        // Process lines
        let lines: Vec<String> = content.lines()
            .filter(|line| !line.trim().is_empty())
            .map(|line| line.trim().to_string())
            .collect();
        
        if lines.len() < self.min_lines {
            return Err(ProcessError::InvalidFormat(
                format!("Expected at least {} lines, got {}", self.min_lines, lines.len())
            ));
        }
        
        Ok(ProcessedData {
            line_count: lines.len(),
            word_count: self.count_words(&lines),
            lines,
        })
    }
    
    fn count_words(&self, lines: &[String]) -> usize {
        lines.iter()
            .map(|line| line.split_whitespace().count())
            .sum()
    }
}

#[derive(Debug)]
struct ProcessedData {
    line_count: usize,
    word_count: usize,
    lines: Vec<String>,
}

fn main() {
    let processor = FileProcessor::new("data.txt", 5);
    
    match processor.process() {
        Ok(data) => {
            println!("✅ Processing successful!");
            println!("Lines: {}, Words: {}", data.line_count, data.word_count);
        }
        Err(ProcessError::FileNotFound(path)) => {
            eprintln!("❌ Error: File not found: {}", path);
        }
        Err(ProcessError::InvalidFormat(msg)) => {
            eprintln!("❌ Error: Invalid format - {}", msg);
        }
        Err(ProcessError::EmptyContent) => {
            eprintln!("❌ Error: File is empty");
        }
        Err(ProcessError::IoError(e)) => {
            eprintln!("❌ I/O Error: {}", e);
        }
    }
}

// This pattern is used in CLI tools, log parsers, and data pipelines!

🎯 Real-World Example: State Machine with Match Expression

Building a robust state machine for a vending machine using pattern matching!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
#[derive(Debug, Clone, Copy, PartialEq)]
enum VendingState {
    Idle,
    CoinInserted(u32),
    ProductSelected { amount_paid: u32, product_price: u32 },
    Dispensing,
}

#[derive(Debug)]
enum VendingAction {
    InsertCoin(u32),
    SelectProduct(u32),
    Cancel,
    Dispense,
}

struct VendingMachine {
    state: VendingState,
}

impl VendingMachine {
    fn new() -> Self {
        VendingMachine {
            state: VendingState::Idle,
        }
    }
    
    fn process(&mut self, action: VendingAction) -> Result<String, String> {
        match (&self.state, action) {
            // From Idle state
            (VendingState::Idle, VendingAction::InsertCoin(amount)) => {
                self.state = VendingState::CoinInserted(amount);
                Ok(format!("💰 Coin inserted: ${}", amount))
            }
            
            // From CoinInserted state
            (VendingState::CoinInserted(current), VendingAction::InsertCoin(more)) => {
                self.state = VendingState::CoinInserted(current + more);
                Ok(format!("💰 Total inserted: ${}", current + more))
            }
            
            (VendingState::CoinInserted(amount), VendingAction::SelectProduct(price)) => {
                if *amount >= price {
                    self.state = VendingState::ProductSelected {
                        amount_paid: *amount,
                        product_price: price,
                    };
                    Ok(format!("🎯 Product selected (Price: ${})", price))
                } else {
                    Err(format!("❌ Insufficient funds: need ${}, have ${}", price, amount))
                }
            }
            
            // From ProductSelected state
            (VendingState::ProductSelected { amount_paid, product_price }, VendingAction::Dispense) => {
                let change = amount_paid - product_price;
                self.state = VendingState::Dispensing;
                Ok(format!("✅ Dispensing! Change: ${}", change))
            }
            
            // Dispensing completes
            (VendingState::Dispensing, _) => {
                self.state = VendingState::Idle;
                Ok("🔄 Ready for next transaction".to_string())
            }
            
            // Cancel from any state with coins
            (VendingState::CoinInserted(amount), VendingAction::Cancel) |
            (VendingState::ProductSelected { amount_paid: amount, .. }, VendingAction::Cancel) => {
                self.state = VendingState::Idle;
                Ok(format!("🔙 Transaction canceled. Refunding: ${}", amount))
            }
            
            // Invalid transitions
            _ => Err("❌ Invalid action for current state".to_string()),
        }
    }
}

fn main() {
    let mut machine = VendingMachine::new();
    
    // Happy path
    println!("{:?}", machine.process(VendingAction::InsertCoin(5)));
    println!("{:?}", machine.process(VendingAction::InsertCoin(3)));
    println!("{:?}", machine.process(VendingAction::SelectProduct(7)));
    println!("{:?}", machine.process(VendingAction::Dispense));
    
    println!("\nFinal state: {:?}", machine.state);
}

// This pattern is used in embedded systems, IoT devices, and protocol handlers!

🎯 Hands-On Assignment: Build a CLI Task Manager with Full Error Handling 🚀

📝 Your Mission

Build a production-ready CLI task manager demonstrating functions, control flow, pattern matching, and comprehensive error handling with Result and Option types!

🎯 Requirements

  1. Create a Task struct with:
    • id: u32
    • title: String
    • completed: bool
    • priority: Priority (enum: Low, Medium, High)
  2. Create a TaskManager struct managing Vec<Task>
  3. Implement these functions:
    • add_task(&mut self, title: String, priority: Priority) -> Result<u32, String>
    • complete_task(&mut self, id: u32) -> Result<(), String>
    • delete_task(&mut self, id: u32) -> Result<Task, String>
    • find_task(&self, id: u32) -> Option<&Task>
    • list_tasks(&self, filter: TaskFilter) -> Vec<&Task>
  4. Create TaskFilter enum: All, Completed, Pending, ByPriority(Priority)
  5. Implement pattern matching for:
    • Filtering tasks based on TaskFilter
    • CLI command parsing (add, complete, delete, list)
    • Priority comparison
  6. Use control flow:
    • for loop to iterate over tasks
    • while loop for CLI input processing
    • if let for Option unwrapping
  7. Implement proper error handling:
    • Return Result for operations that can fail
    • Return Option for lookups
    • Use ? operator for error propagation
  8. Write unit tests for all functions

💡 Implementation Hints

  1. Use Vec::iter() and Vec::iter_mut() for safe iteration
  2. Use Vec::retain() for filtering tasks
  3. Use match expressions for CLI command parsing
  4. Implement Display trait for pretty printing tasks
  5. Use Result::map() and Option::map() for transformations

🚀 Example Input/Output

#[derive(Debug, Clone, Copy, PartialEq)]
enum Priority {
    Low,
    Medium,
    High,
}

#[derive(Debug)]
struct Task {
    id: u32,
    title: String,
    completed: bool,
    priority: Priority,
}

enum TaskFilter {
    All,
    Completed,
    Pending,
    ByPriority(Priority),
}

struct TaskManager {
    tasks: Vec<Task>,
    next_id: u32,
}

impl TaskManager {
    fn new() -> Self {
        TaskManager {
            tasks: Vec::new(),
            next_id: 1,
        }
    }
    
    fn add_task(&mut self, title: String, priority: Priority) -> Result<u32, String> {
        if title.trim().is_empty() {
            return Err("Task title cannot be empty".to_string());
        }
        
        let id = self.next_id;
        self.tasks.push(Task {
            id,
            title,
            completed: false,
            priority,
        });
        self.next_id += 1;
        
        Ok(id)
    }
    
    fn find_task(&self, id: u32) -> Option<&Task> {
        self.tasks.iter().find(|task| task.id == id)
    }
    
    fn list_tasks(&self, filter: TaskFilter) -> Vec<&Task> {
        self.tasks.iter().filter(|task| {
            match filter {
                TaskFilter::All => true,
                TaskFilter::Completed => task.completed,
                TaskFilter::Pending => !task.completed,
                TaskFilter::ByPriority(p) => task.priority == p,
            }
        }).collect()
    }
}

fn main() {
    let mut manager = TaskManager::new();
    
    // Add tasks
    match manager.add_task("Write Rust code".to_string(), Priority::High) {
        Ok(id) => println!("✅ Task added with ID: {}", id),
        Err(e) => println!("❌ Error: {}", e),
    }
    
    // List high priority tasks
    let high_priority = manager.list_tasks(TaskFilter::ByPriority(Priority::High));
    println!("\n🔥 High Priority Tasks: {}", high_priority.len());
    
    for task in high_priority {
        println!("  - [{}] {}", task.id, task.title);
    }
}

🏆 Bonus Challenges

  • Level 2: Add due_date: Option<String> and filter by overdue tasks
  • Level 3: Implement file persistence (save/load from JSON)
  • Level 4: Add undo/redo functionality using a command pattern
  • Level 5: Implement task dependencies (task B depends on task A)
  • Level 6: Add full CLI with clap crate for argument parsing

📚 Learning Goals

  • Master function definitions and return types 🎯
  • Apply pattern matching with match expressions ✨
  • Use loops effectively (for, while, loop) 🔄
  • Implement robust error handling with Result 🛡️
  • Handle optional values with Option 💡
  • Build production-ready CLI applications 🚀

💡 Pro Tip: This task manager pattern is the foundation for tools like cargo, git CLI, and production task schedulers!

Share Your Solution! 💬

Completed the project? Post your code in the comments below! Show us your Rust control flow mastery! 🦀✨


Conclusion: Master Control Flow for Robust Rust Applications 🎓

Rust’s powerful functions, expressive control flow, exhaustive pattern matching, and type-safe error handling with Result and Option enable you to build production-grade applications that are both safe and performant. By mastering these fundamental concepts, you’ll write code that’s not only fast but also maintainable, testable, and resilient to errors – the hallmarks of professional Rust development powering systems from web servers to embedded devices.

This post is licensed under CC BY 4.0 by the author.