Error when passing a generic type through callbacks if the type is actually a reference

Hi!
I have the following code failed to compile:

fn process<IF, F, Item>(iter_func: IF, mut handler: F)
where
    IF: FnOnce(&mut dyn FnMut(Item)),
    F: FnMut(Item),
{
    iter_func(&mut |item| handler(item))
}

fn iter_units(_func: &mut dyn FnMut(&())) {}

fn main() {
    process(|func| iter_units(func), |_| ());
}

(Playground)

Error:

error[E0308]: mismatched types
  --> src/main.rs:12:30
   |
12 |     helper(|func| iter_units(func), |_| ());
   |                              ^^^^ one type is more general than the other
   |
   = note: expected trait object `dyn for<'a> FnMut(&'a ())`
              found trait object `dyn FnMut(&())`

Is it a known issue or my mistake?

It should be noted that the similar code where the type Item is replaced by the concrete &() (see below process_units) works fine, as well as for the case when the lifetime for &() is defined but not bound for iter_units_extra_lt (passed into the original process) so the lifetime is required to be longer than in the original case:

fn process<IF, F, Item>(iter_func: IF, mut handler: F)
where
    IF: FnOnce(&mut dyn FnMut(Item)),
    F: FnMut(Item),
{
    iter_func(&mut |item| handler(item))
}

// works with a concrete item type specified in the helper
fn process_units<IF, F>(iter_func: IF, mut handler: F)
where
    IF: FnOnce(&mut dyn FnMut(&())),
    F: FnMut(&()),
{
    iter_func(&mut |item| handler(item))
}

// fails to compile with process()
fn iter_units(_func: &mut dyn FnMut(&())) {}
// fails to compile as well
#[allow(dead_code)]
fn iter_units2(_func: &mut dyn for<'a> FnMut(&'a ())) {}
// works with an extra lifetime
fn iter_units_extra_lt<'a>(_func: &mut dyn FnMut(&'a ())) {}
// works as well since there is no ref in item
fn iter_units_copied(_func: &mut dyn FnMut(())) {}

fn main() {
    // fails to compile
    // process(|func| iter_units(func), |_| ());
    // fails to compile as well
    // process(|func| iter_units2(func), |_| ());

    // works with an extra lifetime
    process(|func| iter_units_extra_lt(func), |_| ());
    // works as well since there is no ref in item
    process(|func| iter_units_copied(func), |_| ());
    // works with a concrete item type specified in the helper
    process_units(|func| iter_units(func), |_| ());
}

(playground ok)

Any ideas are appreciated!

Broadly, the problem is that &() is not one type — it always has a lifetime, whether or not you write that lifetime explicitly. Then when functions get involved, dyn for<'a> FnMut(&'a ()) is a different type than dyn FnMut(&'b ()) (with 'b being declared somewhere outside). You have to pick which one you want to work with.

  • If you want dyn FnMut(&'b ()), then your iter_units_extra_lt is one possible solution.

  • If you want want dyn for<'a> FnMut(&'a ()), then process must have for<'a> in it:

    fn process<IF, F, Item>(iter_func: IF, mut handler: F)
    where
        IF: FnOnce(&mut dyn for<'a> FnMut(&'a Item)),
        F: for<'a> FnMut(&'a Item),
    {
        iter_func(&mut |item| handler(item))
    }
    
3 Likes

Something that potentially makes this case more confusing is that the Fn traits and dyns have special syntactic sugar, where elided input lifetimes correspond to being higher ranked. So these two signatures are the same:

fn iter_units(_func: &mut dyn FnMut(&())) {}
fn iter_units2(_func: &mut dyn for<'a> FnMut(&'a ())) {}
// _func can accept any lifetime on its `&()` input
// (This is the only way it can accept local borrows within `iter_units*`)

But this one is different:

fn iter_units_extra_lt<'a>(_func: &mut dyn FnMut(&'a ())) {}
// _func can only accept a single lifetime `'a` on its `&()` input
// (every possible `'a` is at least just longer than the function call)

In contrast with how lifetime elision works with, say, bare references in function signatures (where elision introduces a new parameter on the function item).

