AlphaModder
AlphaModder

Reputation: 3386

Why does this Rust code compile with a lifetime bound on the struct, but give a lifetime error if the bound is only on the impl?

Recently, I tried to write a piece of code similar to the following:

pub struct Foo<'a, F> /* where F: Fn(&u32) -> bool */ {
    u: &'a u32,
    f: F
}

impl<'a, F> Foo<'a, F> 
    where F: Fn(&u32) -> bool 
{
    pub fn new_foo<G: 'static>(&self, g: G) -> Foo<impl Fn(&u32) -> bool + '_>
        where G: Fn(&u32) -> bool 
    {
        Foo { u: self.u, f: move |x| (self.f)(x) && g(x) }
    }
}

Here, an instance of Foo represents a condition on a piece of data (the u32), where a more restrictive Foo can be built from a less restrictive one via new_foo, without consuming the old. However, the above code does not compile as written, but gives the rather cryptic error message:

error[E0308]: mismatched types
 --> src/lib.rs:9:52
  |
9 |     pub fn new_foo<G: 'static>(&self, g: G) -> Foo<impl Fn(&u32) -> bool + '_>
  |                                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^ one type is more general than the other
  |
  = note: expected type `std::ops::FnOnce<(&u32,)>`
             found type `std::ops::FnOnce<(&u32,)>`

error: higher-ranked subtype error
  --> src/lib.rs:9:5
   |
9  | /     pub fn new_foo<G: 'static>(&self, g: G) -> Foo<impl Fn(&u32) -> bool + '_>
10 | |         where G: Fn(&u32) -> bool 
11 | |     {
12 | |         Foo { u: self.u, f: move |x| (self.f)(x) && g(x) }
13 | |     }
   | |_____^

error: aborting due to 2 previous errors

After much experimentation, I did find a way to make the code compile, and I believe it then functions as intended. I am used to the convention of placing bounds on impls rather than declarations when the declaration can be written without relying on those bounds, but for some reason uncommenting the where clause above, that is, copying the bound F: Fn(&u32) -> bool from the impl to the declaration of Foo itself resolved the problem. However, I don't have a clue why this makes a difference (nor do I really understand the error message in the first place). Does anyone have an explanation of what's going on here?

Upvotes: 5

Views: 243

Answers (1)

eggyal
eggyal

Reputation: 126035

The only subtypes that exist in Rust are lifetimes, so your errors (cryptically) hint that there's some sort of lifetime problem at play. Furthermore, the error clearly points at the signature of your closure, which involves two lifetimes:

  1. the lifetime of the closure itself, which you have explicitly stated outlives the anonymous lifetime '_; and

  2. the lifetime of its argument &u32, which you have not explicitly stated, so a higher-ranked lifetime is inferred as if you had stated the following:

    pub fn new_foo<G: 'static>(&self, g: G) -> Foo<impl for<'b> Fn(&'b u32) -> bool + '_>
        where G: Fn(&u32) -> bool
    

Using the more explicit signature above gives a (very) slightly more helpful error:

error[E0308]: mismatched types
 --> src/lib.rs:9:52
  |
9 |     pub fn new_foo<G: 'static>(&self, g: G) -> Foo<impl for<'b> Fn(&'b u32) -> bool + '_>
  |                                                    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ one type is more general than the other
  |
  = note: expected type `std::ops::FnOnce<(&'b u32,)>`
             found type `std::ops::FnOnce<(&u32,)>`

At least we can now see that "one type is more general than the other": we expected a closure that can take an argument with any lifetime but for some reason Rust thinks that what we have instead is a closure that takes an argument that may have some more restricted range of lifetimes.

What's happened? Well, the function's return value is the following expression:

Foo { u: self.u, f: move |x| (self.f)(x) && g(x) }

This is of course an instance of struct Foo<'a, F>, where this F bears no relation to that declared on the impl block (with its trait bound). Indeed, since there's no explicit bound on F in the struct definition, the compiler must fully infer this type F from the expression itself. By giving the struct definition a trait bound, you are telling the compiler that instances of Foo, including the above expression, have an F that implements for<'b> Fn(&'b u32) -> bool: i.e. the range of lifetimes for the &u32 argument are unbounded.

Okay, so the compiler needs to infer F instead, and indeed it does infer that it implements Fn(&u32) -> bool. However, it's just not quite smart enough to determine to what range of lifetimes that &u32 argument might be restricted. Adding an explicit type annotation, as suggested in @rodrigo's comment above, states that the argument can indeed have any lifetime.

If there are in fact some restrictions on the possible lifetimes of the closure's argument, you would need to indicate that more explicitly by changing the definition of 'b from a higher-ranked trait bound (i.e. for<'b> in the return type above) to whatever is appropriate for your situation.

Hopefully once chalk is fully integrated into the compiler it will be able to perform this inference in both the unrestricted and restricted cases. In the meantime, the compiler is erring on the side of caution and not making potentially erroneous assumptions. The errors could definitely have been a bit more helpful, though!

Upvotes: 2

Related Questions