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 T
s.
Later I found out that for some implementations, the to_intermediate
procedure is merely a table lookup, and the heap allocation for owned String
s is redundant since the downstream consumes the Intermediate
by joining the strings together, and owning the String
s does no good.
What about changing String
s to &'static str
s in enum Intermediate
? But I didn't want to eliminate the possibility of returning Intermediate
s holding owned String
s, 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 str
s and String
s, 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 str
s and String
s in a generic way?
If not, how can we improve the learning process?