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