Iteration patterns for Result & Option
Posted on Mon 10 April 2017 in Code • Tagged with Rust, iterators • Leave a comment
Towards the end of my previous post about for
loops in Rust,
I mentioned how those loops can often be expressed in a more declarative way.
This alternative approach involves chaining methods of
the Iterator
trait
to create specialized transformation pipelines:
let odds_squared: Vec<_> = (1..100)
.filter(|x| x % 2 != 0)
.map(|x| x * x)
.collect();
Code like this isn’t unique to Rust, of course. Similar patterns are prevalent in functional languages such as F#, and can also be found in Java (Streams), imperative .NET (LINQ), JavaScript (LoDash) and elsewhere.
This saying, Rust also has its fair share of unique iteration idioms.
In this post, we’re going to explore those arising on the intersection of iterators
and the most common Rust enums: Result
and Option
.
filter_map()
When working with iterators, we’re almost always interested in selecting elements that match some criterion or passing them through a transformation function. It’s not even uncommon to want both of those things, as demonstrated by the initial example in this post.
You can, of course, accomplish those two tasks independently:
Rust’s filter
and map
methods
work just fine for this purpose.
But there exists an alternative, and in some cases it fits the problem amazingly well.
Meet filter_map
.
Here’s what the official docs
have to say about it:
Creates an iterator that both filters and maps.
Well, duh.
On a more serious note, the common pattern that filter_map
simplifies
is unwrapping a series of Option
s.
If you have a sequence of maybe-values,
and you want to retain only those that are actually there,
filter_map
can do it in a single step:
// Get the sequence of all files matching a glob pattern via the glob crate.
let some_files = glob::glob("foo.*").unwrap().map(|x| x.unwrap());
// Retain only their extensions, e.g. ".txt" or ".md".
let file_extensions = some_files.filter_map(|p| p.extension());
The equivalent that doesn’t use filter_map
would have to split the checking & unwrapping of Option
s into separate steps:
let file_extensions = some_files.map(|p| p.extension())
.filter(|e| e.is_some()).map(|e| e.unwrap());
Because of this check & unwrap logic,
filter_map
can be useful even with a no-op predicate (.filter_map(|x| x)
)
if we already have the Option
objects handy.
Otherwise, it’s often very easy to obtain them,
which is exactly the case for the Result
type:
// Read all text lines from a file:
let lines: Vec<_> = BufReader::new(fs::File::open("file.ext")?)
.lines().filter_map(Result::ok).collect();
With a simple .filter_map(Result::ok)
, like above,
we can pass through a sequence of Result
s and yield only the “successful” values.
I find this particular idiom to be extremely useful in practice,
as long as you remember that Err
ors will be discarded by it1.
As a final note on filter_map
,
you need to keep in mind that regardless of how great it often is,
not all combinations of filter
and map
should be replaced by it.
When deciding whether it’s appropriate in your case,
it is helpful to consider the equivalence of these two expressions:
iter.filter(f).map(m)
iter.filter_map(|x| if f(x) { Some(m(x)) } else { None })
Simply put, if you find yourself writing conditions like this inside filter_map
,
you’re probably better off with two separate processing steps.
collect()
Let’s go back to the last example with a sequence of Result
s.
Since the final sequence won’t include any Err
oneous values,
you may be wondering if there is a way to preserve them.
In more formal terms, the question is about turning a vector of results
(Vec<Result<T, E>>
) into a result with a vector (Result<Vec<T>, E>
).
We’d like for this aggregated result to only be Ok
if all original results were Ok
.
Otherwise, we should just get the first Err
or.
Believe it or not, but this is probably the most common Rust problem!2
Of course, that doesn’t necessarily mean the problem is particularly hard. Possible solutions exist in both an iterator version:
let result = results.into_iter().fold(Ok(vec![]), |mut v, r| match r {
Ok(x) => { v.as_mut().map(|v| v.push(x)); v },
Err(e) => Err(e),
});
and in a loop form:
let mut result = Ok(vec![]);
for r in results {
match r {
Ok(x) => result.as_mut().map(|v| v.push(x)),
Err(e) => { result = Err(e); break; },
};
}
but I suspect not many people would call them clear and readable, let alone pretty3.
Fortunately, you don’t need to pollute your codebase with any of those workarounds. Rust offers an out-of-the-box solution which solves this particular problem, and its only flaw is one that I hope to address through this very post.
So, here it goes:
let result: Result<Vec<_>, _> = results.collect();
Yep, that’s all of it.
The background story is that Result<Vec<T>, E>
simply “knows”
how to construct itself from a sequence of Result
s.
Unfortunately, this API is hidden behind Rust’s iterator abstraction,
and specifically the fact that
Result
implements FromIterator
in this particular manner.
The way
the documentation page for Result
is structured, however — with trait implementations at the very end —
ensures this useful fact remains virtually undiscoverable.
Because let’s be honest: no one scrolls that far.
Incidentally, Option
offers analogous functionally:
a sequence of Option<T>
can be collect
ed into Option<Vec<T>>
,
which will be None
if any of the input elements were.
As you may suspect, this fact is equally hard to find in the relevant docs.
But the good news is: you know about all this now! :) And perhaps thanks to this post, those handy tricks become a little better in a wider Rust community.
partition()
The last technique I wanted to present here follows naturally
from the other idioms that apply to Result
s.
Instead of extracting just the Ok
values with flat_map
,
or keeping only the first error through collect
,
we will now learn how to retain all the errors and all the values,
both neatly separated.
The partition
method,
as this is what the section is about,
is essentially a more powerful variant of filter
.
While the latter only returns items that do match a predicate,
partition
will also give us the ones which don’t.
Using it to slice an iterable of Result
s is straightforward:
let (oks, fails): (Vec<_>, Vec<_>) = results.partition(Result::is_ok);
The only thing that remains cumbersome is
the fact that both parts of the resulting tuple still contain just Result
s.
Ideally, we would like them to be already unwrapped into values and errors,
but unfortunately we need to do this ourselves:
let values: Vec<_> = oks.into_iter().map(Result::unwrap).collect();
let errors: Vec<_> = fails.into_iter().map(Result::unwrap_err).collect();
As an alternative,
the partition_map
method
from the itertools
crate
can accomplish the same thing in a single step,
albeit a more verbose one.
-
A symmetrical technique is to use
.filter_map(Result::err)
to get just theErr
or objects, but that’s probably much less useful as it drops all the successful values. ↩ -
Based on my completely unsystematic and anecdotal observations, someone asks about this on the #rust-beginners IRC approximately every other day. ↩
-
The
fold
variant is also rife with type inference traps, often requiring explicit type annotations, a “no-op”Err
arm inmatch
, or both. ↩