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. 💡
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
returnneeded? The final expression’s value becomes the function’s output, promoting concise, clean code. - Note: You can still use the
returnkeyword 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;(Assigns5tox, but doesn’t return the value5itself)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 to8)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 }returns5. ifBlocks:if,match, andloopblocks 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
breakstatement to exit, preventing truly endless execution. It can evenbreakwith a value, making it useful for simplematchscenarios.
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;
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
xis less thany”). - 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;
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
Iteratortrait, 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-onemistakes or goingout-of-boundsbecause 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 yourvalue(of typeT).Err(error): Something went wrong! Here’s theerror(of typeE).
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 theOkvalue or panics (crashes) if it’sErr.expect("message"): Likeunwrap(), 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 ifSome, otherwise provides a default you specify.- Example:
let user_name = maybe_name.unwrap_or("Guest");
- Example:
map: Transforms the value insideSome, leavingNoneuntouched. Great for changing types!- Example:
let next_age = age_option.map(|a| a + 1);
- Example:
and_then: Chains operations that also return anOption. If any step isNone, the chain stops.- Example:
user_id_option.and_then(|id| get_user_details(id))
- Example:
- Pattern Matching (
match): The most robust way to handleOption<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
- Create a
Taskstruct with:id: u32title: Stringcompleted: boolpriority: Priority(enum: Low, Medium, High)
- Create a
TaskManagerstruct managingVec<Task> - 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>
- Create
TaskFilterenum:All,Completed,Pending,ByPriority(Priority) - Implement pattern matching for:
- Filtering tasks based on
TaskFilter - CLI command parsing (add, complete, delete, list)
- Priority comparison
- Filtering tasks based on
- Use control flow:
forloop to iterate over taskswhileloop for CLI input processingif letfor Option unwrapping
- Implement proper error handling:
- Return
Resultfor operations that can fail - Return
Optionfor lookups - Use
?operator for error propagation
- Return
- Write unit tests for all functions
💡 Implementation Hints
- Use
Vec::iter()andVec::iter_mut()for safe iteration - Use
Vec::retain()for filtering tasks - Use
matchexpressions for CLI command parsing - Implement
Displaytrait for pretty printing tasks - Use
Result::map()andOption::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
clapcrate 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.