How to add a trait bound to a generic associated type?
Asked Answered
P

1

10

Recently, the post "The push for GATs stabilization" was published on the Rust blog. I'm interested in the LendingIterator trait but hit a problem when trying to use it. This is the definition from the post:

trait LendingIterator {
    type Item<'a> where Self: 'a;
    fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}

All the things advertised in the blog post work fine and I see how GATs help in several ways. But: how do you add a trait bound to the associated Item type?

With the standard Iterator trait, it's easy:

fn print_all<I>(mut i: I) 
where
    I: Iterator,
    I::Item: std::fmt::Debug,    // <-------
{
    while let Some(x) = i.next() {
        println!("{:?}", x);
    }
}

But with the new trait, the bound this bound does not compile (Playground):

where
    I: LendingIterator,
    I::Item: std::fmt::Debug,

Compiler says:

error[E0107]: missing generics for associated type `LendingIterator::Item`
  --> src/lib.rs:12:8
   |
12 |     I::Item: std::fmt::Debug,
   |        ^^^^ expected 1 lifetime argument
   |
note: associated type defined here, with 1 lifetime parameter: `'a`
  --> src/lib.rs:5:10
   |
5  |     type Item<'a> where Self: 'a;
   |          ^^^^ --
help: add missing lifetime argument
   |
12 |     I::Item<'a>: std::fmt::Debug,
   |        ^^^^^^^^

So we need a lifetime parameter somehow. Apart from just using 'static (which is overly restrictive), I see two ways to achieve that. However, both have subtle but significant disadvantages/problems.

Lifetime parameter on function

fn print_all<'a, I>(mut i: I) 
where
    I: 'a + LendingIterator,
    I::Item<'a>: std::fmt::Debug,
{
    while let Some(x) = i.next() {
        println!("{:?}", x);
    }
}

(Playground)

This fails to compile with:

error[E0499]: cannot borrow `i` as mutable more than once at a time
  --> src/lib.rs:14:25
   |
9  | fn print_all<'a, I>(mut i: I) 
   |              -- lifetime `'a` defined here
...
14 |     while let Some(x) = i.next() {
   |                         ^-------
   |                         |
   |                         `i` was mutably borrowed here in the previous iteration of the loop
   |                         argument requires that `i` is borrowed for `'a`

The reason for this is that lifetime parameters (actually, all generic parameters) of print_all are chosen by the caller. This implies that the lifetime is larger than the scope of print_all: the caller cannot know of any lifetime that is just inside of print_all. That means that the call to next has to borrow i for the whole lifetime 'a. But that includes all of print_all, so we can only borrow exactly once!

In any case, this solution is not viable.

Higher kinded trait bounds (♥y bois)

fn print_all<I>(mut i: I) 
where
    I: LendingIterator,
    for<'a> I::Item<'a>: std::fmt::Debug,

(Playground)

Hey, it compiles! However, there is a subtle problem with it. Lets take the WindowsMut iterator from the blog post and try to pass it to print_all: Playground. It does not compile:

error[E0277]: `<_ as LendingIterator>::Item<'a>` doesn't implement `Debug`
  --> src/main.rs:43:5
   |
9  | fn print_all<I>(mut i: I) 
   |    --------- required by a bound in this
...
12 |     for<'a> I::Item<'a>: std::fmt::Debug,
   |                          --------------- required by this bound in `print_all`
...
43 |     print_all(windows);
   |     ^^^^^^^^^ `<_ as LendingIterator>::Item<'a>` cannot be formatted using `{:?}` because it doesn't implement `Debug`
   |
   = help: the trait `for<'a> Debug` is not implemented for `<_ as LendingIterator>::Item<'a>`

Strange! Remember:

type Item<'a> where Self: 'a = &'a mut [T];

And std::fmt::Debug is definitely implemented for mutable references to slices. Regardless of lifetime. (Compared the docs).

I think that the bound is not satisfied because for<'a> means "for all possible lifetimes", which includes 'static. Let's write that out:

Does WindowsMut<'t, T>::Item<'a> implement Debug for all possible 'as? Does WindowsMut<'t, T>::Item<'static> implement Debug? That's &'static mut [T]. That type is only valid if T: 'static. And there is also this weird where Self: 'a bound. And WindowsMut<'t, T>: 'static is definitely not satisfied unless 't == 'static.


How do I properly add a bound to the Item type of LendingIterator and implement print_all? This should be possible, right? Has this problem been discussed already? Is my reasoning above incorrect, in particular regarding the HRTBs?

Puglia answered 4/8, 2021 at 19:22 Comment(1)
Part of the problem seems to be explained here, and implementing the private module can help us get rid of it complaining about Debug not being implemented. But then the function is requiring that arr have 'static lifetime. PlaygroundNoway
N
1

I see your reasoning why this this pattern should extend, but isn't this maybe just a limitation of GATs at the moment (or just a bug)? Though, you can refactor your trait bound farther up the chain for this to work just fine by requiring ...

trait LendingIterator {
    type Item<'a>: std::fmt::Debug where Self: 'a; // Require Debug trait
    fn next<'a>(&'a mut self) -> Option<Self::Item<'a>>;
}

// Require T has Debug trait
impl<'t, T: std::fmt::Debug> LendingIterator for WindowsMut<'t, T> {
    type Item<'a> where Self: 'a = &'a mut [T];
    
    fn next<'a>(&'a mut self) -> Option<Self::Item<'a>> {
        let retval = self.slice[self.start..].get_mut(..self.window_size)?;
        self.start += 1;
        Some(retval)
    }
}

and removing your bound on the function ...

fn print_all<I>(mut i: I) 
where
    I: LendingIterator,
    // Remove 
    // for<'a> I::Item<'a>: std::fmt::Debug, 
{
    while let Some(x) = i.next() {
        println!("{:?}", x);
    }
}

But if I then un-remove the trait bound, I get a different error than you had before ...

   Compiling playground v0.0.1 (/playground)
error[E0271]: type mismatch resolving `for<'a> <WindowsMut<'_, {integer}> as LendingIterator>::Item<'a> == <WindowsMut<'_, {integer}> as LendingIterator>::Item<'a>`
  --> src/main.rs:45:5
   |
9  | fn print_all<I>(mut i: I) 
   |    --------- required by a bound in this
...
13 |     for<'a> I::Item<'a>: std::fmt::Debug, 
   |                          --------------- required by this bound in `print_all`
...
45 |     print_all(windows);
   |     ^^^^^^^^^ expected associated type, found `&mut [_]`
   |
   = note: expected associated type `<WindowsMut<'_, {integer}> as LendingIterator>::Item<'a>`
            found mutable reference `&'a mut [{integer}]`
   = help: consider constraining the associated type `<WindowsMut<'_, {integer}> as LendingIterator>::Item<'a>` to `&'a mut [_]` or calling a method that returns `<WindowsMut<'_, {integer}> as LendingIterator>::Item<'a>`
   = note: for more information, visit https://doc.rust-lang.org/book/ch19-03-advanced-traits.html

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

I would think that this additional trait bound is a tautology to the others, which to me is a red flag the compiler is complaining for no reason.

Playground to show the example works.

Noway answered 5/8, 2021 at 17:44 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.