Skip to Content
Course content

106: Option Combinators: map, and_then, unwrap_or

Click on the "Edit" button in the top corner of the screen to edit your slide content.

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_then to navigate from the Game to the Resolution.
  • Use map to extract the width from the Resolution.
  • Use unwrap_or to return a default width of 1920 if any part of the chain is None.

Ensure you use as_ref() where necessary to avoid moving values out of the references.

Rating
0 0

There are no comments for now.

to be the first to leave a comment.