Adam Johnston

Learning Rust №1 - Writing a map function

What is map?

Map is a common higher order function which takes a list, loops over said list and applies a given function against each item creating a new list from the results. For example, say we have a list of numbers [1, 2, 3] and we have a function that adds one to a number number => number + 1, when we loop over our list with this function we will have a new list containing [2, 3, 4]. Read more about Map on Wikipedia

Writing a map function in Rust

Something to note is that Rust ships its own Map function in its standard library but as a learning exercise I feel a good way to learn the language is to implement things you already know. So this is my current understanding of how to implement map in Rust. Not sure it’s the most performant it could be or even very elegant code but I think it’s readable for the most part.

fn map<A, B>(list: &Vec<A>, mapper: Box<dyn Fn(&A) -> B>) -> Vec<B> {
    let mut mapped: Vec<B> = Vec::new();

    for item in list {
        mapped.push(mapper(item));
    }

    mapped
}

fn main() {
    let numbers: Vec<u32> = vec![2, 3, 4];
    let squared: Vec<u32> = map(&numbers, Box::new(| x: &u32 | x * x));

    println!("Mapped {:?}", squared); // [4, 9, 16]
}

So lets break this down and share my current understanding of how this works. I’ll skip over main as its purpose is to run the program and simply print the result which is [4, 9, 16].

Passing values by reference or borrowing

One unique aspect of Rust is its ownership system. We don’t want to give the map function the ownership of the original list, in the example numbers. The reason for this is we expect to return a new list which would mean that numbers would go out of scope and be cleared. If we passed the original list then you’d not be able to use the after calling map with it. To avoid this we pass it by reference.

// list is owned by map.
fn map<A, B>(list: Vec<A>, mapper: Box<dyn Fn(A) -> B>) -> Vec<B> {
    // …
}

// list is borrowed as denoted by &Vec<A> which also means that the 
// closure that is provided as a mapper fn receives an item value 
// also by reference.
fn map<A, B>(list: &Vec<A>, mapper: Box<dyn Fn(&A) -> B>) -> Vec<B> {
    // …
}

Box and dynamic values at runtime

For values that cannot be determined (maybe sized) at runtime, you need to wrap it in Box along with the dyn keyword. This means this value will be allocated to heap memory rather than the stack. This makes writing the closure function a bit annoying and likely less performant. I am sure there’s ways around this but I am not sure how right now.

Box::new(| x: &u32 | x + 1)

Using Rust’s map function

To do the above with Rust’s built in map function would look something like this:

fn main() {
    let numbers: Vec<u32> = vec![2, 3, 4];
    let squared: Vec<u32> = numbers.iter().map(|x: &u32| x * x).collect::<Vec<u32>>();

    println!("Mapped {:?}", squared) // [4, 9, 16]
}

So let’s dig into this. We take our vector which is a collection, we turn it into an iterator by calling .iter(), once we have an iterator we can call .map() which returns a new iterator so finally we call .collect() to turn it back into a collection.