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
106: Option Combinators: map, and_then, unwrap_or
I've been working on a small project where I'm pulling user data from a messy JSON API. You know how it goes—half the fields are optional, and the other half are nested inside other optional objects. My initial instinct was to use match statements for everything, but the code started looking like a sideways pyramid. It was getting impossible to read.
The Pyramid of Doom
Let's look at what I was dealing with. I have a User that might have a Profile, and that Profile might have a nickname. I just want to get that nickname as a string, or a default value if anything is missing.
struct Profile {
nickname: Option<String>,
}
struct User {
profile: Option<Profile>,
}
fn get_nickname(user: &User) -> String {
match &user.profile {
Some(profile) => {
match &profile.nickname {
Some(name) => name.clone(),
None => "Guest".to_string(),
}
},
None => "Guest".to_string(),
}
}
It works, sure. But look at it. I'm writing "Guest".to_string() twice, and I'm indenting further and further to the right just to get to a single piece of data. There has to be a more "functional" way to pipe these options together.
Transforming values without the boilerplate
I wondered if I could just transform the value inside the Option without manually unpacking it. If I just wanted to take the profile and check if it exists, and if it does, maybe just return the length of the nickname... wait, I can't do that because the nickname itself is an Option. Let's simplify. Let's say I just wanted to turn the Profile into a string saying "Has Profile" or "No Profile".
I tried using .map(). It's a method that says: "If this is Some, apply this function to the inner value and wrap it back in a Some. If it's None, just stay None."
let user = User { profile: Some(Profile { nickname: Some("Rusty".into()) }) };
// I want to transform the Profile into a boolean (exists)
let has_profile = user.profile.map(|_| true);
// Result: Some(true)
That's cleaner. No match, no nesting. But it doesn't solve my nickname problem yet because my transformation function (getting the nickname) also returns an Option.
The "Option inside an Option" problem
Here is where I hit a wall. I tried to use map to get the nickname:
let name_option = user.profile.map(|p| p.nickname);
// This returns Option<Option<String>>!
Gross. Now I have a double-wrapped value. If the user has a profile but no nickname, I get Some(None). If they have neither, I get None. This is where and_then comes in. In other languages, this is often called flatMap.
Instead of wrapping the result of the closure back into an Option, and_then assumes the closure already returns an Option and just passes it through. It "flattens" the result.
let name_option = user.profile.and_then(|p| p.nickname);
// Now it's just Option<String>
Now we're talking. I've gone from a nested match block to a single line of code. I'm essentially saying: "Try to get the profile; if that works, try to get the nickname from it."
Landing the plane
The only problem left is that name_option is still an Option. My function needs to return a String. I could use unwrap(), but that's a great way to make my program crash in production. I need a safe fallback.
I'll use unwrap_or. It's the final step in the chain: if we have a value, give it to me; otherwise, use this default.
fn get_nickname(user: &User) -> String {
user.profile
.as_ref() // Convert &Option<Profile> to Option<&Profile>
.and_then(|p| p.nickname.as_ref())
.map(|n| n.clone())
.unwrap_or("Guest".to_string())
}
Wait, why the as_ref()? I noticed that without it, Rust tried to move the profile out of the user struct, which isn't allowed because I only have a reference to the user. A quick lesson learned: when chaining combinators on references, as_ref() is your best friend to keep everything as references rather than trying to take ownership.
Look at the difference. We went from a nested mess to a linear pipeline. It reads like a sentence: "Get the profile, then get the nickname, then clone it, or just call them Guest."
📋 Practical Task
Exercise: Building a Nested Configuration Resolver
You are building a system that loads settings for a game. The settings are hierarchical: the Game has an optional VideoSettings object, and the VideoSettings has an optional Resolution object.
struct Resolution {
width: u32,
height: u32,
}
struct VideoSettings {
resolution: Option<Resolution>,
}
struct Game {
video: Option<VideoSettings>,
}
Your task is to write a function called get_screen_width(game: &Game) -> u32. This function should:
- Use
and_thento navigate from theGameto theResolution. - Use
mapto extract thewidthfrom theResolution. - Use
unwrap_orto return a default width of1920if any part of the chain isNone.
Ensure you use as_ref() where necessary to avoid moving values out of the references.
There are no comments for now.