Is there benefit to having two lifetimes in this From<&'a str> implementation?

Due to my ignorance on lifetime subtyping and reborrows, I am finding myself writing non-idiomatic code "out of fear" of the code not being general enough. For example:

struct Foo<'a> {
    value: &'a str,
}
impl<'a: 'b, 'b> From<&'a str> for Foo<'b> {
    fn from(value: &'a str) -> Self {
        Self { value }
    }
}

I think removing 'b will lead to equivalent code, but I am unsure. For example one often sees PartialEq implemented like below:

impl<'a, 'b> PartialEq<Foo<'a>> for Foo<'b> {
    fn eq(&self, other: &Foo<'a>) -> bool {
        self.value == other.value
    }
}

or with lifetime elision:

impl PartialEq<Foo<'_>> for Foo<'_> {
    fn eq(&self, other: &Foo<'_>) -> bool {
        self.value == other.value
    }
}

One, can someone show me code that will not compile if the PartialEq were implemented using the same lifetime parameter like below?

impl<'a> PartialEq<Self> for Foo<'a> {
    fn eq(&self, other: &Self) -> bool {
        self.value == other.value
    }
}

Two, assuming one can be answered, what makes it important to use different lifetimes in the PartialEq implementation but not the From one?

I can come up with not entirely unreasonable code that would rely on such a From implementation:

Using a function like this

// supports Error types that can be created from &'static str
fn custom_fun_with_callback<E>(callback: impl FnOnce(i32) -> Result<(), E>) -> Result<(), E>
where
    E: From<&'static str>,
{
    callback(42)?;
    Err("custom")?;
    Ok(())
}

which is generic over error types (passing them along from a callback) but requires them to be creatable with a From impl, too.

Now if we do

fn use_case(foo: Foo<'_>) -> Result<(), Foo<'_>> {
    custom_fun_with_callback(|n| Err(foo))?;
    Ok(())
}

this works fine with the generic implementation, but fails with one that equates the lifetimes 'a and 'b

error: lifetime may not live long enough
  --> src/lib.rs:22:34
   |
21 | fn use_case(foo: Foo<'_>) -> Result<(), Foo<'_>> {
   |             --- has type `Foo<'1>`
22 |     custom_fun_with_callback(|n| Err(foo))?;
   |                                  ^^^^^^^^ returning this value requires that `'1` must outlive `'static`

I think, it should generally never be a problem to have the less general implementations for the purpose of calling the relevant method (from or ==/!=) directly, but it can make a difference when they are used to meet explicit trait bounds, such as in the example above where the From<&'static str> bound was solved, not just <…>::from("foo") was called with a &'static str constant "foo". In the latter case, subtyping coercions could always still shorten the lifetime of the &str before the call to from.

4 Likes

For PartialEq, the same thing applies: PartialEq bounds might not be met. S: PartialEq<T> bounds are not too common, but occasionally come up in some other (generic) PartialEq implementations. Such heterogeneous (generic) implementations aren’t too common, but they do exist. Combined with a grain of invariance (so subtyping coercions cannot come save the day after all), I could for example find some implementations involving the most basic invariant types: &mut … types! Here, for example even the PartialEq implementation for &mut S == &mut T itself is generic/heterogeneous in such a way, or also the one between &S and &mut T (so both of these work for types S: PartialEq<T> so S and T can be different types)! With this implementation in mind, here would be code that can break with the less generic implementation:

fn execute(place: &mut Foo<'_>, error_value: &Foo<'_>) {
    if place == error_value {
        place.value = "something went wrong!";
    }
}
2 Likes

Your comprehension of Rust is enviable. I aspire to have such fluency in the language. I cannot thank you enough. I spent some time trying to come up with counterexamples and failed each time. I tried relying on a couple trivial polymorphic functions with trait bounds, but they clearly were not clever enough. Again, thank you.

3 Likes

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.