This week I learned 14
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 Result
s and
Option
s 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()
.