Idea: aliasing the `'static` lifetime for lifetime parameters / trait bounds, e.g., `'auto`

Trait aliases are unstable. FYI, because of this, you almost certainly will run into edge cases where the compiler isn't as helpful as we'd like it to be. If you're a beginner and not familiar with Rust, you absolutely should not be enabling any unstable features.

If the problematic middle step had been written without a trait alias:

pub enum Intermediate<T: Borrow<str> + Into<Cow<'a, str>>> {
    SingleField(T),
    DoubleField { first: T, second: T },
}

pub trait ToIntermediate<T> {
    type Text: Borrow<str> + Into<Cow<'a, str>>;
    fn to_intermediate(&self) -> Intermediate<Self::Text>;
}

you'd get the error of

error[E0261]: use of undeclared lifetime name `'a`
 --> src/lib.rs:4:49
  |
4 | pub enum Intermediate<T: 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
  |
4 | pub enum Intermediate<T: Borrow<str> + for<'a> Into<Cow<'a, str>>> {
  |                                        +++++++
help: consider introducing lifetime `'a` here
  |
4 | pub enum Intermediate<'a, T: Borrow<str> + Into<Cow<'a, str>>> {
  |                       +++

error[E0261]: use of undeclared lifetime name `'a`
  --> src/lib.rs:13:39
   |
13 |     type Text: Borrow<str> + Into<Cow<'a, str>>;
   |                                       ^^ undeclared lifetime
   |
help: consider making the bound lifetime-generic with a new `'a` lifetime
   |
13 |     type Text: Borrow<str> + for<'a> Into<Cow<'a, str>>;
   |                              +++++++
help: consider introducing lifetime `'a` here
   |
13 |     type Text<'a>: Borrow<str> + Into<Cow<'a, str>>;
   |              ++++
help: consider introducing lifetime `'a` here
   |
12 | pub trait ToIntermediate<'a, T> {
   |                          +++

which I think might lead to a bit more productive of an error fix cycle.


If there's a "root" issue here, it's that std provides

impl<'a> From<String> for Cow<'a, str>
// and
impl<'a> From<&'a str> for Cow<'a, str>
// but not
impl<'a> From<&'static str> for Cow<'a, str>

// without overlapping impls, it'd be spelled
impl<'short, 'long: 'short> From<&'long str> for Cow<'short, str>
// but this would cause a lot of inference breakage
// as Cow::from("str") would have multiple candidates

A general fix might be for the compiler to synthesize more general impls when variant types are involved, or at least to note this in the "lifetime not general enough" error.

I believe the miscommunication with the compiler happened somewhere around here — the error message is about the &str implementation, and not the String implementation. String satisfies the for<'a> Into<Cow<'a, str>> bound you asked for, but &str doesn't.

The reason that Cow<'static, str>: From<String> holds is that the implementation is provided for impl<'a>. This doesn't mean anything w.r.t. the String, because String doesn't have any lifetime parameters.

I find that subtyping is a horrible way of thinking about generic applicability. It's necessary for coercions, but what's useful for generics is the "outlives" relation and the "forall" binder. The impl is provided with impl<'a>, and no bounds are placed on 'a; this means that when you use the implementation, you're allowed to choose any lifetime you fancy, including 'static.

Nowadays the impl might be written as impl From<String> for Cow<'_, str> instead, which based on the explanation of your logic chain, I expect you would have intuited better. Many existing Rust devs are actually the other way, and prefer the named but unbound lifetime.

Generalizing like this is unfortunately not such a simple matter. It means requiring running headfirst into lifetime problems, and any time you're considering or the compiler starts suggesting "higher-ranked" lifetimes (for<'a>) you're venturing off anything remotely close to the "simple" path. "Lifetime not general enough" is a contender for the worst and most difficult to improve error that the compiler will emit.

The general advice is two bits:

  • Any time you're expressing a function signature of the shape fn() -> for<'a> &'a T, you want to say fn() -> &'static T instead. This holds for any "covariant" lifetime (most of them).
  • To accept either &'static str or String, don't use generics. Just use Cow<'static, str> instead. (For &'a str, Cow<'a, str>.)
1 Like