Enter your email address to follow our blog with all the latest software engineering tricks, tips, and updates from the R&D team at KnowBe4!
By: Daniel Cormier
Published: 28 Jan 2022
Last Updated: 12 Aug 2022
This article will discuss Rust's Option
and Result
enums, and ways to work with them without using match
. This came about because someone in our internal chat was lamenting how frequently they needed to use match
. I found that I used match
very frequently when Rust was newer to me, but now I use it much, much less.
Both of those enums have many methods that can often be used instead of match
. I frequently refer back to the docs for Option
/Result
when I have an instance of one of those that I need to do something with. Specifically, I want to draw your attention to their methods that take an FnOnce
. Let's go over some of those.
.map()
The most common of these methods that I use is .map()
(see Option::map()
/Result::map()
). It lets you inspect the inner T
of an Option<T>
/Result<T, E>
, and optionally replace it, even with a different type.
Before we dive into this, a couple of thing notes:
First, let's look at an example of how you could use match
to convert an Option<i32>
to Option<String>
:
let before: Option<i32> = Some(-2);
let after: Option<String> = match before {
Some(val) => Some(val.to_string()),
None => None,
};
assert_eq!(Some(String::from("-2")), after);
Now let's look at Option::map()
. For Option
, if you call .map()
and you have Some
, it passes the inner value to your FnOnce
, and whatever value you return becomes the value in the Some
. If the value was None
, then your FnOnce
isn't called.
For example, let's again say you have Option<i32>
and you need Option<String>
. Here's how you can do that with .map()
:
let before: Option<i32> = Some(-2);
let after: Option<String> = before.map(|i| i.to_string());
assert_eq!(Some(String::from("-2")), after);
If the value is None
, then that FnOnce
passed to .map()
just isn't called, and you're still given an Option<String>
(but the value is still None
).
let before: Option<i32> = None;
let after: Option<String> = before.map(|i| i.to_string());
assert_eq!(None, after);
It works similarly for Result
, just with Ok
instead of Some
, and Err
instead of None
.
This will convert Result<i32, &str>
to Result<String, &str>
:
let before: Result<i32, &str> = Ok(-2);
let after: Result<String, &str> = before.map(|i| i.to_string());
assert_eq!(Ok(String::from("-2")), after);
And it won't call your FnOnce
if the value is Err
.
let before: Result<i32, &str> = Err("there was an error");
let after: Result<String, &str> = before.map(|i| i.to_string());
assert_eq!(Err("there was an error"), after);
.map_err()
Closely related to that is .map_err()
. This one only exists on Result
, but is very useful. This method works the same as .map()
, but only gets called if the value is Result::Err
, and lets you inspect/change the error value. This is useful if you need to change the type of the error, or even when you just want to trace/log an error if there is one. For example:
val.map_err(|err| {
error!(error = %err, "Error doing it");
err
})?;
.and_then()
Another one is .and_then()
(Option::and_then()
/Result::and_then()
). If you call this and you have Some
, it passes the inner value to your FnOnce
, and instead of returning a new value to go inside Some
(like what .map()
does), you return an entire replacement Option
. This means you can, for example, have Some
and convert it to None
.
Extending the original example of converting an Option<i32>
into an Option<String>
, but only if the value is greater than zero. Otherwise, None
.
First, for Option<i32>
:
let before: Option<i32> = Some(2);
let after: Option<String> = before.and_then(|i| {
if i > 0 {
Some(i.to_string())
} else {
None
}
});
assert_eq!(Some(String::from("2")), after);
It works the same way for Result<i32, &str>
, just with Ok
instead of Some
:
let after: Result<i32, &str> = Ok(2);
let before: Result<String, &str> = after.and_then(|i| {
if i > 0 {
Ok(i.to_string())
} else {
Err("value must be > 0")
}
});
assert_eq!(Ok(String::from("2")), before);
.or_else()
You can use .or_else()
(Option::or_else()
/Result::or_else()
) to try something else if you don't have Some
/Ok
. The FnOnce
passed to this method will be called for None
/Err
, and it must return the same type as the original Option
/Result
. You cannot use it to convert the inner type. It's useful, for example, if you have another way to try to get the needed value.
Continuing the examples with Option<i32>
:
let before: Option<i32> = None;
let after: Option<i32> = before.or_else(|| Some(0));
assert_eq!(Some(0), after);
It's similar for Result
(with the FnOnce
getting called for Err
), but you may change the type of Err(E)
(like Result::map_err()
). You could use it to return an Ok
value for some Err
values, while returning or mapping other Err
values.
let before: Result<i32, &str> = Err("a specific error");
let after: Result<i32, OtherError> = before.or_else(|err| {
if err == "a specific error" {
Ok(0)
} else {
Err(OtherError("mapped to another error"))
}
});
assert_eq!(Err(OtherError("mapped to another error")), after);
.ok_or_else()
If you have an Option
and need a Result
(Some
→ Ok
, None
→ Err
), there's Option::ok_or_else()
.
let before: Option<i32> = Some(-2);
let after: Result<i32, &str> = before.ok_or_else(|| "no value");
assert_eq!(Ok(-2), after);
let before: Option<i32> = None;
let after: Result<i32, &str> = before.ok_or_else(|| "no value");
assert_eq!(Err("no value"), after);
.ok()
If you have a Result
and need an Option
(Ok
→ Some
, Err
→ None
), there's Result::ok()
method. It doesn't take an FnOnce
, but it's nice to know about how to do the opposite of Option::ok_or_else()
.
let before: Result<i32, &str> = Ok(-2);
let after: Option<i32> = before.ok();
assert_eq!(Some(-2), after);
let before: Result<i32, &str> = Err("no value");
let after: Option<i32> = before.ok();
assert_eq!(None, after);
.flatten()
Sometimes chaining methods together can get you something like Option<Option<T>>
, where you need Option<T>
. The .flatten()
method will remove one layer of nested Option
.
let before: Option<Option<Option<i32>>> = Some(Some(Some(-2)));
let after: Option<Option<i32>> = before.flatten();
assert_eq!(Some(Some(-2)), after);
let last: Option<i32> = after.flatten();
assert_eq!(Some(-2), last);
That there is an experimental Result::flatten()
method, but it is still under discussion.
.transpose()
Other times you'll end up with something like Option<Result<T, E>>
when you need Result<Option<T>, E>
. Guess what? There's a method for that: .transpose()
is what you want.
And if you need to, you can use Result::transpose()
to go the other way, from Result<Option<T>, E>
to Option<Result<T, E>>
let input: Option<&str> = Some("-2");
let mapped: Option<Result<i32, ParseIntError>> = input.map(str::parse);
let result: Result<Option<i32>, ParseIntError> = mapped.transpose();
assert_eq!(Ok(Some(-2)), result);
// Now that it's a `Result`, you could do normal error handling
// stuff, like use the `?` operator.
// And if you need to turn a `Result<Option<T>, E>` into an
// `Option<Result<T, E>>`, you can call `.transpose()` on _that_.
assert_eq!(Some(Ok(-2)), result.transpose());
let input: Option<&str> = Some("not an i32");
let mapped: Option<Result<i32, ParseIntError>> = input.map(str::parse);
let result: Result<Option<i32>, ParseIntError> = mapped.transpose();
assert_eq!(Err(ParseIntError{kind: IntErrorKind::InvalidDigit}), result);
assert_eq!(Some(Err(ParseIntError{kind: IntErrorKind::InvalidDigit})), result.transpose());
let input: Option<&str> = None;
let mapped: Option<Result<i32, ParseIntError>> = input.map(str::parse);
let result: Result<Option<i32>, ParseIntError> = mapped.transpose();
assert_eq!(Ok(None), result);
assert_eq!(None, result.transpose());
If you have an Option
or Result
and you want to do something with the value inside, or convert it in some way, look at what methods are available. There's a good chance there's something that'll let you do what you're after without having to use match
.
KnowBe4 Engineering heavily uses On-Demand environments for quick iterations on native cloud-based…
How KnowBe4 solved the "It Works on My Machine" problem with a new approach to provisioning test…