Iterator that owns another iterator and creates items with generic lifetime

Viewed 44

I want to make an iterator that owns another iterator and gives items based on that other iterator. Originally, the inner iterator is formed from the result of a database query, it gives the raw data rows as arrived from the DB. The outer iterator takes items of the inner iterator and puts them into a struct that is meaningful within my program. Because different software versions store the same data in different database table structures, I have a parser trait that takes a row and creates a structure. My outer iterator takes two parameters for creation: the iterator for the DB rows and an object which implements how to parse the data.

But I run into a lifetime error which I don't really see the reason of, and following the compiler's hints only lead me in circles. I literally follow the compiler's advice and getting back to the same problem. I tried to minic the code and bring it to a minimal form to reproduce the same compiler errors I'm getting. I'm not entirely sure if it could be minimized further, but I also wanted it to resemble my real code.

Here is the sample:

struct Storeroom<'a> {
    storeroom_id: i64,
    version: &'a str
}

trait StoreroomParser {
    fn parse(&self, row: Row) -> Result<Storeroom, Error>;
}

struct StoreroomParserX;
impl StoreroomParser for StoreroomParserX {
    fn parse(&self, row: Row) -> Result<Storeroom, Error> {
        Ok(Storeroom { storeroom_id: row.dummy, version: "0.0.0"})
    }
}

struct StoreroomIterator {
    rows: Box<dyn Iterator<Item = Row>>,
    parser: Box<dyn StoreroomParser>
}

impl StoreroomIterator {
    fn new() -> Result<Self, Error> {
        let mut rows: Vec<Row> = vec![];
        rows.push(Row { dummy: 4});
        rows.push(Row { dummy: 6});
        rows.push(Row { dummy: 8});
        let rows = Box::new(rows.into_iter());
        let parser = Box::new(StoreroomParserX {});
        Ok(Self {rows, parser})
    }
}

impl Iterator for StoreroomIterator {
    type Item<'a> = Result<Storeroom<'a>, Error>;

    fn next(&mut self) -> Option<Self::Item> {
        if let Some(nextrow) = self.rows.next() {
            Some(self.parser.parse(nextrow))
        }
        else {
            None
        }
    }
}

During my first attempt, the compiler suggested to add a lifetime annotation to the Item type declaration, because it uses a struct that requires a lifetime. But this resulted in the following error:

error[E0658]: generic associated types are unstable
  --> src/main.rs:59:5
   |
59 |     type Item<'a> = Result<Storeroom<'a>, Error>;
   |     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
   |
   = note: see issue #44265 <https://github.com/rust-lang/rust/issues/44265> for more information

error[E0195]: lifetime parameters or bounds on type `Item` do not match the trait declaration
  --> src/main.rs:59:14
   |
59 |     type Item<'a> = Result<Storeroom<'a>, Error>;
   |              ^^^^ lifetimes do not match type in trait

Here's the sample code on Playground.

When I tried to mitigate this by moving the lifetime annotation to the impl block instead, I provoked the following error I can't progress from:

error: lifetime may not live long enough
  --> src/main.rs:61:13
   |
56 | impl<'a> Iterator for StoreroomIterator<'a> {
   |      -- lifetime `'a` defined here
...
59 |     fn next(&mut self) -> Option<Self::Item> {
   |             - let's call the lifetime of this reference `'1`
60 |         if let Some(nextrow) = self.rows.next() {
61 |             Some(self.parser.parse(nextrow))
   |             ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ associated function was supposed to return data with lifetime `'a` but it is returning data with lifetime `'1`

Playground.

I've been stuck on this problem for about a week now. Do you have any ideas how to resolve these errors? I'm also thinking that I should probably just use map() on the rows with whatever closure it takes to properly convert the data, but at this point it would definitely feel like a compromise.

1 Answers

So you say you force version to be 'static with Box::leak(). If so, you can can just remove the lifetime parameter entirely:

struct Storeroom {
    storeroom_id: i64,
    version: &'static str
}

playground

You also mention that the compiler "forces" you to Box<dyn> rows and parser. You can avoid that by making StoreroomIterator generic over two types for the two members. Only change needed is to take rows in the constructor:

struct StoreroomIterator<R: Iterator<Item = Row>, P: StoreroomParser> {
    rows: R,
    parser: P
}

impl<R: Iterator<Item = Row>> StoreroomIterator<R, StoreroomParserX> {
    fn new(rows: R) -> Result<Self, Error> {
        let parser = StoreroomParserX {};
        Ok(Self { rows, parser })
    }
}

playground

It may be possible to get everything to work with lifetimes as well, but from your incomplete example, it's hard to say exactly. You may want to store a String containing the version in Storeroom and then add a version() method to generate a Version on demand, rather than generating them all up front. But it's hard to say without knowing what this all is for. You may just want to switch to a different library for handling version comparisons.

Related