Problems specifying lifetime parameters for data shared between structs

I'm getting a bit lost with lifetime parameters in a theoretically simple scenario:

I have a struct that contains some data, and work will be done on this data. This struct can be chunked so the work can be done in parallel, and it implements a trait to expose this functionality. For this example I will call the trait Chunkable and the specific struct MyChunkable.

The chunks themselves also implement a trait, and so I will call the trait Chunk and the specific struct MyChunk.

The structs look like this:

struct MyChunkable {
    data: Vec<u32>,
}

struct MyChunk<'a> {
    data: &'a mut [u32],
}

The MyChunk struct above contains a mutable reference to a subset of the data in MyChunkable.

The trait Chunk is simple, as is the implementation:

trait Chunk {
    fn do_it(&mut self);
}

impl<'a> Chunk for MyChunk<'a> {
    fn do_it(&mut self) {
        self.data[0] = 0;
    }
}

In this example I'm just setting the zeroth element of each chunk to zero for the implementation of the Chunk trait.

The trait Chunkable gets more interesting. I need to tie the lifetime of the data referenced in Chunk to the lifetime of the data in Chunkable, and so I need to specify a lifetime parameter 'a on the trait so the implementation can make use of it. Perhaps there is a better way to do this? But this is what I've got:

trait Chunkable<'a, TChunk> 
where 
    TChunk : Chunk
{
    fn chunks_mut(&'a mut self) -> Vec<TChunk>;
}

impl<'a> Chunkable<'a, MyChunk<'a>> for MyChunkable {
    fn chunks_mut(&'a mut self) -> Vec<MyChunk<'a>> {
        self.data
            .chunks_mut(10)
            .map(|v| MyChunk { data: v })
            .collect()
    }
}

So far, so good. Next, for various reasons, I need a builder trait for Chunkable, and an implementation:

trait ChunkableBuilder<'a, TChunkable, TChunk> 
where
    TChunk : Chunk,
    TChunkable : Chunkable<'a, TChunk> 
{
    fn create(&self, count: u32) -> TChunkable;
}

struct MyChunkableBuilder {}

impl<'a> ChunkableBuilder<'a, MyChunkable, MyChunk<'a>> for MyChunkableBuilder {
    fn create(&self, count: u32) -> MyChunkable {
        MyChunkable {
            data: (0..count).collect(),
        }
    }
}

This is fine, although the ChunkableBuilder has been infected with the lifetime 'a, even though it doesn't seem like the builder should really care about the lifetimes data it creates and returns.

Despite that, I can verify that all this works, like so:

fn main() {
    let builder = MyChunkableBuilder {};

    let mut chunkable = builder.create(100);
    let chunks = chunkable.chunks_mut();
    for mut chunk in chunks {
        chunk.do_it();
    }

    print!("{:#?}", chunkable.data);
}

Now is where I get a bit lost. Let's say I have a Runner trait. The implementation of this trait will do the work above and return the Chunkable so I can read the data out of it later. Here is the trait:

trait Runner<TChunkable, TChunk>
where
    for<'a> TChunkable: Chunkable<'a, TChunk>,
    TChunk : Chunk
{
    fn run(&self) -> TChunkable;
}

The struct that implements this trait owns the builder, and does the work. The generic type parameters are getting a bit out of hand here, but they compile ok.

struct MyRunner<TChunkableBuilder, TChunkable, TChunk>
where
    for<'a> TChunkableBuilder: ChunkableBuilder<'a, TChunkable, TChunk>,
    for<'a> TChunkable: Chunkable<'a, TChunk>,
    TChunk: Chunk,
{
    chunkable_builder: TChunkableBuilder,
    _phantom_data: PhantomData<(TChunkable, TChunk)>,
}

impl<TChunkableBuilder, TChunkable, TChunk> Runner<TChunkable, TChunk>
    for MyRunner<TChunkableBuilder, TChunkable, TChunk>
where
    for<'a> TChunkableBuilder: ChunkableBuilder<'a, TChunkable, TChunk>,
    for<'a> TChunkable: Chunkable<'a, TChunk>,
    TChunk: Chunk,
{
    fn run(&self) -> TChunkable {
        let mut chunkable = self.chunkable_builder.create(100);
        let chunks = chunkable.chunks_mut();
        for mut chunk in chunks {
            chunk.do_it();
        }

        chunkable
    }
}

