How to filter, based on another collection being mutated?

Hi there, I'm currently working on some code, which can basically be reduced to the following canonical form, consisting in iterating over a filtered collection, where the filter is computed based on another collection being mutated during the iterations.

fn does_not_compile(set: &mut Set<u32>, items: Vec<u32>) {
    for item in items.into_iter().filter(|item| !set.contains(&item)) {
        set.insert(item);
    }
}

Unfortunately, the compiler complains about using set both with mutable and immutable references at the same time, due to the closure capture. However, I know that the same thing (semantically) can be done if we are a bit more explicit and do our filtering manually.

fn does_compile(set: &mut Set<u32>, items: Vec<u32>) {
    for item in items {
        if set.contains(&item) {
            continue;
        }
        set.insert(item);
    }
}

So I'm wondering if there is a way to tell rust somehow that the closure should update its reference to the set every time it's called within the iterator instead of binding it once and for all before the iterations start. That would enable compilation of the expression with the filter, which is much more explicit.

PS: here is a playground with both functions Rust Playground

I don't myself think this solution is better than what you have, but to get started, RefCell and/or Cell can be used to move from static borrow checking to "dynamic borrow checking".

And yes, we can put a &mut Set inside a RefCell - the refcell can contain any value, including a reference, it doesn't have to own the Set.

use std::cell::RefCell;

fn did_not_compile_but_now_it_does(set: &mut Set<u32>, items: Vec<u32>) {
    let set = RefCell::new(set);
    for item in items.into_iter().filter(|item| !set.borrow().contains(&item)) {
        set.borrow_mut().insert(item);
    }
}

I think using RefCell kind of matches what you said, but it's not very smooth.

Maybe we want some sort of state machine where the active ownership of the mutable reference is continually passed from the filter to the "for each" step of the loop, and back. I'm not quite sure what that would look like.. But just merging filtering and mapping into one step is a lot simpler, for sure - filter_map or anything similar, like your original loop.

1 Like

insert returns true if the inserted item was not yet present.

fn does_compile(set: &mut Set<u32>, items: Vec<u32>) {
    for item in items.into_iter().filter(|item| set.insert(*item)) {

    }
}
3 Likes

Thanks @quinedot but this example with Set.insert was more of a minimal case, but in reality, I have a lot of processing to do when the item is not filtered out, and such processing is generating a value that I'm using to update a hash map (instead of a set).

I personally find the loop-based solution nicer than the iterator-based one. I would especially not resort to interior mutability in this case, if a simple loop and continue solves the problem.

1 Like

The loop with a continue is rather simple to understand, but in fact I think the RefCell approach is the closest thing to that closure behavior which dynamically takes a reference to the thing in the loop I was looking for. Except the dynamic binding is explicitly handled by a cell wrapper and borrow() calls. So I believe @bluss answer is a solution to that question of mine. I'll keep the continue without filter though since it will be easier to understand for people reading my code.

The fact that it can be solved without RefCell indicates that there's nothing inherently dynamic going on here. If you can rely on static, compile-time guarantees instead of runtime checks, then it is better to do so, if only for the reason that other readers of the code will likely frown upon an unnecessary use of dynamism.

It's the use of a closure that imposes that dynamic behavior. Do you think the compiler and borrow checker could some time be smart enough to figure this one out? Is there a collection of borrow checking examples of possible improvements where I should post this?

I do not think that its realistic that you can change the borrow-checker so that it would accept this.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.