rustclosurespattern-matchinglifetime

Why do I need an apparently useless type annotation when a closure matches against an enum-with-lifetime argument?


Here's a minimal reproduction for an issue I was having in a larger codebase:

enum A<'b> {
    A1(&'b u8)
}

fn consume_fnmut(_f: &mut impl FnMut (A)) {}
// Desugared:
// fn consume_fnmut<'a>(_f: &'a mut impl for<'b> FnMut (A<'b>)) {}

fn main() {
    let mut fnmut = |a| {
        match a {
            A::A1(_) => ()
        }
    };
    consume_fnmut(&mut fnmut);
}

Playground link

If I try to run this code, I get a "closure is not general enough" warning:

error: implementation of `FnMut` is not general enough
  --> src/main.rs:15:5
   |
15 |     consume_fnmut(&mut fnmut);
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^ implementation of `FnMut` is not general enough
   |
   = note: closure with signature `fn(A<'2>)` must implement `FnMut<(A<'1>,)>`, for any lifetime `'1`...
   = note: ...but it actually implements `FnMut<(A<'2>,)>`, for some specific lifetime `'2`

It looks like the Rust compiler has inferred my FnMut fnmut to take an argument of type A<'2> for some specific lifetime '2, which therefore isn't general enough to be passed to consume_fnmut (which requires the function referenced by its argument to implement for<'b> FnMut(A<'b>), i.e. to be able to work with arbitrary lifetimes).

I was able to fix this problem simply by explicitly specifying the type of the argument:

    let mut fnmut = |a: A| {
        match a {
            A::A1(_) => ()
        }
    };

Here, the a: A (with the lifetime 'b not mentioned at all) is a way to say "for all lifetimes 'b, a can be an A<'b>". The compiler accepts this code and it builds and runs without errors.

My question is: why is the type annotation needed in this context? From experimenting with minor variants of the problem, the cause seems to be the match statement – it is what causes the type of a to be inferred as A (because it is a pattern match against the variants of A), but I don't understand why it is inferring a specific lifetime '2 on A<'2>, rather than working with arbitrary lifetimes – it isn't even mentioning any of the fields of A::A1, so theoretically the lifetime should't matter!


Solution

  • Almost a year later, I discovered the answer to my own question.

    This is caused by a long-standing known deficiency in the Rust type inference algorithm. The basic issue is that the currently used algorithm has to decide, by the end of the let mut fnmut = … statement, whether the type of the closure is generic in the lifetime of the argument or not, and there's no evidence by that point of whether or not it should be generic, so it has to guess. Either guess (generic lifetime or specific lifetime) could break some potential future code. Unfortunately, the error message doesn't let you know "the type inference algorithm guessed that a specific lifetime was needed here", so if you haven't seen this problem before, it's hard to trace it back to the type inference algorithm making an incorrect guess as to how generic the closure should be.

    It works if the call to consume_fnmut appears before the declaration of the closure, because doing it that way round gives the type inference algorithm evidence about whether or not the closure is meant to be lifetime-generic or not:

    enum A<'b> {
        A1(&'b u8)
    }
    
    fn consume_fnmut(_f: &mut impl FnMut (A)) {}
    
    fn main() {
        consume_fnmut(&mut |a| {
            match a {
                A::A1(_) => ()
            }
        });
    }
    

    Neither the match nor the enum is actually necessary to trigger the issue in question; but most methods of specifying the closure that don't contain some sort of pattern match (match, or let with a non-trivial left-hand-side) will end up naming a type and giving a hint about how generic the closure should be in the process, allowing its genericness to be inferred correctly.

    This issue is currently considered to be a missing feature by the Rust developers, rather than a bug, presumably because the type inference algorithm wasn't expected to be able to handle this case. I think in theory it can always be worked around by giving an appropriate type annotation, but in some cases the type annotation might be hard to give in an elegant way (in extreme cases you might have to define a function fn assert_generic<F: impl for<'b> FnMut(A<'b>)>(f: F){ f } and construct the closure as an argument to that, in order to tell Rust how generic it was supposed to be without actually naming its type – that sort of workaround is needed because closures can't be named explicitly).

    There's a lot of relevant background in Rust RFC 3216, which is what pointed me to the correct Rust enhancement request, and is well worth reading for people who are interested in this topic.