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

The following problem doesn't make sense to me. Adding where 'a: 'a fixes the problem. Is this a bug in Rust nightly? Or does 'a: 'a involve some magic that's important in this case?

I'm using rustc 1.59.0-nightly (cfa4ac66c 2022-01-06), and Rust Playground is at 1.59.0-nightly (2022-01-05 f1ce0e6a00593493a12e).

#![feature(generic_associated_types)]

trait SomeTrait {
    type Datum<'a>: std::fmt::Debug;
    fn show<'a, A>(args: A)
    where
        A: IntoIterator<Item = Self::Datum<'a>>;
}

struct SomeType {}

impl SomeTrait for SomeType {
    type Datum<'a> = i32;
    fn show<'a, A>(args: A)
    where
        // 'a: 'a, // uncomment to make code compile
        A: IntoIterator<Item = i32>,
    {
        for arg in args {
            println!("{:?}", arg);
        }
    }
}

fn main() {
    SomeType::show([22, 55, 999]);
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0195]: lifetime parameters or bounds on method `show` do not match the trait declaration
  --> src/main.rs:14:12
   |
5  |     fn show<'a, A>(args: A)
   |            ------- lifetimes in impl do not match this method in trait
...
14 |     fn show<'a, A>(args: A)
   |            ^^^^^^^ lifetimes do not match method in trait

For more information about this error, try `rustc --explain E0195`.
error: could not compile `playground` due to previous error

1 Like

My totally un-researched and citation-needed guess is that in the trait declaration,

    fn show<'a, A>(args: A)
    where
        A: IntoIterator<Item = Self::Datum<'a>>;

constitutes a where-clause bound on 'a and thus 'a is an early-bound parameter to the show fn-item type. Where as in the implementation:

    fn show<'a, A>(args: A)
    where
        A: IntoIterator<Item = i32>,

There is no bound and thus show is higher-ranked over 'a; Rust gets upset and wants them to match. By adding 'a: 'a, you make it early-bound, and it lines up with the declaration.

I'll probably get nerd-sniped in to searching around for some issue about it later, if no one else does.

2 Likes

"early-bound", "late-bound" – new terms for me, but I'm willing to learn.

Doing a search I found this: What is the meaning of 'a: 'a in rust lifetime parameters

Which shows:

fn f<'a>() {}
fn g<'a: 'a>() {}

fn main() {
    f::<'static> as fn(); // doesn't work
    g::<'static> as fn(); // works
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error: cannot specify lifetime arguments explicitly if late bound lifetime parameters are present
 --> src/main.rs:5:9
  |
5 |     f::<'static> as fn(); // doesn't work
  |         ^^^^^^^
  |
note: the late bound lifetime parameter is introduced here
 --> src/main.rs:1:6
  |
1 | fn f<'a>() {}
  |      ^^

error: could not compile `playground` due to previous error


P.S.: See also: https://dtolnay.github.io/rust-quiz/11 (you can click on "reveal")

2 Likes

Welp, that was easy. Yes, that's the issue.

I'll write up an early/late summary soon.

3 Likes

Just to note, this is another workaround that "solves" the compiler error (as shown by "Typecheck fails when providing explicit types instead of GAT #87803", linked by @quinedot):

-        A: IntoIterator<Item = i32>,
+        A: IntoIterator<Item = Self::Datum<'a>>,

(Playground)

If I get it right, (regarding my original case), the compiler will allow more flexibility (late binding) regarding 'a if I have fn show<'a, … where 'a isn't used and totally arbitrary. (Edit: Which also seems to be the case with HRTBs.) This makes the function signature be different.

(Well, not a very good explanation. Edit: And I don't really understand it yet.)

(But the error message of the compiler also isn't really helpful.)

I'm sure @quinedot can explain the problem better (Edit: Or someone else; didn't want to push anyone to write something, of course. I can also try to look deeper into it myself, but of course I appreciate hints to understand early/late binding better.)


Currently encountering another issue with lifetimes and HRTBs, but still investigating on that Edit: posted here.

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: ↩︎

9 Likes

Incidentally, I also find it better practice to spell out associated types literally as they are defined in the trait being implemented, because it is more future-proof in terms of refactoring, when the concrete types change.

I understand the syntactical implications now. I wonder, though, if the original error should or shouldn't be considered to be a bug:

For sure, this error message is kinda confusing. Maybe if there was documentation on late/early bounds, then the error would be justified at least.

What do you think? Should this behavior be reported as issue or not?

It's certainly a diagnostic shortcoming. I think it's already tracked at:

Oh right, you gave these links earlier. So nothing to do (for me) then.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.