Search code examples
iteratorrustnested-loopslifetime

Specifying lifetimes in nested iterators for flattening


I'm writing a function that should take a couple of vectors and produce their Cartesian product (all of their pair combinations) as a vector in row-major order. In other words, if I have

let x_coords = vec![1, 2, 3];
let y_coords = vec![4, 5, 6];

I want to produce

vec![ [1,4], [1,5], [1,6], [2,4], [2,5], [2,6], [3,4], [3,5], [3,6] ]

This seemed like a perfect job for .flat_map():

fn main() {
    let x_coords = vec![1, 2, 3];
    let y_coords = vec![4, 5, 6];

    let coord_vec: Vec<[isize; 2]> =
        x_coords.iter().map(|&x_coord| {
            y_coords.iter().map(|&y_coord| {
                [x_coord, y_coord]
            })
        }).flat_map(|s| s).collect();

    // Expecting to get: vec![ [1,4], [1,5], [1,6], [2,4], [2,5], [2,6], [3,4], [3,5], [3,6] ]
    println!("{:?}", &coord_vec);
}

But this doesn't work, because &x_coord doesn't live long enough. According to the compiler, it ends up inside the y_coords map and then never makes it back out.

I tried using .clone() and move in the closures, but got a strangely long and unclear lecture by the compiler in the form of multiple Note: lines.

Am I just completely off base with flat_map, or can this be saved?


Solution

  • Solution

    You were really close! This works:

    let coord_vec: Vec<_> = x_coords.iter()
        .flat_map(|&x_coord| {
            y_coords.iter().map(move |&y_coord| {
            //                  ^^^^
                [x_coord, y_coord]
            })
        })
        .collect();
    

    The only thing I added is the move keyword in front of the inner closure. Why? Let's try to understand what the compiler is thinking below!

    A few notes, though:

    • I renamed map to flat_map and removed the second flat_map call... You made your life more complicated than it has to be ;-)
    • I omitted part of the type annotation of coord_vec, because it's not necessary

    Explanation

    The type x_coord is i32 (or any other integer). Not a reference or anything but the value directly. This means that x_coord is owned by the enclosing function, which happens to be a closure, specifically the "outer" closure, that you pass to flat_map. Thus x_coord lives just inside the closure, not longer. That's what the compiler is telling you. So far so good.

    When you define the second closure (the "inner" one), you access the environment, specifically x_coord. The important question now is: how does a closure access its environment? It can do so with an immutable reference, with a mutable reference and by value. The Rust compiler determines what kind of access to the environment is needed and chooses the "least intrusive" option that works. Let's look at your code: the compiler figures out that the closure only needs to borrow the environment immutably (because i32 is Copy and thus the closure can transform its &i32 into a i32 easily).

    But in this case, the compiler's reasoning is wrong! The closure borrowing its environment leads to a limited lifetime. And in this case we need the closure to live for longer than it can, hence the error.

    By adding move, we force the compiler to pass the environment to the closure by value (transferring ownership). This way the closure borrows nothing and can live forever (satisfies 'static).