Thanks so much for your detailed explanation!
I'd like to share where I got the idea in the title from, and maybe we could discuss how the documentation could be improved for my fellow beginners to avoid the detours I took.
I ran into this problem when writing a library with a data structure like this:
pub enum Intermediate {
SingleField(String),
DoubleField {
first: String,
second: String,
},
}
It is intended to be returned by a trait method like this:
pub trait ToIntermediate<T> {
fn to_intermediate(&self) -> Intermediate;
}
where the trait ToIntermediate<T> would be implemented for the same type multiple times with different type parameter Ts.
Later I found out that for some implementations, the to_intermediate procedure is merely a table lookup, and the heap allocation for owned Strings is redundant since the downstream consumes the Intermediate by joining the strings together, and owning the Strings does no good.
What about changing Strings to &'static strs in enum Intermediate? But I didn't want to eliminate the possibility of returning Intermediates holding owned Strings, so I introduced a generic type parameter, and the code looked like this:
pub trait Text = Borrow<str>;
pub enum Intermediate<T: Text> {
SingleField(T),
DoubleField {
first: T,
second: T,
},
}
pub trait ToIntermediate<T> {
type Text: Text;
fn to_intermediate(&self) -> Intermediate<Self::Text>;
}
Now T: Text could work with either &'static str or String.
Later on, when I was writing the function consuming the Intermediate<T> values, I needed to do an equality check on the second field of a Intermediate<T>::DoubleField variant, and hold on to the value of the field for later checks.
But I couldn't just store the borrowed &str provided by the Borrow<str> bound, as it would have been dropped on the next iteration.
Converting a borrowed &str to String works, but it does a heap allocation and clones the underlying bytes, which again is redundant, since we typically work with &'static strs and Strings, and it's enough to just copy a reference / transfer the ownership respectively.
After digging through the std library, I found that Cow<'a, str> meets the requirements perfectly, so I modified the trait alias Text into this:
pub trait Text = Borrow<str> + Into<Cow<'a, str>>;
Of course this didn't work. I deliberately started a failed build just to get some help from the compiler. (Why didn't I try to fully understand the syntaxes and mechanisms before writing the correct code by hand? Well I'd say that could be really difficult)
The compiler complained:
error[E0261]: use of undeclared lifetime name `'a`
--> src\**
|
** | pub trait Text = Borrow<str> + Into<Cow<'a, str>>;
| ^^ undeclared lifetime
|
= note: for more information on higher-ranked polymorphism, visit https://doc.rust-lang.org/nomicon/hrtb.html
help: consider making the bound lifetime-generic with a new `'a` lifetime
|
** | pub trait Text = Borrow<str> + for<'a> Into<Cow<'a, str>>;
| +++++++
help: consider introducing lifetime `'a` here
|
** | pub trait Text<'a> = Borrow<str> + Into<Cow<'a, str>>;
| ++++
So I tried the first advice, as it required fewer changes to the signatures. But then the compiler complained about the implementation:
error: implementation of `From` is not general enough
--> src\**
|
** | fn to_intermediate(&self) -> Intermediate<Self::Text> {
| ^^^^^^^^^^^^^^^^^^^^^^^^ implementation of `From` is not general enough
|
= note: `From<&'static str>` would have to be implemented for the type `Cow<'0, str>`, for any lifetime `'0`...
= note: ...but `From<&'1 str>` is actually implemented for the type `Cow<'1, str>`, for some specific lifetime `'1`
Wait, no help?
Without knowing how to get From<&'static str> implemented for Cow<'a, str> (From<String> worked without problem), I tried the second advice (introducing lifetime parameter 'a)
After introducing a whole bunch of nastiness to the signatures, I got:
error[E0392]: parameter `'a` is never used
--> src\**
|
** | pub enum Intermediate<'a, T: Text<'a>> {
| ^^ unused parameter
|
= help: consider removing `'a`, referring to it in a field, or using a marker such as `PhantomData`
But I wasn't using unsafe pointers, why should I use PhantomData? Anyway, let's give it a try, but then the compiler complained:
error[E0308]: mismatched types
--> src\**
|
** | fn to_intermediate(&self) -> Intermediate<Self::Text>;
| ^^^^^^^^^^^^^^^^^^^^^^^^ lifetime mismatch
|
= note: expected trait `Text<'_>`
found trait `Text<'a>`
note: the anonymous lifetime defined here...
--> src\**
|
** | fn to_intermediate(&self) -> Intermediate<Self::Text>;
| ^^^^^
note: ...does not necessarily outlive the lifetime `'a` as defined here
--> src\**
|
** | pub trait ToIntermediate<'a, T> {
| ^^
And now I got really stuck.
Anyway, when I was trying random ways to rephrase my code without making the IDE complain, I fould that (after I had reverted all the changes since I added + Into<Cow<'a, str>> to the trait alias Text) I could just write:
pub trait Text = Borrow<str> + Into<Cow<'static, str>>;
And code successfully compiled.
Well, this didn't seem unnatural to me, since the only implementation of ToIntermediate<T> had an associated type Text = &'static str;
So I changed that to type Text = String (and changed the return expressions in correspondence).
Magically, the code still compiled (and ran correctly) without any problem.
So, after such a long way through “compiler hint driven coding", I finally made the observation that Into<Cow<'static, str>> is auto implemented for String.
In other words, From<String> is implemented for Cow<'static, str> even if the String itself might not have a 'static lifetime, and the documentation stated:
impl<'a> From<String> for Cow<'a, str>
instead of:
impl From<String> for Cow<'static, str>
although Cow<'static, str> is a subtype of Cow<'a, str> and could be coerced to it.
And the compiler mentioned before:
= note: `From<&'static str>` would have to be implemented for the type `Cow<'0, str>`, for any lifetime `'0`...
= note: ...but `From<&'1 str>` is actually implemented for the type `Cow<'1, str>`, for some specific lifetime `'1`
One would naturally think that for Into<Cow<'static, str>> to be implemented, a String with 'static lifetime would be needed which, surprisingly, is not the case.
Since I've spent so much time to figure out how to set such a simple matter straight. I couldn't help but ask myself: is that effort something necessary for a Rust beginner who just wants to treat &'static strs and Strings in a generic way?
If not, how can we improve the learning process?