Reputation: 152
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
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)
Upvotes: 3