Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
31: Combinators on Option and Result
I've spent a lot of time reviewing PRs where the author is treating Option and Result like they're just fancy wrappers that you have to "unpack" using a match statement every single time. While match is the gold standard for exhaustiveness, using it for every single transformation leads to what I call "The Pyramid of Doom"—where your actual logic is indented so far to the right that you're running out of screen real estate.
The Nested Match Trap
Let's look at a real-world scenario: imagine you're building a configuration loader. You need to find a user in a database, check if they have a custom theme setting, and if they do, parse that setting into a Theme enum. If anything fails along the way, you just want to return a default theme.
fn get_user_theme(user_id: u32) -> Theme {
match find_user(user_id) {
Some(user) => {
match user.theme_preference {
Some(pref_string) => {
match Theme::from_str(pref_string) {
Ok(theme) => theme,
Err(_) => Theme::Default,
}
},
None => Theme::Default,
}
},
None => Theme::Default,
}
}
This works, and it's explicit. But look at the signal-to-noise ratio. We have three levels of nesting just to perform a linear sequence of operations. If you add one more check—say, verifying the theme is actually supported by the current version of the app—you're suddenly staring at a wall of curly braces. It's tedious to write and, surprisingly, harder to read because the "happy path" is buried deep inside the nesting.
Flattening the Logic with Combinators
In Rust, we can treat Option and Result as functors (don't worry about the category theory term; just think of them as containers). Instead of pulling the value out to work on it, we send the logic into the container using combinators. The two you'll use 90% of the time are .map() and .and_then().
The distinction is critical: .map() transforms the value inside the container, but it keeps the container type the same. .and_then() (often called flatMap in other languages) is used when the transformation function itself returns an Option or Result. If you used .map() for something that returns an Option, you'd end up with a nested Option<Option<T>>, which is almost never what you want.
fn get_user_theme(user_id: u32) -> Theme {
find_user(user_id)
.and_then(|user| user.theme_preference) // Returns Option<String>
.and_then(|pref| Theme::from_str(pref).ok()) // Convert Result to Option, then flatten
.unwrap_or(Theme::Default)
}
Look at how the cognitive load changes. We've turned a branching tree into a pipeline. We start with a user, we drill down into their preference, we try to parse it, and we provide a fallback. Each step only happens if the previous step succeeded. If find_user returns None, the rest of the chain is skipped entirely, and we jump straight to unwrap_or.
Bridging the Gap Between Option and Result
One of the most powerful patterns I use is converting an Option into a Result halfway through a chain using .ok_or(). This is useful when a "missing value" should be treated as a specific error rather than just a "nothing happened" state.
Suppose we want to return an error if the user doesn't exist, but a default theme if the preference is missing. We can switch tracks like this:
fn get_theme_strict(user_id: u32) -> Result<Theme, AppError> {
let user = find_user(user_id).ok_or(AppError::UserNotFound)?;
let theme = user.theme_preference
.and_then(|pref| Theme::from_str(pref).ok())
.unwrap_or(Theme::Default);
Ok(theme)
}
I personally prefer this "hybrid" approach. I use the ? operator to bail out early on critical failures (like the user not existing) and combinators to handle the optional, "nice-to-have" data transformations. It keeps the function flat without sacrificing the ability to provide detailed error messages.
Knowing When to Stop Chaining
A word of caution: don't let the elegance of combinators trick you into writing "one-liners from hell." If you find yourself chaining seven or eight different methods with complex closures, you've just traded the Pyramid of Doom for a Wall of Text. If the logic inside a .map() closure exceeds three lines, I usually pull it out into its own named function. The goal is clarity, not brevity.
📋 Practical Task
Exercise: Refining the API Token Validator
You are building a middleware that validates API tokens. You have the following setup:
fn fetch_token_metadata(token: &str) -> Option<TokenMetadata>struct TokenMetadata { owner_id: u32, scope: String }fn verify_scope(scope: &str) -> Result<bool, ScopeError>
Your task is to write a function is_token_valid(token: &str) -> bool. Currently, it's written using nested match statements. Rewrite it using combinators (.and_then(), .map(), etc.) to make it a concise pipeline. The logic should be:
1. Fetch the metadata.
2. If metadata exists, verify the scope.
3. If the scope verification is successful and returns true, the token is valid.
4. In any other case (metadata missing, scope error, or scope is false), return false.
// Naive implementation to be refactored:
fn is_token_valid(token: &str) -> bool {
match fetch_token_metadata(token) {
Some(meta) => {
match verify_scope(&meta.scope) {
Ok(true) => true,
Ok(false) => false,
Err(_) => false,
}
},
None => false,
}
}There are no comments for now.