Map methods in Rust

Dec 20256 min read

Rust has functional programming features, namely the ability to use functions as arguments or return them from other functions. To better understand adapter methods such as map, it is worth first revisiting iterators and collections.

Iterator Vs collection

A collection is a data structure that holds values in memory eg. Vec<T>. An iterator however, does not store values and only provides a way to access a sequence of values and generate them often into another collection.

The iterator as defined in the iterator trait has a next method which returns the elements of the iterator Option<T> i.e. Some<T> or None. Other iterator methods are built on top of the next method.

Create an iterator from a collection

Here are 3 methods used to create an iterator from a collection.

1. iter()

Provides an iterator of immutable references to all content of the collection.


let v: Vec<i32> = vec![1, 2, 3]; for i in v.iter() { println!("value: {}", i); }

2. iter_mut()

Provides an iterator of mutable references which means that the elements of the collection can be changed with this iterator.


let mut v: Vec<String> = vec!["hi".to_string(), "there".to_string()]; for s in v.iter_mut() { *s += "!"; } println!("{:?}", v); // hi!there!

3. into_iter()

Transforms the collection into an iterator when the collection is no longer needed. In other words, it transfers ownership of each element.


// use a data type that does not implement COPY such as an array // the code errors out due to "borrow of moved value: `vec`" let vec: Vec<i32> = vec![1, 2, 3]; println!("{:?}", vec.into_iter()); println!("{:?}", vec); // bad

To produce a collection from the iterator from into_iter(), use collect(). The collect() method needs to know the type it is collecting whether inferred from the variable type or using something like vec.into_iter().collect::<Vec<_>>(). Elements of the collection generated have the same type as the ones in the original collection.

To collect iterator elements into a different type, use the following:


vec.into_iter().map(|x| x as i64).collect::<Vec<i64>>()

This leads us nicely to adapter methods.

Adapter methods

They take an iterator type and return another iterator. map and filter are the most common adapter methods.

The map method as seen in its source code is an iterator method. It takes a closure as an argument. The iterator will call the closure on every element to produce a new iterator.

It is worth mentioning at this point that iterators and their methods are lazy meaning that they execute when calling methods like next() and collect(), consumer methods.

What are some different flavors of the map() method?

filter_map

filter_map is used to create an iterator that both filters and maps, such that the chain of methods to do both is replaced by 1 method.

Given a vector of strings vec!["one", "1", "two", "2", "three", "3"], we want to produce another vector containing all values that can be parsed to type i32. We can do that using filter().map() or more efficiently using filter_map(). Here is the code for both:


// Our starting point let original_vec: Vec<&str> = vec!["one", "1", "two", "2", "three", "3"]; // Return values that can be parsed into i32 let filtered: Vec<_> = original_vec.iter().filter(|x| x.parse::<i32>().is_ok()).collect(); println!("{:?}", filtered); // ["1", "2", "3"] // but this is not Vec<i32> // Transform that into Vec<i32> let end_result: Vec<i32> = filtered.iter().map(|x| x.parse::<i32>().unwrap()).collect(); println!("{:?}", end_result); // [1, 2, 3]

Here is the code using filter_map():


// Our starting point let original_vec: Vec<&str> = vec!["one", "1", "two", "2", "three", "3"]; // Map and parse at the same time let end_result: Vec<i32> = original_vec.iter().filter_map(|s| s.parse::<i32>().ok()).collect(); println!("{:?}", end_result); // [1, 2, 3]

map_windows [Nightly]

This method creates an iterator over all contiguous windows of size n. As the name indicates, it provides a rolling window of a certain length on which you can run a closure or an operation.

You can try this method locally using the Rust nightly build or on Rust playground. In the code below, the average of the rolling 3 numbers is computed.


#![feature(iter_map_windows)] fn main() { let data: Vec<f64> = vec![12.0, 13.0, 14.0, 15.0, 16.0]; let moving_average: Vec<f64> = data.iter().map_windows(|[x, y, z]| (*x + *y + *z)/ 3 as f64).collect(); println!("{:?}", moving_average); } // [13.0, 14.0, 15.0]

find_map

This method helps find the first occurence of an element that satisfies certain criteria. For example if you are looking for the first integer in a vector of strings.


let v: Vec<&str> = vec!["one", "1", "two", "2", "three", "3"]; let first: Option<i32> = v.iter().find_map(|x| x.parse::<i32>().ok()); println!("{}", first.unwrap()); // 1

flat_map

With flat_map you can iterate over elements while flattening them at the same time. The example below demonstrates flattening arrays but this can be done to characters as well.


let v: Vec<[i32; 3]> = vec![[0, 1, 2], [3, 4, 5], [6, 7, 8]]; let result: Vec<i32> = v.iter().flat_map(|arr| arr.iter().copied()).collect(); println!("{:?}", result); // [0, 1, 2, 3, 4, 5, 6, 7, 8]



There are many other iterator methods in Rust. Although it might take a while to understand long lines with chained methods, I find they make the code richer with added variety.

Another Rust blog

(with the occasional DevOps article) 😬