Unpacking some Rust ergonomics: getting a single Result from an iterator of them
Monday, October 23, 2023
Rust has a lot of nice things that make life easy. One of the least discussed ones is also one of my favorites. It's a little nugget in the standard library that makes handling possible failures a lot easier. And it's not even baked in—it just falls out from the type system.
Nicely handling multiple Result
s or Option
s
When you do something that can fail, you get back a type that reflects that.
You'll get either a Result<T, E>
or an Option<T>
, depending on if it's something that could fail or could just not be present.
When you work in Rust, you end up getting very comfortable with these types, and there are a lot of ergonomics to help you.
One of those bits of ergonomics that I love is how you can collect an iterable of Results
into a Result
of a Vec
, effectively flipping the result inside out: you would expect a Vec<Result<T, E>>
, and you can get a Result<Vec<T>, E>
instead!
The same thing applies for Option
.
Let's see it in action.
Suppose you have a function which could fail, and you call it a number of times. Something like this:
fn fetch_chunk(from: usize, to: usize) -> Result<Row, Error> {
// some implementation
}
When we call it, and if we collect directly, we get a bunch of Result
s:
let chunks: Vec<Result<Row, Error>> =
indexes.iter().map(|i| fetch_chunk(i, i+1)).collect();
Now this is kind of ugly to deal with.
In a lot of cases, it is the type you want, because you can see which operations failed
1
.
But sometimes, you just want to know if anything failed, and in that case you can collect directly into a Result
.
let chunks: Result<Vec<Row>, Error> =
indexes.iter().map(|i| fetch_chunk(i, i+1)).collect();
This is the same code with a different type signature, and it collects into a different type. That's pretty darn cool, if you ask me. Just by which type you ask for, you get that one back!
This pattern of pulling the Result from the inside to the outside is one that's present in functional programming languages.
I was trying to find a name for it, and the closest parallel we
2
found was Haskell's sequence
, which is somewhat unsatisfying in the end since it feels like there should be a name for the concept of this pulling the result type from the inside to the outside.
You can do other nice things in a similar way here.
How it works
Under the hood, there's no magic here. This isn't built into Rust. It's just part of the standard library, and you can implement things like that for your own types!
collect
is the method where the magic happens.
It's a very general method on iterators, with this type from the docs:
fn collect<B>(self) -> B
where
B: FromIterator<Self::Item>,
Self: Sized,
This is basically saying that for any type that implements FromIterator
for the type that this iterator yields, you can collect it into that type.
An easy example is how an iterator with Item = i32
can be used to collect into a Vec<i32>
, since Vec
implements FromIterator
for all types.
And then the magic is these two impls:
FromIterator<Result<A, E>> for Result<V, E> where V: FromIterator<A>
FromIterator<Option<A>> for Option<V> where V: FromIterator<A>
We know that that type V
can be our Vec or whatever, so these implementations provide what we need to get the whole magical collect
behavior to fall out.
The types are scary, though, especially if you're not very familiar with strongly typed FP languages.
How do you find this out?
Things like this are hard to discover on your own in Rust. That's one of my laments with the language.
How I discovered it: initally, I think I saw it in the book or when pairing with other people.
Later on, I also saw it in the collect
docs, which gave some very useful examples of how to use it for this use case.
It's also explained in Rust By Example
3
, along with a few other examples.
The type system here does get in the way of good discoverability, in my opinion, since it's not super clear what combinations of traits on which types will give you what you need. I don't know how to improve it, other than talking gleefully about things that are fun like this and spreading the word.
What other cool Rust things should the world know about?
- In this case, you may want to use partition to collect the successes and failures separately. ↩
- We had a discussion about this at Recurse Center. Thank you to Erika, an anonymous recurser, Mikkel, and Miccah for enriching the discussion and teaching me new things. ↩
- Thanks to an anonymous Recurser for pointing this out! ↩
If this post was enjoyable or useful for you, please share it! If you have comments, questions, or feedback, you can email my personal email. To get new posts and support my work, subscribe to the newsletter. There is also an RSS feed.
Want to become a better programmer?
Join the Recurse Center!
Want to hire great programmers?
Hire via Recurse Center!