Search code examples
rustclosurestraitslifetime-scoping

Lifetime specification for closure return type


I have the following snippet of code (lifetimes attempts elided):

pub struct NamedArgument<T>(pub(in crate) &'static str, pub(in crate) T);

pub struct LoggedArgument<T>(pub(in crate) &'static str, pub(in crate) T);

impl<T> NamedArgument<T> {
    pub fn log_with<T2, F>(self, log_level: log::Level, transform: F) -> LoggedArgument<T>
    where
        T2: Display,
        F: FnOnce(&T) -> T2,
    {
        log::log!(log_level, "{} = {}", self.0, transform(&self.1));

        LoggedArgument(self.0, self.1)
    }
}

Which I would like to call as follows:

fn foo(argument: NamedArgument<impl AsRef<str>>) {
    argument.log_with(log::Level::Info, |s| s.as_ref());
}

But this fails with the following error:

error: lifetime may not live long enough
  --> src/lib.rs:20:45
   |
20 |     argument.log_with(log::Level::Info, |s| s.as_ref());
   |                                          -- ^^^^^^^^^^ returning this value requires that `'1` must outlive `'2`
   |                                          ||
   |                                          |return type of closure is &'2 str
   |                                          has type `&'1 impl AsRef<str>`

I do understand the issue (at least I think), but find myself unable to specify the lifetimes correctly. Essentially I need to specify that the returned T2 is outlived by the parameter passed in to the closure. I've tried dozens of combinations of lifetime specifications, including using higher-ranked trait bounds for the function trait.

How do I specify the lifetimes as required to make this system functional?


Solution

  • Unfortunately, this is impossible in current Rust. You have two options:

    1. Define the function to transform &T to &T2. This does limit it to functions that return references, so you can have two functions, one for references and the other for owned types. This still does not allow all types, but this does allow the absolute majority of them.
    impl<T> NamedArgument<T> {
        pub fn log_with<T2, F>(self, log_level: log::Level, transform: F) -> LoggedArgument<T>
        where
            T2: Display,
            F: FnOnce(&T) -> T2,
        {
            log::log!(log_level, "{} = {}", self.0, transform(&self.1));
    
            LoggedArgument(self.0, self.1)
        }
        
        pub fn log_with_reference<T2, F>(self, log_level: log::Level, transform: F) -> LoggedArgument<T>
        where
            T2: Display + ?Sized,
            F: FnOnce(&T) -> &T2,
        {
            log::log!(log_level, "{} = {}", self.0, transform(&self.1));
    
            LoggedArgument(self.0, self.1)
        }
    }
    
    fn foo(argument: NamedArgument<impl AsRef<str>>) {
        argument.log_with_reference(log::Level::Info, |s| s.as_ref());
    }
    
    1. Define a custom trait for the transformer. With a GATified trait, you can cover all possible types, and you can also have the convenience of closure syntax for the majority of cases by implementing the trait for owned functions, but you will have to use the less convenient explicit struct and trait implementation for everything else:
    pub trait Transformer<T> {
        type Output<'a>: Display
        where
            T: 'a;
    
        fn transform(self, arg: &T) -> Self::Output<'_>;
    }
    
    impl<T1, T2, F> Transformer<T1> for F
    where
        F: FnOnce(&T1) -> T2,
        T2: Display,
    {
        type Output<'a> = T2
        where
            T1: 'a;
    
        fn transform(self, arg: &T1) -> Self::Output<'_> {
            self(arg)
        }
    }
    
    impl<T> NamedArgument<T> {
        pub fn log_with<F>(self, log_level: log::Level, transform: F) -> LoggedArgument<T>
        where
            F: Transformer<T>,
        {
            log::log!(log_level, "{} = {}", self.0, transform.transform(&self.1));
    
            LoggedArgument(self.0, self.1)
        }
    }
    
    fn foo(argument: NamedArgument<impl AsRef<str>>) {
        argument.log_with(log::Level::Info, {
            struct MyTransformer<T>(std::marker::PhantomData<T>);
    
            impl<T: AsRef<str>> Transformer<T> for MyTransformer<T> {
                type Output<'a> = &'a str
                where
                    T: 'a;
    
                fn transform(self, s: &T) -> Self::Output<'_> {
                    s.as_ref()
                }
            }
    
            MyTransformer(std::marker::PhantomData)
        });
    }
    

    Yep, that's long. That's the disadvantage.