On Rust's Option and Result Enums


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:

  • For clarity, I am going to use type annotations for all variables, even when they may not be needed.
  • To try to make all the types involved more clear, I will often assign new variables rather than chain functions together.
  • At the end of each section, there is a link to related code in the Rust playground for you to explore further.

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);

Playground

.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
})?;

Playground

.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);

Playground

.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);

Playground

.ok_or_else()

If you have an Option and need a Result (SomeOk, NoneErr), 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);

Playground

.ok()

If you have a Result and need an Option (OkSome, ErrNone), 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);

Playground

.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);

Playground

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());

Playground

TL; DR

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.

See the docs for Option and Result for more.

Topics: Engineering, Rust, Software Development

Get the latest about social engineering

Subscribe to CyberheistNews