Lifetime specification for closure return type

83 Views Asked by At

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?

2

There are 2 best solutions below

0
Chayim Friedman On BEST ANSWER

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.

0
EvilTak On

If your closure will only ever return references, you can explicitly specify the closure return type as a reference (and its lifetime) with a higher-ranked trait bound:

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

    LoggedArgument(self.0, self.1)
}

But if you cannot make this guarantee, your only option is to use a custom trait with a generic associated type (GAT):

trait Transform {
    type Input;
    type Output<'a>;

    fn transform<'a>(self, input: &'a Self::Input) -> Self::Output<'a>;
}

The drawback to this approach is that you cannot use closures anymore and have to manually create a new struct that implements Transform for every possible transformation you may need to make.

A fully generic approach for cases where the type returned by the closure is not a reference (i.e. F: for<'a> FnOnce(&'a T) -> T2<'a>) will not work without the custom trait approach as Rust does not yet support the concept of higher-kinded types.