Why do I need a (useless) bound `'a: 'a`, is this a bug regarding GATs?

Alright, here's the mnemonic-heavy version I keep in my head. Closures are a little different as it turns out, but as for generic parameters to functions and the like, they are:

  • Late Bound xor Early Bound
  • Trait Bound xor Type Bound
  • Higher-ranked xor Turbofishable

And as it turns out, these exclusive groups all line up. Say it with me:

  • Late Bound is
  • Trait Bound is
  • Higher-ranked sound

OK, but what do the terms actually mean?

Here's a more official source on the topic. I'll summarize, but you can just read that instead if you prefer.

One preliminary note: Today, everything is early-bound except lifetimes. This will likely change in the future, but I'll be concentrating on lifetimes only.


Higher-ranked means it's fully generic over the parameter. This can be demonstrated by meeting a higher-ranked bound:

fn hr(_: &str) {}
fn confirm_hr<F: for<'any> Fn(&'any str)>(_: F) {}
fn main() {
    confirm_hr(hr);
}

And indeed, you cannot apply turbofish to such functions:

let _ = hr::<'static>;
/*
error: cannot specify lifetime arguments explicitly if late bound lifetime parameters are present
 --> src/main.rs:5:18
  |
5 |     let _ = hr::<'static>;
  |                  ^^^^^^^
  |
note: the late bound lifetime parameter is introduced here
 --> src/main.rs:1:10
  |
1 | fn hr(_: &str) {}
  |          ^
*/

This is true even if you name the lifetime (so long as you don't constraint it):

fn hr<'any>(_: &'any str) {}
fn main() {
    let _ = hr::<'static>;
}
/*
error: cannot specify lifetime arguments explicitly if late bound lifetime parameters are present
 --> src/main.rs:3:18
  |
3 |     let _ = hr::<'static>;
  |                  ^^^^^^^
  |
note: the late bound lifetime parameter is introduced here
 --> src/main.rs:1:7
  |
1 | fn hr<'any>(_: &'any str) {}
  |       ^^^^
*/

Why can't you turbofish it? Because the higher-ranked lifetime is not part of the fn-item's type! It's not type bound. If it were part of the type -- if it was type bound -- you could turbofish it.

(Why is it called trait-bound then? See the link above, but in short, implementations -- like those of the Fn traits -- must be constrained over their generic parameters. The generic parameters of the implementation have to be part of the trait or part of the type. So if they're not part of the type...)


What determines if a lifetime is early-bound or late-bound then? If it is involved in a where-clause, it's early bound. As far as I know, that's it. And yes, that includes trivial ones like 'a: 'a. [1] OK, so why do lifetimes involved in where-clauses need to be early-bound? I'm going to just defer to Niko on that one. In large part though, I think the limitations of all of fn pointers, the Fn traits, and the ability of the type system to reason (e.g. coherence) demand it.


What do things look like when they are type-bound? Let's try a non-fn example.

trait Trait<'lt> {}

// Lifetime is type bound
struct L<'lt>(&'lt str);
impl<'lt> Trait<'lt> for L<'lt> {}

// Lifetime is not type bound
// But (in this implementation), it's still trait bound
// (and if it wasn't you'd get an error)
struct S;
impl<'lt> Trait<'lt> for S {}

fn confirm_hr<T: for<'lt> Trait<'lt>>(_: T) {}

fn main() {
    confirm_hr(S);
    confirm_hr(L(""));
}

Can you predict the outcome? confirm_hr(S) is ok, but

8 |     confirm_hr(S(""));
  |     ^^^^^^^^^^ implementation of `Trait` is not general enough
  |
  = note: `Trait<'0>` would have to be implemented for the type `S<'_>`, for any lifetime `'0`...
  = note: ...but `Trait<'1>` is actually implemented for the type `S<'1>`, for some specific lifetime `'1`

If a lifetime parameter of your implementation is part of the implementing type, then the implementation is type-bound and not higher-ranked over that lifetime.

Fun sidenote:

You can make this example compile with

-impl<'lt> Trait<'lt> for L<'lt> {}
+impl<'tr, 'lt> Trait<'tr> for L<'lt> {}

What are the implications for functions? Probably nothing but I only just now thought of this and don't really have an idea yet. :clown_face:


Summary

  • Late Bound is
  • Trait Bound is
  • Higher-ranked sound
    • Is not turbofishable

And,

  • Bounds (where-clauses) on lifetimes make them early-bound
    • As in, part of their type. Type-bound, if you will.

And if interested, you should read more at


  1. Reminder: <LHS: RHS> is syntactic sugar for <LHS> ... where LHS: RHS. ↩︎

  2. 404 :man_shrugging: ↩︎

11 Likes