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?
Unfortunately, this is impossible in current Rust. You have two options:
&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());
}
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.