Issue with closure and ownership

The following code gives a compile-time error:

  • error[E0506]: cannot assign to 'found_part' because it is borrowed
fn match_vectors(xs: &Vec<String>, ys: &Vec<String>) {
    let mut found_part = false;

    let mut match_pair = |x: &str, y: &str| {
        if y.starts_with(x) {
            found_part = true;
        }
    };

    for x in xs {
        found_part = false;

        for y in ys {
            match_pair(x, y);
        }
    }
}

Playground

As far as I understand, the ownership of found_part is moved into the match_pair closure. Therefore it is no longer available in the next loop iteration.

Is there a way to fix this without inlining the closure?

Here is an alternative approach using a State struct, which should be a bit less performant due to Clone/Copy:

fn match_vectors(xs: &Vec<String>, ys: &Vec<String>) {
    #[derive(Clone, Copy)]
    struct State {
        found_part: bool
    }

    fn match_pair(x: &str, y: &str, mut s: State) {
        if y.starts_with(x) {
            s.found_part = true;
        }
    }

    let mut s = State {
        found_part: false 
    };

    for x in xs {
        s.found_part = false;

        for y in ys {
            match_pair(x, y, s);
        }
    }
}

Playground

match_pair captures found_part by mutable reference. So while match_pair exists, it holds a unique mutable reference to found_part, which means you can't otherwise write to it, which you are attempting to do in your for x in xs loop.

What about making the closure stateless by returning the bool from it, rather than setting the found_part flag?

fn match_strings(xs: &[String], ys: &[String]) {
    let match_pair = |x: &str, y: &str| {
        y.starts_with(x)
    };

    for x in xs {
        let mut found_part = false;

        for y in ys {
            found_part = match_pair(x, y);
        }
    }
}

Playground.


As a side note, passing &Vec<_> as argument is discouraged, you shoud use &[_] instead, which is more flexible.

2 Likes

Thanks! The production code has a bit more variables (see below). In principle returning a tuple of boolean variables would work here as well, although inlining the closure might be a bit simpler to understand.

fn match_strings(xs: &[String], ys: &[String]) -> bool {
    let mut has_matched_x = vec![false; xs.len()];
    let mut has_matched_y = vec![false; ys.len()];
    let mut has_big_part = false;
    let mut is_match = true;
    let mut found_part = false;
    
    let mut match_pair = |(x, i): (&str, usize), (y, j): (&str, usize)| {
        if y.starts_with(x) && !has_matched_x[i] && !has_matched_y[j] {
            has_matched_x[i] = true;
            has_matched_y[j] = true;
            found_part = true;
            has_big_part |= x.len() > 1;
        }
    };

    for (i, x) in xs.iter().enumerate() {
        found_part = false;

        for (j, y) in ys.iter().enumerate() {
            match_pair((x, i), (y, j));
        }

        is_match = is_match && found_part;
    }

    is_match
}

Playground

The problem is still just the found_part variable. How about this:

fn match_strings(xs: &[String], ys: &[String]) -> bool {
    let mut has_matched_x = vec![false; xs.len()];
    let mut has_matched_y = vec![false; ys.len()];
    let mut has_big_part = false;
    let mut is_match = true;

    let mut match_pair = |(x, i): (&str, usize), (y, j): (&str, usize)| {
        if y.starts_with(x) && !has_matched_x[i] && !has_matched_y[j] {
            has_matched_x[i] = true;
            has_matched_y[j] = true;
            has_big_part |= x.len() > 1;
            return true;
        }
        false
    };

    for (i, x) in xs.iter().enumerate() {
        let mut found_part = false;

        for (j, y) in ys.iter().enumerate() {
            found_part |= match_pair((x, i), (y, j));
        }

        is_match = is_match && found_part;
    }

    is_match
}

Playground.

2 Likes

If you would like to not restructure your code, then another option is to use std::cell::Cell<bool> in place of bool. That allows mutation through a non-exclusive borrow, so it avoids the conflict.

3 Likes

@jofas Oh right, didn't realize found_part was the only issue here :slight_smile:

@kpreid Thanks for the pointer, Cell is definitely a nice escape hatch here :sunny: