Really confusing Error

I'm getting a lifetime error on this bit of code and I'm lost as what todo.

error[E0308]: mismatched types
   --> avtor-cli/src/main.rs:22:9
    |
22  |         create_super_user(this_find_super_user, this_insert_user, user_dto).await;
    |         ^^^^^^^^^^^^^^^^^ lifetime mismatch
    |
   ::: /home/decapo01/projects/avtor-rs/avtor-core/src/models/users.rs:193:6
    |
193 | ) -> impl FnOnce(&'a User) -> BoxFuture<'a, Result<(), CreateSuperUserError>> {
    |      ------------------------------------------------------------------------
    |      |
    |      the expected opaque type
    |      the found opaque type
    |
    = note: expected associated type `<impl FnOnce(&User)-> Pin<Box<dyn Future<Output = Result<(), CreateSuperUserError>> + Send>> as FnOnce<(&User,)>>::Output`
               found associated type `<impl FnOnce(&User)-> Pin<Box<dyn Future<Output = Result<(), CreateSuperUserError>> + Send>> as FnOnce<(&User,)>>::Output`
note: the lifetime requirement is introduced here
   --> /home/decapo01/projects/avtor-rs/avtor-core/src/models/users.rs:233:35
    |
233 |     insert: impl FnOnce(&User) -> FB,
    |                                   ^^

this is the offending function


pub async fn create_super_user<FA, FB>(
    find_super_user: impl FnOnce() -> FA,
    insert: impl FnOnce(&User) -> FB,
    user_dto: &UserDto,
) -> Result<(), CreateSuperUserError>
where
    FA: Future<Output = Result<Option<User>, CreateSuperUserError>>,
    FB: Future<Output = Result<(), CreateSuperUserError>>,
{
    let _ = user_dto.validate().map_err(|e| {
        let hash_map = hash_map_from_validation_errors(e);
        CreateSuperUserError::UserInvalid(hash_map)
    })?;
    let user = user_from_dto(user_dto.clone());
    let maybe_existing_user = find_super_user().await?;
    match maybe_existing_user {
        Some(_) => Err(CreateSuperUserError::SuperUserExists),
        None => {
            let ins_res = insert(&user).await;
            match ins_res {
                Ok(_) => Ok(()),
                Err(e) => Err(e)
            }
        },
    }
}

I'm injecting in two other functions and this is what they look like set up

async fn _create_super_user(client: Client, user_dto: &UserDto) {
    let this_insert_user = insert_user(&client);
    let this_find_super_user = find_super_user(&client);

    let x = 
        create_super_user(this_find_super_user, this_insert_user, user_dto).await;
    ()
}

The issue is that your insert function implements the trait for<'a> FnOnce(&'a User) -> FB, which means that it implements the following infinite list of traits, which goes through every possible lifetime:

  • for<'lifetime1> FnOnce(&'lifetime1 User) -> FB
  • for<'lifetime2> FnOnce(&'lifetime2 User) -> FB
  • for<'lifetime3> FnOnce(&'lifetime3 User) -> FB
  • ...
  • for<'static> FnOnce(&'static User) -> FB

So if you have a reference &'a User, then you can call the function with that reference no matter lifetime is annotated on it. However, the main thing to notice here is that the return type is the same no matter which lifetime you call it with. This means that the returned future cannot depend on the lifetime of the argument. This is a problem because an async fn will store the reference in the future.

One way to fix it is to use a helper trait like this:

use std::future::Future;

trait UserFnOnce<'a, T> {
    type Output: Future<Output = T> + 'a;
    
    fn call(self, user: &'a User) -> Self::Output;
}

impl<'a, T, F, Fut> UserFnOnce<'a, T> for F
where
    F: FnOnce(&'a User) -> Fut,
    Fut: Future<Output = T> + 'a,
{
    type Output = Fut;
    
    fn call(self, user: &'a User) -> Fut {
        self(user)
    }
}
pub async fn create_super_user<FA>(
    find_super_user: impl FnOnce() -> FA,
    insert: impl for<'a> UserFnOnce<'a, Result<(), CreateSuperUserError>>,
    user_dto: &UserDto,
) -> Result<(), CreateSuperUserError>
where
    FA: Future<Output = Result<Option<User>, CreateSuperUserError>>,
{

This works because it allows the returned future type to depend on the lifetime.

However, the compiler has some trouble with these helper traits. It works if you pass a function pointer like you're doing now, but the compiler gives some weird errors if you try to pass a closure.

There's also the easier fix: Don't use a &User as argument. For example, if you clone the user and pass it by ownership, the issue goes away because the argument has no lifetime. The helper trait is also rather complicated, so its better to avoid it if you can.

2 Likes

Thanks again for your help...I think I'm doing a lot of things the rust language wasn't really meant to do (at least the borrow checker) so I should probably use clone more. The clone route works here and I'm going to use it for most of the cases where I'm injecting these functions (Dependency Injection).