12 Mapping over Iterators
- Understand how to transform iterator values using
.map()
- Learn how to use closures (anonymous functions) in Rust
- Collect iterator results back into a collection using
.collect()
- Use turbofish 🐠💨 syntax for type annotations when needed
Often we want to transform the values in a vector. To do this in Rust, we can use the .map()
method. .map()
lets us apply an operation on each element of the iterator.
In R, we often use the purrr::map()
function or the apply()
family of functions to do this.
Using .map()
We apply .map()
to an iterator. For example, to square each element in a vector we can write:
// providing explicit type annotation in one
// value so the compiler knows its f64 and not f32
let x: Vec<f64> = vec![5.9, 6.8, 4.5, 7.3, 6.2];
.iter().map(|xi| xi.powi(2)) x
Using .map()
returns another iterator. We can chain operations over iterators by using multiple .map()
statements.
Closures
In the above example we used a closure to modify each element of the iterator. Closures are Rust’s version of an anonymous function.
A closure takes the structure |arg| expression
. They can also be multiple lines by wrapping the expression in a braces:
x.iter()
.map(|xi| {
let squared = xi.powi(2);
.sqrt()
xi})
Since iterators only contain one item, the closure only has one argument. However, you can use destructuring in the closure. For example if using .enumerate()
we want to access i
we can do so:
x.iter()
.enumerate()
.map(|(i, xi)| {
// do stuff here
})
Collecting results
In R when we use map()
the results are always returned as a vector. In Rust, we have to explcitly collect the iterator into our own type using .collect()
.
Typically iterators are collected into a Vec<T>
. Rust cant always infer the type that you want to collect into so we must tell the compiler what type we want.
We can do this during assignment:
let x_squared: Vec<f64> = x.iter().map(|xi| xi.powi(2)).collect();
Turbofish 🐠💨
Alternatively, we can use “turbofish” syntax. We can specify the type directly in the .collect()
function. .collect()
is a generic method. And in the words of the Rust standard library documentation (emphasis mine)
“Because
collect()
is so general, it can cause problems with type inference. As such,collect()
is one of the few times you’ll see the syntax affectionately known as the ‘turbofish’:::<>
. This helps the inference algorithm understand specifically which collection you’re trying to collect into.”
Turbofish takes the structure of .collect::<TYPE>()
.
Examples
Explicit typing
fn main() {
let nums = vec![1, 2, 3];
// Add 1 to each element
let incremented: Vec<_> = nums.iter()
.map(|x| x + 1)
.collect();
println!("{:?}", incremented);
}
Inference w/ turbofish
fn main() {
let nums = vec![1, 2, 3];
// Add 1 to each element
let incremented = nums.iter()
.map(|x| x + 1)
.collect::<Vec<_>>();
println!("{:?}", incremented);
}
Turbofish is a bit more awkward at first, but it is more flexible and doesn’t require modification whenever the inner type changes.
Exercise 1
Calculate the variance of a slice of f64
values.
\[ \text{variance} = \frac{\sum_{i=1}^n (x_i - \bar{x})^2}{n - 1} \]
Create a function
variance()
that:- Uses
.map()
to calculate squared differences from the mean - Uses
.sum()
to add them up - Divides by
n - 1
- Uses
- Use
.powi(2)
to square values. - Use your previously defined
mean()
function insidevariance()
.
View hint
fn variance(x: &[f64]) -> f64 {
let n = x.len() as f64;
let avg = mean(x);
let sq_diffs: f64 = x
.iter()
.map(|xi| ___ ) // squared difference here
.__(); // sum method here
/ (n - 1.0)
sq_diffs }
Solution
View solution
fn variance(x: &[f64]) -> f64 {
let n = x.len() as f64;
let avg = mean(x);
let sq_diffs: f64 = x
.iter()
.map(|xi| (xi - avg).powi(2))
.sum();
/ (n - 1.0)
sq_diffs }
fn main() {
let x = vec![2.0, 4.0, 4.0, 4.0, 5.0, 5.0, 7.0, 9.0];
println!("Variance is: {:.2}", variance(&x));
}
Exercise 2
Create a function standardize()
to perform z-score standardization on a vector of f64
.
\[ z_i = \frac{x_i - \mu}{\sigma} \]
- Use
.iter()
and.map()
to calculate mean and variance. - Use
.into_iter()
,.map()
, and.collect()
to build the standardized vector. - Return a new
Vec<f64>
of standardized values.
Solution
View solution
fn standardize(x: &[f64]) -> Vec<f64> {
let avg = mean(x);
let std_dev = variance(x).sqrt();
.iter().map(|xi| (xi - avg) / std_dev).collect()
x}
fn main() {
let x = vec![1.0, 2.0, 3.0, 4.0, 5.0];
let standardized = standardize(x);
println!("Standardized: {:?}", standardized);
}