1

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.

Herohtar
  • 5,347
  • 4
  • 31
  • 41
MegaBrutal
  • 310
  • 2
  • 9
  • Is the `version` field always `&'static str` in `parse()`? If not, what is its actual value? – Chayim Friedman Sep 14 '22 at 22:21
  • Is there a reason you boxed `rows` and `parser`? – PitaJ Sep 14 '22 at 22:40
  • In the real code, there is a ``Version`` struct from the ``version-compare`` crate, but it can't be used on playground. Version uses ``&str`` unfortunately so it doesn't own the string. The version also comes from the DB row and I force it to be static via ``Box::leak()``. I know this is ugly, but I'll figure out how to get rid of the leaks later, for now I tolerate them. – MegaBrutal Sep 14 '22 at 22:45
  • I need to see the code. In short, the problem is that the compiler thinks the version comes from the parser while in reality it is not. But this is only in the example; in the real code the situation may be different. – Chayim Friedman Sep 14 '22 at 22:47
  • 1
    So you say you force it to be `&'static str` with `Box::leak()`. If so, why not remove the lifetime parameter from `Storeroom` in the first place? – PitaJ Sep 14 '22 at 22:49
  • `fn parse(&self, row: Row) -> Result;` is equal to `fn parse<'a>(&'a self, row: Row) -> Result, Error>;` after desugaring the lifetime elision. That's what is binding the lifetime of the `Storeroom` to the parser. – PitaJ Sep 14 '22 at 22:55
  • The compiler forced me to box ``rows`` and ``parser`` for they are ``dyn`` they don't have a fixed size. – MegaBrutal Sep 14 '22 at 22:55
  • 1
    You can probably make `StoreroomIterator` generic to avoid dynamic dispatch: `struct StoreroomIterator where R: Iterator, P: StoreroomParser { ... }` – PitaJ Sep 14 '22 at 22:58
  • @ChayimFriedman It is the case, because the parser calls ``Box::leak()`` on the string it gets from ``postgres::Row``, so the version comes from the parser. @PitaJ If I remove the lifetime parameter from ``Storeroom``, the compiler complains until I add it back (at least in the real code). – MegaBrutal Sep 14 '22 at 23:30
  • I didn't mean the parser is responsible to create it (or leak it, for that matter). I meant that it is stored in the parser and the `'a` refers to there. – Chayim Friedman Sep 14 '22 at 23:35
  • If you remove the lifetime from Storeroom, you just need to change `&'a str` to `&'static str` – PitaJ Sep 14 '22 at 23:49

1 Answers1

1

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.

PitaJ
  • 12,969
  • 6
  • 36
  • 55
  • Thank you, it helped! I might apply the suggestion about generics as well, although I don't see boxing ``rows`` and ``parser`` as that much of a concern. But I definitely dislike ``Box::leak()``, once I'll go through the code and remove them all. My idea is to store the source strings in the structs (there are more of them, not just Storeroom), so they'll live together with Version. Version comparisons are prominent within the program, not sure how much effort it would take to switch to a different crate. But true, I don't like that ``version-compare::Version`` doesn't own its data. – MegaBrutal Sep 15 '22 at 20:45
  • You won't be able to store the source strings and a reference to them (`Version`) in the same struct. See: https://stackoverflow.com/questions/32300132/why-cant-i-store-a-value-and-a-reference-to-that-value-in-the-same-struct – PitaJ Sep 15 '22 at 20:50
  • Seems like switching to generics is not so straightforward after all. In the real code, the created StoreroomIterator depends on a version value passed to it: ``` fn new(version: &Version) -> Result { if version >= &Version::from("4.0.0").unwrap() { ``` This if statement decides what SQL query to run (= ``rows``) and which parser to assign. Probably I'm cursed with OOP thinking. – MegaBrutal Sep 16 '22 at 19:20
  • I'm not sure if you can use an associated function, but you can use a standalone function: https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=820292e8076c4c3ce26188ea920e6819 – PitaJ Sep 16 '22 at 21:21
  • Please open a new question if you need more help getting that to work. – PitaJ Sep 16 '22 at 21:22