Short intro to options: you either have a value or you don't, and you can guarantee at compile time that you are handling both None and Some(value) cases.
Here's how the Option type definition looks like in the Rust standard library.
pub enum Option<T> {
None,
Some(T),
}
I like to think of options as a type-safe null pointer.
In languages without options, you'd handle the optionality by using a pointer type. The null pointer tells you there is nothing there, and you need to check for null before dereferencing.
If you don't check, the compiler might not warn you, and this leads to a NullPointerException at runtime.
The key point here is by using the option type we have moved a whole category of programming errors earlier in the process at compile time.
Back to #rustlang options, as a beginner, you might start by using .unwrap(), which gives us the value inside the option or panics.
Let's write a simple function to print optional text, if we have text we want to print it.
Here is a basic implementation and usage.
fn print_text(text: Option<String>) {
let actual_text = text.unwrap();
println!("The text is: {}", actual_text);
}
print_text(Some("Hello".to_string()));
// Prints: The text is: Hello
print_text(None);
// Panics, program stops
print_text(Some("Goodbye".to_string()));
// We never get here because the code above panics :(
You might be thinking the whole point was moving checks to compile-time, but the compiler didn't warn us we'd never get to Goodbye.
And this is precisely why we would want to avoid using unwrap. We lose the benefits that option gives us. We are back to runtime errors.
Let's see how we can do better.
By using match, we are forced to think about what should happen if we have no text. In this case, we choose to print "No text", but the benefit is the compiler made us cover both cases.
And no more panics!
fn print_text(text: Option<String>) {
match text {
Some(actual_text) => println!("The text is: {}", actual_text),
None => println!("No text"),
}
}
print_text(Some("Hello".to_string()));
print_text(None);
print_text(Some("Goodbye".to_string()));
// program prints;
// The text is: Hello
// No text
// The text is: Goodbye
Let's say we didn't want to print anything if there was no text. After all, what is the point of printing "No text"?
In that case, we can use "if let" to destructure the option and handle only the Some(_) case.
fn print_text(text: Option<String>) {
if let Some(actual_text) = text {
println!("The text is: {}", actual_text)
}
}
print_text(Some("Hello".to_string()));
print_text(None);
print_text(Some("Goodbye".to_string()));
// program prints;
// The text is: Hello
// The text is: Goodbye
We can achieve the same result as above by using .map. Map will take our option and only execute the code inside it if we have Some(value).
/// Maps an Option<T> to Option<U> by applying a function to a contained value.
pub fn map<U, F>(self, f: F) -> Option<U>
// If there is some text, print it
fn print_text(text: Option<String>) {
text.map(|actual_text| println!("The text is: {}", actual_text));
}
print_text(Some("Hello".to_string()));
print_text(None);
print_text(Some("Goodbye".to_string()));
// program prints;
// The text is: Hello
// The text is: Goodbye
If we wanted to go back to providing a default "No text", we can use map_or_else.
fn print_text(text: Option<String>) {
text.map_or_else(
|| println!("No text"), // default, no text
|actual_text| println!("The text is: {}", actual_text), // actual text
);
}
print_text(Some("Hello".to_string()));
print_text(None);
print_text(Some("Goodbye".to_string()));
// program prints;
// The text is: Hello
// No text
// The text is: Goodbye
Let's say we want to take optional text and convert it to uppercase. If we want to return an option, we can use and_then.
/// If there is some text, uppercase it
fn uppercase_text(text: Option<String>) -> Option<String> {
// use and_then to get an Option<String> where the string inside
// is the uppercase of text.
let uppercase_text = text.and_then(|actual_text| Some(actual_text.to_uppercase()));
uppercase_text
}
let result = uppercase_text(Some("Hello".to_string()));
println!("{:?}", result);
let result = uppercase_text(None);
println!("{:?}", result);
let result = uppercase_text(Some("Goodbye".to_string()));
println!("{:?}", result);
// program prints:
// Some("HELLO")
// None
// Some("GOODBYE")
Another common technique is to do something with an option and return a Result with some processed value or Error. Let's convert uppercase_text to return a Result.
/// If there is some text, uppercase it
fn uppercase_text(text: Option<String>) -> Result<String, String> {
// use and_then to get an Option<String> where the string inside
// is the uppercase of text.
let uppercase_text = text.and_then(|actual_text| Some(actual_text.to_uppercase()));
// take Option<String> and:
// if it's Some(val) return Ok(val)
// if it's None return Err("No text to uppercase")
let result = uppercase_text.ok_or("No text to uppercase".to_string());
result
}
let result = uppercase_text(Some("Hello".to_string()));
println!("Result {:?}", result);
let result = uppercase_text(None);
println!("Result {:?}", result);
let result = uppercase_text(Some("Goodbye".to_string()));
println!("Result {:?}", result);
// program prints:
// Result Ok("HELLO")
// Result Err("No text to uppercase")
// Result Ok("GOODBYE")
To summarize, Option allows us to be intentional about expressing optionality and provides compile-time guarantees (unless we deliberately use methods that panic).
There are many useful ways of dealing with options and choosing which is a matter of style and requirements.
Hope you enjoyed this 🧵
Share something about this post.