Why impl<'a> Trait &'a str {} does mean the impl is not for all lifetimes

I'm reading You Can't Do That: Abstracting over Ownership in Rust with Higher-Rank Type Bounds. Or Can You? | Armin Ronacher's Thoughts and Writings
For the following trait impl

impl<'a> TryConvertValue for &'a str {
    fn try_convert_value(value: &'a Value) -> Option<&'a str> {
        match value {
            Value::String(s) => Some(s),
            _ => None,
        }
    }
}

the rustc says it only impl for a specific lifetime 'a and impl trait for &str is impling for any lifetime.
Following is the error from the blog

error: implementation of `TryConvertValue` is not general enough
--> src/main.rs:21:20
|
21 |     let to_upper = ArgCallback::new(|a: &str| Value::String(a.to_uppercase()));
|                    ^^^^^^^^^^^^^^^^ implementation of `TryConvertValue` is not general enough
|
= note: `TryConvertValue<'0>` would have to be implemented for the type `&str`, for any lifetime `'0`...
= note: ...but `TryConvertValue<'1>` is actually implemented for the type `&'1 str`, for some specific lifetime

I understand that impl trait for &str is for all lifetimes as there is no lifetime constraint.

But I don't get it why the impl<'a> TryConvertValue for &'a str is not for all lifetimes.

Apparently, no matter which lifetime 'a is, the impl is valid, i.e, for any lifetime 'a, I can call the func &str.try_convert_value() to get a `Option<&str>.
The code is in the reply
Appreciate any help to clear my doubt!

No, there's no semantic difference. Rust has "lifetime elision", which is just a syntax sugar that allows you to skip typing 'a, but the lifetimes are still there.

impl T for &str {} is equivalent of impl T for &'_ str, which is the same as impl<'a> T for &'a str {}.

In all cases these are generic over lifetime, so work with any lifetime.

You can have impl T for &'static str {} where 'static is one special lifetime for leaked memory and things hardcoded into the executable.

1 Like

Thanks for the quick reply. I have pasted the error message that says it is not for all the lifetime. Could you check it again?

Even though a trait implementation may be for any lifetime, the specific instances of that type can be for specific short lifetimes.

This is like Vec<T> is for any type, but Vec<String> is not compatible with Vec<u32>. So you can have impl<'any> Trait for &'any str, but then have &'a str and &'b str that you can't use interchangeably.

In your case the definition of TryConvertValue is odd, because it's implemented for a str, but it takes Value as an argument. These are different types that you require to have same lifetime. You probably should design it to be implemented for Value and take &self as an argument. See also AsRef (for borrowed views) and TryFrom (for owned conversions) traits.

The code in the first block can't compile for reasons other than the error currently in the top comment. Do you have a full reproduction?

(It's OK to just post new comments vs editing the OP.)

1 Like

the error message is a bit misleading, the impl<'a> Trait for &'a str {} IS generic over all lifetime, error is because the CallbackTrait requirement is different than what the TryConver trait provided.

when you call ArgCallback::new() with an closure:

    let to_upper = ArgCallback::new(|a: &str| Value::String(a.to_uppercase()));

the closure has the an compiler generated opaque type impl Fn(&str) -> Value, and the blanket impl for closures has such bounds:

impl<Func, Arg> CallbackTrait<Arg> for Func
where
    Func: Fn(Arg) -> Value + Send + Sync + 'static,
    Arg: for<'a> TryConvertValue<'a>,
{
    //...
}

plug in the (opaque) closure type, we can see Arg here is infered as &'x str for some lifetime 'x, which we know implements TryConvertValue<'x>, but the type bounds requires Arg (i.e. &'x str) must implement TryConvertValue<'a> for all possible lifetime 'a (which is unconnected in anyway to the lifetime argument of Arg itself, 'x)

so, the generic argument Arg (remember, it's a reference type) itself contains a lifetime argument, over which the TryConvertValue trait is generically implemented, but not for an arbitrary lifetime that is not related to Arg. so if we want TryConvertValue and CallbackTrait be compatible, we would need to be able to:

// pseudo code, it can't be done
type Arg<'x> = &`x str;
impl<'a, 'x> TryConvert<'a> for Arg<'x> {
    fn try_convert_value(value: &'a Value) -> Option<Arg<'x>> {
        match value {
            Value::String(s) => {
                // we have: `s: &'a str`
                // but we want to return: &'x str
                // ???
            }
            _ => None,
        }
    }
}

to make TryConvertValue work in this case, we could add a sub-type bounds 'a: 'x instead of for all 'x and 'a, but then we cannot express the same relation in the CallbackTrait, so we put an universal qualifier there, because we can't express the bounds precisely.

in other words, the bounds