At least this all compiles fine until I try to write some code to actually use MyRunner:

fn main() {
    let runner = MyRunner {
        chunkable_builder: MyChunkableBuilder {},
        _phantom_data: PhantomData,
    };

    let chunkable = runner.run();

    print!("{:#?}", chunkable.data);
}

At which point I get:

   Compiling playground v0.0.1 (/playground)
error[E0599]: the method `run` exists for struct `MyRunner<MyChunkableBuilder, MyChunkable, MyChunk<'_>>`, but its trait bounds were not satisfied
   --> src/main.rs:116:28
    |
4   | struct MyChunkable {
    | ------------------ doesn't satisfy `MyChunkable: Chunkable<'a, MyChunk<'_>>`
...
49  | struct MyChunkableBuilder {}
    | ------------------------- doesn't satisfy `_: ChunkableBuilder<'a, MyChunkable, MyChunk<'_>>`
...
82  | struct MyRunner<TChunkableBuilder, TChunkable, TChunk>
    | ------------------------------------------------------
    | |
    | method `run` not found for this struct
    | doesn't satisfy `_: Runner<MyChunkable, MyChunk<'_>>`
...
116 |     let chunkable = runner.run();
    |                            ^^^ method cannot be called due to unsatisfied trait bounds
    |
note: the following trait bounds were not satisfied:
      `MyChunkable: Chunkable<'a, MyChunk<'_>>`
      `MyChunkableBuilder: ChunkableBuilder<'a, MyChunkable, MyChunk<'_>>`
   --> src/main.rs:95:32
    |
92  | impl<TChunkableBuilder, TChunkable, TChunk> Runner<TChunkable, TChunk>
    |                                             --------------------------
93  |     for MyRunner<TChunkableBuilder, TChunkable, TChunk>
    |         -----------------------------------------------
94  | where
95  |     for<'a> TChunkableBuilder: ChunkableBuilder<'a, TChunkable, TChunk>,
    |                                ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound introduced here
96  |     for<'a> TChunkable: Chunkable<'a, TChunk>,
    |                         ^^^^^^^^^^^^^^^^^^^^^ unsatisfied trait bound introduced here
note: the traits `Chunkable` and `ChunkableBuilder` must be implemented
   --> src/main.rs:24:1
    |
24  | trait Chunkable<'a, TChunk> 
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^
...
41  | trait ChunkableBuilder<'a, TChunkable, TChunk> 
    | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

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

I'm a bit stuck on where I go from here to fix the error, but this is where my current thinking is:

  • The lifetime 'a seems to be very infectious, even though it is really only a bound on the data shared between MyChunk and MyChunkable. Is this normal?

  • The generic parameters can get very busy very quickly, although I can accept that this is a result of trying to avoid using dyn and dynamic dispatch.

  • The main error: It feels like the higher rank trait bounds on MyRunner are perhaps not quite tying all the lifetimes together correctly, but I'm not sure how to better specify this.

Any help or advice is appreciated..

This code is runnable here:

You'll see that if you comment out fn main() at the bottom it compiles fine, and renaming fn main_this_works_fine() to fn main() will allow the code run as expected (without using the Runner).

One solution is to use an associated type so that the Chunk type is derived from the lifetime of the Chunkable:

trait Chunkable<'a> {
    type Chunk: Chunk;

    fn chunks_mut(&'a mut self) -> Vec<Self::Chunk>;
}

Then, you can write for<'a> Chunkable<'a> for a Chunkable object that works over all lifetimes. Conceptually, it's a good idea to use type parameters for "input" types to the trait that the caller chooses, and associated types for "output" types to the trait that the implementer chooses. So the ChunkableBuilder and Runner traits can also be written to use associated types, e.g., Rust Playground.

In fact, if you don't need to support dyn Chunkable<'a, Chunk = ...> trait objects, then this is a good use case for generic associated types. We move the <'a> to type Chunk<'a>: Chunk:

trait Chunkable {
    type Chunk<'a>: Chunk
    where
        Self: 'a;

    fn chunks_mut(&mut self) -> Vec<Self::Chunk<'_>>;
}

Then, for<'a> Chunkable<'a> simply becomes Chunkable, Rust Playground.

5 Likes

Thanks @LegionMammal978, that makes a lot of sense and solves all the points I made at the end of my question. I really like the use of generic associated types as well. Thanks again!

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.