Options, results, combinators, and clippy

The Rust standard library provides two types for representing when a value is present or absent (or an error). These types are Result and Option. In general a lot of things can go wrong all the time. Result and Option provide ways to react to success and failure.

Consider a function called cmd() that returns the name of the running process. For now, we’ll stub out cmd() to return an empty ffi::OsString.

fn cmd() -> ffi::OsString {
    ffi::OsString::new()
}

Rust provides env::current_exe() to get a Result<PathBuf> who’s Ok(T) variant contains the full path of the current process. This is a nice affordance however, we really just want the name of the process. Fortunately, PathBuf has a file_name() method who’s Some(T) variant gets exactly what we want. Combining the two looks something like this:

fn cmd() -> ffi::OsString {
    env::current_exe().unwrap().file_name().unwrap()
}

This kind of accomplishes what we want, however, we’ve introduced two potential panic!s. This is where Option and Result come in. Recall that env::current_exe() returns an io::Result<PathBuf>. Our first call to unwrap() elides the error case of getting the name of the current executable. Let’s not do that by returning an io::Result<ffi::OsString> instead.

fn cmd() -> io::Result<ffi::OsString> {
    let cmd_path = env::current_exe()?;
    Ok(cmd_path.file_name().unwrap().to_os_string())
}

A little better, but we’re still calling unwrap(); let’s take a closer look at file_name().

Recall that PathBuf::file_name() returns an Option<&OsStr> where Some(T) represents the file name of the current path::PathBuf and None represents a path::PathBuf that ends in “..”. This is almost what we want, however, it’s an Option<T>, not a Result<T,E>. At this point we need to make a decision: Do we care about the error? Or do we just care about presence or absence of a value? For this example we’ll implement the latter. This means we need a way to convert cmd_path into an Option<T> instead of a Result<T,E>. Luckily, Result<T,E> provides ok() to adapt a Result<T,E> into an Option<T>.

fn cmd() -> Option<ffi::OsString> {
    let cmd_path = env::current_exe().ok()?;
    let cmd = cmd_path.file_name()?;
    Some(cmd.to_os_string())
}

We did it. We have an API that represents success and failure of getting the name of the process. Naturally, there are other ways to do this. In cmd() we’re handling all errors with the ? operator. Option<T> and Result<T,E> provide combinators to chain Results and Options together: operating on the respective variants of each.

fn cmd() -> Option<ffi::OsString> {
    env::current_exe()
        .ok()
        .as_ref()
        .and_then(|p| p.file_name())
        .map(|c| c.to_os_string())
}

This implementation accomplishes the same thing as the previous one but with combinators instead of the ? operator. and_then and map operate on the T held by a Some(T) Option<T> variant. For this post I’m not going to go into more information on combinators; Andrew Gallant has a great post showing the power of Result, Option, and combinators.

An aside on ok()

In both implementations we adapted a Result<T,E> into an Option<T> with ok(). When I first learned about adapting Result<T,E>s into Option<T>s I overlooked ok(). Instead, I wrote the following:

fn cmd() -> Option<ffi::OsString> {
    let cmd_path = env::current_exe().map_or(None, Some)?;
    let cmd = cmd_path.file_name()?;
    Some(cmd.to_os_string())
}

While this accomplished the same thing (and is similar to how the standard library implements ok), it’s a lot more verbose than a simple call to ok(). I recently opened a pull request on Clippy to catch this reimplementation of ok().