    Arg: for<'a> TryConvertValue<'a>,

should really be

impl<Func, Arg<'_>> CallbackTrait<Arg> for Func
where
    Func: Fn(Arg) -> Value + Send + Sync + 'static,
    for <'a> Arg<'a>: TryConvertValue<'a>,

but this is invalid in rust (yet???)

1 Like

This is the reproducible code. Note the last statement is the failed line.

#[derive(Debug)]
enum Value {
    String(String),
    Number(i64),
}

trait TryConvertValue<'a>: Sized {
    fn try_convert_value(value: &'a Value) -> Option<Self>;
}

fn convert<'a, T: TryConvertValue<'a>>(value: &'a Value) -> Option<T> {
    T::try_convert_value(value)
}



impl<'a> TryConvertValue<'a> for &'a str {
    fn try_convert_value(value: &'a Value) -> Option<&'a str> {
        match value {
            Value::String(s) => Some(s),
            _ => None,
        }
    }
}

impl<'a> TryConvertValue<'a> for i64 {
    fn try_convert_value(value: &'a Value) -> Option<i64> {
        match value {
            Value::String(_) => None,
            Value::Number(number) => Some(*number),
        }
    }
}

struct ArgCallback(Box<dyn Fn(&Value) -> Value + Sync + Send + 'static>);

impl ArgCallback {
    pub fn new<F, Arg>(f: F) -> ArgCallback
    where
        F: CallbackTrait<Arg>,
        Arg: for<'a> TryConvertValue<'a>,
    {
        ArgCallback(Box::new(move |arg| -> Value {
            // since i'm lazy this will just panic for this demo
            f.invoke(convert(arg).unwrap())
        }))
    }

    pub fn invoke(&self, arg: &Value) -> Value {
        (self.0)(arg)
    }
}

trait CallbackTrait<Arg>: Send + Sync + 'static {
    fn invoke(&self, args: Arg) -> Value;
}

impl<Func, Arg> CallbackTrait<Arg> for Func
where
    Func: Fn(Arg) -> Value + Send + Sync + 'static,
    Arg: for<'a> TryConvertValue<'a>,
{
    fn invoke(&self, arg: Arg) -> Value {
        (self)(arg)
    }
}


fn main() {

let square = ArgCallback::new(|a: i64| Value::Number(a * a));
dbg!(square.invoke(&Value::Number(2)));
let to_upper = ArgCallback::new(|a: &str| Value::String(a.to_uppercase()));
// the above line cannot compile with
//error: implementation of `TryConvertValue` is not general enough
 // --> src/main.rs:73:16
 //  |
//73 | let to_upper = ArgCallback::new(|a: &str| Value::String(a.to_uppercase()));
   //|                ^^^^^^^^^^^^^^^^ implementation of `TryConvertValue` is not general enough
 //  |
 //  = note: `TryConvertValue<'0>` would have to be implemented for the type `&str`, for any lifetime `'0`...
  // = note: ...but `TryConvertValue<'1>` is actually implemented for the type `&'1 str`, for some specific lifetime `'1`

//warning: `playground` (bin "playground") generated 1 warning
//error: could not compile `playground` due to previous error; 1 warning emitted
}

Great thanks for the detailed explanation!!! :+1:
A small question for

Did you mean "the blanket impl for closures has no such bounds"?

After reading your reply, I think I understand the whole story! Thanks again :100:
( I will spend some time digesting it)

by the way, the yet nvalid rust code mentioned looks like something in this rfc https://github.com/tema3210/rfcs/blob/extended_hrtbs/text/3621-extended_hrtb.md

Thanks for the reply. I believe nerditation's answer solve the issue I faced.
I had the same feeling about the same thing as you list, but it should be fine as it is valid Rust code.

(I'm stopping midway through reading/looking so maybe missing something; but also maybe what I see is informative; but not the question.)

I see a linguistic error with using the word "Convert".

trait TryConvertValue<'a>: Sized {
    fn try_convert_value(value: &'a Value) -> Option<Self>;
}

To me there isn't conversion. What is see as input is Value, the output is the new type but Value still exists inside it. For lack of a better [maybe is one] word more fitting is "Encapsulate".
(You could argue the reference is the input but "value" screams not a reference.)

Maybe transform is the intention? Following is one of the impl

impl<'a> TryConvertValue<'a> for &'a str {
    fn try_convert_value(value: &'a Value) -> Option<&'a str> {
        match value {
            Value::String(s) => Some(s),
            _ => None,
        }
    }
}

The value is "transform" to &str, maybe~

actually, it's almost idenitcal to the core::convert::TryFrom trait, except Option is returned instead of Result. we can also apply the (not yet existent) hrtb to this trait instead of the CallbackTrait:

impl<'a, T<'_>> TryConvertValue<'a> for T<'a> where T<'a>: TryFrom<&'a Value> {
    fn try_convert_value(value: &'a Value) -> Option<T<'a>> {
        T::try_from(value).ok()
    }
}
1 Like

So here's the call with an error:

    let to_upper = ArgCallback::new(|a: &str| Value::String(a.to_uppercase()));

And here are the bounds in question:

    pub fn new<F, Arg>(f: F) -> ArgCallback
    where
        F: CallbackTrait<Arg>,
        Arg: for<'a> TryConvertValue<'a>,

Type parameters like F and Arg must resolve to a single, concrete type. And &str over all lifetimes or &str without a concrete lifetime isn't a concrete type -- it's a type constructor that takes a lifetime parameter. For the closure to be passed to new, it's going to be interpreted as or coerced to:

|a: &'x str| Value::String(a.to_uppercase())

where 'x is one single inferred lifetime. Then Arg == &'x str is possible.


Next we need to satisfy

&'x str: for<'a> TryConvertValue<'a>

But the implementation is

impl<'a> TryConvertValue<'a> for &'a str {

So &'long str doesn't implement TryConvertValue<'short> or TryConvertValue<'static> for example, it only implements TryConvertValue<'long>.

Hence the error.

The implementation could be made more generic:

impl<'b: 'a, 'a> TryConvertValue<'b> for &'a str {

And now &'long str does implement TryConvertValue<'static> ... but still not TryConvertValue<'short>. And being able to pass in arbitrarily short borrows of a Value is the functionality being required. So this doesn't actually help with the error at hand.


What you really need is for the closure to remain higher-ranked so that it can accept &str of any lifetime, and not some concrete 'x. That means you need to change the bounds so that Arg isn't a type parameter. I walk through that process in this post, which is also linked in that article.

3 Likes

Thanks for the explanation which is a breeze to read! I realized the post you made was also mentioned in the blog and I just couldn't understand it (read it through) as I didn't have the knowledge to understand it before asking the question myself and armed with the help from you guys! Thanks again!

1 Like

@Celthi Hi, I have tried to work out a solution to this, basically, need to make the lifetime of &Value to be explicit outlive the closure

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.