Iterators give us a wonderful array of functional-style combinators. Past readability, the rust compiler can occasionally optimize iterators better than it can for-loops, too. However, as iterators work by taking closures it can be confusing on how to best handle them compared to using classic for-loops. Here's a toy example:
fn parse_str_of_i32(input: &str) -> Vec<i32> {
input.split(",")
.map(|char| char.parse().unwrap()) // `unwrap`!
.collect()
}
let input = "1,2,3,4,5,6,7,8,9,0";
let numbers = parse_str_of_i32(input);
assert_eq!(numbers, vec![1,2,3,4,5,6,7,8,9,0]);
This works but it has an unwrap which means that if callers pass invalid
strings, such as "oh boy, here we go again", it will panic, which gives
callers of this code little control when things go wrong. How can we convert
this to use Result and be more ergonomic? Consider the for-loops variant,
first:
use std::num::ParseIntError;
fn parse_str_of_i32(input: &str) -> Result<Vec<i32>, ParseIntError> {
let mut numbers = vec![];
for char in input.split(",") {
numbers.push(char.parse()?)
}
Ok(numbers)
}
let input = "1,2,3,4,5,6,7,8,9,0";
let numbers = parse_str_of_i32(input).unwrap();
assert_eq!(numbers, vec![1,2,3,4,5,6,7,8,9,0]);
You might think this means if you want to use error handling while iterating you need to have a for-loop instead of using Iterator but you can still have an Iterator and have get the same type signature above for our parser!
use std::num::ParseIntError;
fn parse_str_of_i32(input: &str) -> Result<Vec<i32>, ParseIntError> {
input.split(",")
.map(|char| char.parse())
.collect()
}
let input = "1,2,3,4,5,6,7,8,9,0";
let numbers = parse_str_of_i32(input).unwrap();
assert_eq!(numbers, vec![1,2,3,4,5,6,7,8,9,0]);
How does this work? collect knows how to take an Iterator of Results and
turn it into an Result<Vec<A>, B>. At the first sight of an Err the whole
expression will become the Err case but if everything works out with Ok then
the Iterator will take all the values into their own Vec and return Ok of
the enclosing Vec. This is sometimes referred to as a "transpose" and you can
see similar 'inside-out' behaviour elsewhere, including Result
itself.
You can also specify collections other than Vec. If A is something that can
be collected into some container V, then an Iterator<Item=Result<V, B>> is
possible. Have a poke around the FromIterator trait
docs to get a
better sense of what collect can roll up!