// These two are the same
fn ex1(_: &mut &()) {}
fn ex2<'a>(_: &mut &'a ()) {}
1 Like

Thanks for your clarification!

Thanks for your useful suggestions!
It should be noted that iter_units_extra_lt has limited usage - unfortunately it is not suitable for a local object (for the function) to be passed by ref.
Your second variant having the explicit lifetime extracted from Item (IF: FnOnce(&mut dyn for<'a> FnMut(&'a Item))) can be only used for an actual type corresponding for Item not being another local ref as I understand. Though it is a partial solution, it really solves such concrete problem of passing a local object by ref from inside iter_units so your variant could be marked as solution.
The next question from the point - is it possible to write a generic helper process that can cover the both cases - passing a local object by value and by ref? Or even more advanced case of a nested refs like &&() etc?
I don't know for sure - is it necessary to create a separate topic for a more complex problem that was in the background of the original topic, marking the latter as solved, or is it better to continue the thread here?

It works when Item is not 'static.

fn example<'s>(mut vec: &mut Vec<&'s str>) {
    // `Item = &'s str`
    process(|_| {}, |item| vec.push(*item));
}

Sometimes HRTBs -- F: for<'a> ... -- can end up requiring something to be 'static. But because &'a Item shows up in the inputs to the FnMut trait here, there's an implicit where Item: 'a constraint on the HRTB.

Is it enough for your actual use case? That I'm less sure about.

It works for local lifetimes too if that's what you meant.

    let local = "Hi".to_owned();
    let mut vec = vec![&*local];
    process(|_| {}, |item| vec.push(*item));

It is possible to generalize over "taking something owned or taking something borrowed", but such approaches tend to not be fully generic or to wreck inference. They resemble the general shape that is presented in the "alternative to GATs" article.

Here's an example from awhile ago. I've wanted to revisit that and see if something from this comment could help with the inference challenges, but haven't got around to it.

If you have a specific set of types (or type constructors) that you wish Item could be, there may be a more targeted solution.

1 Like

Thanks a lot for examples and links, I definitely need to read the materials thoroughly.

As for cases of Item that should not work, as I was trying to say, it might not be entirely clear - I meant a ref to an object that is local to iter_unit driver function rather local for a some scope enclosing process call that would narrow down the usage. When I had written 'static` I realized that some non-static sub refs can actually work - and that was then perfectly demonstrated by your examples. Counter examples for sub-refs that I had in mind:

fn iter_units(func: &mut dyn FnMut(&&())) {
    // Item=&()
    let x = ();
    func(&&x)
}

It fails for the considered approach IF: FnOnce(&mut dyn FnMut(&Item)) and requires ref doubling: &&Item. Then we can consider &&&(), etc.

Personally my case is quite simple: need to handle owned and borrowed values that do not have additional refs hidden in Item. So I can just have two separate version of process to make them to work.
So it is more a matter of perfectionism and learning whether it is possible to combine the both cases in one function. So for more general cases with sub-refs.

Just realized that my personal case includes a bit more complex sample, something like Cow<'a, _> having ref inside the type rather external one like in &(). Nevertheless it could be still fit into iter_units_extra_lt approach, but requires the ref to outlive the driver (and helper) function call (to be used with process helper):

fn iter_cows_extra_lt<'a, T>(v: Cow<'a, T>, func: &mut dyn FnMut(Cow<'a, T>)) {
    func(v)
}

So it is still open problem how to pass something like Cow containing a borrow to a local object located inside the driver function (via a helper process generic over Item):

fn iter_local_cows(func: &mut dyn FnMut(Cow<str>)) {
    let s = "Hi".to_owned();
    func(s.as_str().into())
}

The both known approaches are not applicable for the case.

So you want to support all of...

    // Types with no lifetime parameters
    IF: FnOnce(&mut dyn FnMut(Item)),
    // References
    IF: FnOnce(&mut dyn for<'a> FnMut(&'a Item)),
    // Cows
    IF: FnOnce(&mut dyn for<'a> FnMut(Cow<'a, Item>)),

Is that correct?