Functional Rust: Why is `filter` borrowing in closure?

Viewed 197

I'm trying to practice using Rust's functional capabilities.

For example, I would like to convert this loop:

levels: Vec<Vec<u8>> = //< Built by something

let mut total = 0;
for (x, y) in iproduct!(0..10, 0..10) {
    if levels[x][y] > 9 {
        total += count_something(levels, x, y);
    }
}

// Edit: here's the `count_something` function signature
fn count_something (levels: &mut Vec<Vec<u8>>, x: usize, y: usize) -> usize {
    // Count
}

This is the result of my functional refactoring:

iproduct!(0..10, 0..10)
    .filter(|(x, y)| levels[*x][*y] > 9)
    .map(|(x, y)| count_something(levels, x, y))
    .sum()

The problem is: this code won't compile.
The error: error[E0500]: closure requires unique access to *levels but it is already borrowed.

I don't see why filter borrows the levels 2D matrix.
My mental model of what is happening under the hood seems inadequate.

1 Answers

I don't see why filter borrows the levels 2D matrix. My mental model of what is happening under the hood seems inadequate.

filter's callback needs to access the matrix, so there's a borrow here (the alternative would be a move): in Rust a closure is syntactic sugar for a struct + a callable, any free variable is automatically transformed to a member of the implicit, anonymous struct:

.filter(|(x, y)| levels[*x][*y] > 9)

becomes (more or less, there's a lot being skipped)

struct $SECRET1$ {
    levels: &Vec<Vec<u8>>
}
impl $SECRET1$ {
    fn call(&self, x: &usize, y: &usize) -> bool {
        self.levels[*x][*y] > 9
    }
}
[...]
.filter($SECRET1$ { levels: &levels })

So that's why levels gets borrowed.

The other part of the question would then be why levels is still borrowed when the map runs, and the answer is that Rust's iterators are lazy, so they run the operations concurrently (interleaved), not sequentially.

Therefore when you write

iproduct!(0..10, 0..10)
    .filter(|(x, y)| levels[*x][*y] > 9)
    .map(|(x, y)| count_something(levels, x, y))
    .sum()

you're really writing something like:

let p = iproduct!(0..10, 0..10);
let f = Filter {
    f: |(x, y)| levels[*x][*y] > 9,
    iter: p
};
let m = Map {
    f: |(x, y)| count_something(levels, x, y),
    iter: f
};
Iterator::sum(m)

And so as you can see Map.f and Filter.f exist at the same time and need the same data, which would be fine if both just needed to read from it, but apparently count_something (and thus map) needs a mutable (unique) reference, and that's not compatible with a readonly (shared) reference.

Related