Option combinators: Difference between revisions
Created page with "Incase you missed yesterday’s writeup where we covered Modules. You can check it through the link below. Modules I missed publishing yesterday’s writeup but here we are. If you missed our discussion on smart pointers check it out… medium.com Rust’s Option type is crucial to its safety guarantees, letting us explicitly handle cases where a value might be Some(T) or None. Earlier on we had seen the basics of Option, like how it replaces null pointers with a..." |
(No difference)
|
Latest revision as of 19:54, 23 November 2025
Incase you missed yesterday’s writeup where we covered Modules. You can check it through the link below.
Modules I missed publishing yesterday’s writeup but here we are. If you missed our discussion on smart pointers check it out… medium.com
Rust’s Option type is crucial to its safety guarantees, letting us explicitly handle cases where a value might be Some(T) or None. Earlier on we had seen the basics of Option, like how it replaces null pointers with a safer alternative.
Now, we’ll focus on Option combinators which are essentially methods that let you chain operations on Option values to write cleaner, more expressive code. These methods are for transforming, combining, or extracting values from Options without drowning in match statements or if let blocks.
We’ll go through key combinators, show how they work, and tie it all together with examples that feel like tasks you’d encounter in a codebase.
Let’s start with some of the most common combinators and see how they apply to scenarios you might face in a typical Rust project. map
The map combinator is your go-to when you want to apply a function to the value inside a Some, but do nothing if it’s None. It takes an Option<T>, applies a function to the T if it exists, and wraps the result in a new Option.
Formatting User Data
Suppose you’re working on a platform where you need to display a user’s full name on their profile page. The user’s profile might have a middle name, but it’s optional. You want to format the full name as “First Middle Last” if the middle name exists, or “First Last” if it doesn’t.
Here’s how you’d use map to handle this:
struct User {
first_name: String,
middle_name: Option<String>,
last_name: String,
}
fn format_full_name(user: &User) -> String {
user.middle_name
.as_ref()
.map(|middle| format!("{} {} {}", user.first_name, middle, user.last_name))
.unwrap_or(format!("{} {}", user.first_name, user.last_name))
}
fn main() {
let user1 = User {
first_name: "Alice".to_string(),
middle_name: Some("Marie".to_string()),
last_name: "Smith".to_string(),
};
let user2 = User {
first_name: "Bob".to_string(),
middle_name: None,
last_name: "Jones".to_string(),
};
println!("{}", format_full_name(&user1));
println!("{}", format_full_name(&user2));
}
In our code above, map applies the formatting function only if middle_name is Some, and we use unwrap_or to provide a fallback for the None case.
This is cleaner than a match statement checking for Some or None explicitly. In some project, this pattern is common when transforming optional data, like formatting addresses, phone numbers, or optional settings, without cluttering your code with conditionals.
This example above showed how map keeps your logic focused and readable. But what if you need to chain multiple operations where each one might produce a new Option? That’s where and_then is useful.
and_then
The and_then combinator is perfect when you need to perform an operation that itself returns an Option.
Unlike map, which wraps the result in an Option, and_then flattens the result, so you don’t end up with nested Option<Option<T>>.
Picture working on a task management app. You need to fetch a task’s assignee, then check if they have an email address, and finally validate that email before sending a reminder.
Each step depends on the previous one succeeding, and any step could return None. Here’s how and_then helps:
struct User {
email: Option<String>,
}
struct Task {
assignee: Option<User>,
}
fn validate_email(email: &str) -> Option<String> {
if email.contains("@") {
Some(email.to_string())
} else {
None
}
}
fn get_valid_assignee_email(task: &Task) -> Option<String> {
task.assignee.as_ref().and_then(|user| user.email.as_ref().and_then(|email| validate_email(email)))
}
fn main() {
let task1 = Task {
assignee: Some(User {
email: Some("[email protected]".to_string()),
}),
};
let task2 = Task {
assignee: Some(User {
email: Some("invalid_email".to_string()),
}),
};
let task3 = Task { assignee: None };
println!("{:?}", get_valid_assignee_email(&task1));
println!("{:?}", get_valid_assignee_email(&task2));
println!("{:?}", get_valid_assignee_email(&task3));
}
In code above, and_then chains the operations. First checking if there’s an assignee, then checking if they have an email, and finally validating the email. If any step results in None, the chain short-circuits, and None is returned. This is a pattern you’ll see all over in APIs, database queries, or any system where you need to drill down through layers of optional data, like fetching a user’s profile, their preferences, and then a specific setting.
Now that we’ve seen how to transform and chain operations, what if you just want to provide a default value or handle the None case gracefully. Let’s look at unwrap_or and its relatives.
unwrap_or, unwrap_or_else, and unwrap_or_default
These combinators let you specify what happens when an Option is None and are useful when you want to avoid explicit match statements but still provide a fallback value.
- unwrap_or takes a static default value.
- unwrap_or_else takes a closure to compute the default, which is only called if the Option is None.
- unwrap_or_default uses the type’s default value (if it implements Default).
Below is an example how you’d use these combinators.
fn get_timeout(config: &Option<u32>) -> u32 {
config.unwrap_or_else(|| {
// Simulate fetching a default from environment or system
if std::env::var("FAST_MODE").is_ok() {
30
} else {
60
}
})
}
fn main() {
let config1 = Some(45);
let config2 = None;
println!("Timeout: {}", get_timeout(&config1)); // Output: Timeout: 45
println!("Timeout: {}", get_timeout(&config2)); // Output: Timeout: 60 (or 30 if FAST_MODE is set)
}
In the code above, unwrap_or_else lets you compute the default lazily—only if the Option is None.
This is great for cases where calculating the default is expensive, like querying a database or reading environment variables.
In a real project, you might use this for settings like retry counts, log levels, or API keys, where defaults depend on runtime conditions.
This pattern is powerful, but sometimes you need to filter values based on a condition. That’s where filter comes in handy.
filter
The filter combinator lets you keep an Option’s value only if it satisfies a condition. If the condition fails or the Option is None, you get None.
Imagine you’re working on a file-sharing service, and you need to check if a user has permission to access a file.
The user’s role is optional (e.g., they might not be logged in), and you only want to proceed if their role is “admin”. The code below is how filter fits in:
struct File {
name: String,
}
fn get_file_if_admin(role: Option<&str>, file: &File) -> Option<&File> {
role.filter(|&r| r == "admin").map(|_| file)
}
fn main() {
let file = File {
name: "secret.txt".to_string(),
};
let role1 = Some("admin");
let role2 = Some("guest");
let role3 = None;
println!("{:?}", get_file_if_admin(role1, &file).map(|f| &f.name));
println!("{:?}", get_file_if_admin(role2, &file).map(|f| &f.name));
println!("{:?}", get_file_if_admin(role3, &file).map(|f| &f.name));
}
In our code snippet above, filter ensures that only users with the “admin” role allow the function to return the file.
Often this is a common pattern in access control systems, where you need to gate access based on optional user attributes like roles, permissions, or subscription status.
Here are a few practical tips to keep note of when working with option combinators we have discussed above.
- Use map for simple transformations, and_then for operations that return Option, and unwrap_or_else for computed defaults.
- Break long chains into smaller functions or add comments to clarify each step.
- Combinators like map and and_then also work with Result, so you can use similar patterns for error handling.
- Always test edge cases to ensure your fallbacks work as expected.
Read the full article here: https://medium.com/rustaceans/option-combinators-7988f0b6c7c7