kyp4
kyp4

Reputation: 152

Rust nested closure moves and multiple owners

Here is a MWE of the issue I am having, which does not compile:

use std::collections::HashSet;
use nom::{
    IResult,
    error::VerboseError,
    bytes::complete::is_not,
    character::complete::space1,
    combinator::map,
    multi::separated_list1,
};

type Set = HashSet<char>;
fn _make_parser(reducer: impl Fn(Set, Set) -> Set) -> impl Fn(&str) -> IResult<&str, Set, VerboseError<&str>> {
    move |input| {
        map(
            separated_list1(
                space1,
                is_not(" \t"),
            ),
            |vec: Vec<&str>| {
                vec.iter().map(|s| s.chars().collect::<Set>())
                    .reduce(reducer).unwrap()
            }
        )(input)
    }
}

fn main() {
    println!("Do nothing");
}

Though not important, _make_parser is intended to create and return a nom parser function that will take a space-delimited set of strings like the following:

abc acdsodin ac azcefgd

The parser should then convert each string to a set (i.e. a HashSet) of char and apply some operation across the sets, passed as the reducer closure to _make_parser. This closure is intended to call something like HashSet::union or HashSet::intersection.

The code fails to compile with the following errors:

error[E0507]: cannot move out of `reducer`, a captured variable in an `FnMut` closure
  --> src/main.rs:21:33
   |
12 |     fn _make_parser(reducer: impl Fn(Set, Set) -> Set) -> impl Fn(&str) -> IResult<&str, Set, VerboseError<&str>> {
   |                     ------- captured outer variable
...
21 |                         .reduce(reducer).unwrap()
   |                                 ^^^^^^^ move occurs because `reducer` has type `impl Fn(Set, Set) -> Set`, which does not implement the `Copy` trait

error[E0507]: cannot move out of `reducer`, a captured variable in an `Fn` closure
  --> src/main.rs:19:17
   |
12 |     fn _make_parser(reducer: impl Fn(Set, Set) -> Set) -> impl Fn(&str) -> IResult<&str, Set, VerboseError<&str>> {
   |                     ------- captured outer variable
...
19 |                 |vec: Vec<&str>| {
   |                 ^^^^^^^^^^^^^^^^ move out of `reducer` occurs here
20 |                     vec.iter().map(|s| s.chars().collect::<Set>())
21 |                         .reduce(reducer).unwrap()
   |                                 -------
   |                                 |
   |                                 move occurs because `reducer` has type `impl Fn(Set, Set) -> Set`, which does not implement the `Copy` trait
   |                                 move occurs due to use in closure

Clearly all the action is in the _make_parser function, and we've got a lot of closures going on. Let's call the closure returned by _make_parser the parser closure and the closure passed to the nom map combinator the map closure.

What I think is happening is that, the reducer closure (or more accuracy the implicit environment struct associated with the closure) is moved into the parser closer since we used the move keyword. This is what we want since we need the reducer closure to outlive the _make_parser function. It then seems like reducer is also being moved into the map closure, which is fine as well since we don't need it within the parser closure except inside of the map closure. It also seems like the reduce method is also trying to take ownership of reducer as well (I tried to make this a borrow from the map closure but it won't accept it). This seems like it should not be a problem because the map closure is really called only once. However the type of the closure taken by map combinator is a FnMut since in certain situations this could be called more than once (e.g. if the map combinator was nested inside a repeating combinator). My suspicion is that this is problematic because reducer is getting moved into the reduce method, which precludes calling the map closure more than once.

Is my interpretation of the error message correct, or is my understanding of the situation way off? If so how could this be solved? It seems like maybe it could be solved using reference counting to support multiple owners but, since reduce needs a closure and not an Rc<closure> I could not get this to work. Note that I am just learning Rust and haven't really used reference counters yet, though I think I basically understand how they work.

Lastly, note that this compiles just fine if I change the reducer type in the _make_parser to a normal fn instead of a closure Fn, which I think makes sense since in that case there is no implicit environment struct to worry about moving. This would even work fine for my actual purpose since I don't actually need to capture any environment in my reducer. However, I read that its better (i.e. more general) to accept closures instead of normal functions, and I'm also just really curious how this could be made to work with _make_parser accepting a closure or whether it's just not possible for some reason.

Upvotes: 0

Views: 1316

Answers (1)

Svetlin Zarev
Svetlin Zarev

Reputation: 15673

The issue is that your reducer cannot be copied or cloned, but is used inside another closure, which moves it out of itself because of that.

Your closure move |input| {} captures reducer and passes it as argument to reduce(). But as you know, Rust has move semantics, so every time you pass something as argument, the compiler will move it. Thus making your closure being able to be called only once, but it has the type Fn which by contract requires it to be callable multiple times.

So in order to solve the issue, you either have to be able to copy or clone the reducer closure. Given that you've said that it can be a simple fn in your case, then you can simply make it Copy:

use std::collections::HashSet;
use nom::{
    IResult,
    error::VerboseError,
    bytes::complete::is_not,
    character::complete::space1,
    combinator::map,
    multi::separated_list1,
};

type Set = HashSet<char>;
fn _make_parser(reducer: impl Copy + Fn(Set, Set) -> Set) -> impl Fn(&str) -> IResult<&str, Set, VerboseError<&str>> {
    move |input| {
        map(
            separated_list1(
                space1,
                is_not(" \t"),
            ),
            |vec: Vec<&str>| {
                vec.iter().map(|s| s.chars().collect::<Set>())
                    .reduce(reducer).unwrap()
            }
        )(input)
    }
}

fn main() {
    println!("Do nothing");
}

Alternatively, if you capture non-Copy types from the environment you can make it Clone in order to make it less restrictive and pass a clone as argument to reduce: .reduce(reducer.clone)

Rust Playground Link

Upvotes: 3

Related Questions