Crazytieguy / gat-lending-iterator

20 stars 4 forks source link

Trouble using the `map` combinator #20

Open DCNick3 opened 11 months ago

DCNick3 commented 11 months ago

I have a use-case where I want to yield a tuple of two items: an idem lended from the iterator and an item borrowed from outside scope (LendedItem and BorrowedItem respectively). Here's a silly example demonstrating the shape of the iterator I want, without any actual logic:

use std::marker::PhantomData;

use gat_lending_iterator::LendingIterator;

struct BorrowedItem<'root> {
    _marker: PhantomData<&'root ()>,
}

struct LendedItem<'a> {
    _marker: PhantomData<&'a ()>,
}

struct Iterator<'root> {
    _marker: PhantomData<&'root ()>,
}

impl<'root> LendingIterator for Iterator<'root> {
    type Item<'a> = (LendedItem<'a>, BorrowedItem<'root>)
        where Self: 'a; // the problem?

    fn next(&mut self) -> Option<Self::Item<'_>> {
        Some((
            LendedItem {
                _marker: PhantomData,
            },
            BorrowedItem {
                _marker: PhantomData,
            },
        ))
    }
}

Then there's a function I want to call on each element:

fn mapper<'root, 'a>(item: (LendedItem<'a>, BorrowedItem<'root>)) {}

If I call it with while let Some(...) = iter.next() it compiles just fine:

// this compiles
let mut iter = Iterator::<'root> {
    _marker: PhantomData,
};
while let Some(item) = iter.next() {
    mapper(item);
}

However, when I try to use it with .map, rustc complains that 'root must actually be 'static

    // this fails to compile
    Iterator::<'root> {
        _marker: PhantomData,
    }
    .map(mapper);

The compile time error:

error[E0521]: borrowed data escapes outside of function
   --> shin-rom/src/repro.rs:37:5
    |
33  |   fn usage<'root>(_root: &'root ()) {
    |            -----  ----- `_root` is a reference that is only valid in the function body
    |            |
    |            lifetime `'root` defined here
...
37  | /     Iterator::<'root> {
38  | |         _marker: PhantomData,
39  | |     }
40  | |     .map(mapper);
    | |                ^
    | |                |
    | |________________`_root` escapes the function body here
    |                  argument requires that `'root` must outlive `'static`
    |
note: due to current limitations in the borrow checker, this implies a `'static` lifetime
   --> /home/dcnick3/.cargo/registry/src/index.crates.io-6f17d22bba15001f/gat-lending-iterator-0.1.3/src/traits/lending_iterator.rs:121:12
    |
121 |         F: for<'a> SingleArgFnMut<Self::Item<'a>>,
    |            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

The error message suggests a limitation in the borrow checker. It this true? Does this mean that there's no way to have an iterator return an item that is simultaneously lended and borrowed from the outer scope? Maybe you know of any workarounds?

full example code to copy into an IDE ```rust use std::marker::PhantomData; use gat_lending_iterator::LendingIterator; struct BorrowedItem<'root> { _marker: PhantomData<&'root ()>, } struct LendedItem<'a> { _marker: PhantomData<&'a ()>, } struct Iterator<'root> { _marker: PhantomData<&'root ()>, } impl<'root> LendingIterator for Iterator<'root> { type Item<'a> = (LendedItem<'a>, BorrowedItem<'root>) where Self: 'a; // the problem? fn next(&mut self) -> Option> { Some(( LendedItem { _marker: PhantomData, }, BorrowedItem { _marker: PhantomData, }, )) } } fn usage<'root>(_root: &'root ()) { fn mapper<'root, 'a>(item: (LendedItem<'a>, BorrowedItem<'root>)) {} // this fails to compile Iterator::<'root> { _marker: PhantomData, } .map(mapper); // this compiles let mut iter = Iterator::<'root> { _marker: PhantomData, }; while let Some(item) = iter.next() { mapper(item); } } ```
ratmice commented 11 months ago

I haven't been using GATs very long, so I don't know if there are any workarounds for stable, but I does seem that this example does actually compile with RUSTFLAGS="-Zpolonius" cargo +nightly build

Crazytieguy commented 11 months ago

@DCNick3 This article should explain why your example doesn't compile. There might be a way to make it work for your specific use case (want to share a more specific example?), but if not I would recommend checking out lender - it has some tricks to get around this borrow checker limitation. Also, here's a slightly more simplified version of your example:

use gat_lending_iterator::LendingIterator;

struct It<'root> {
    unit: &'root (),
}

impl<'root> LendingIterator for It<'root> {
    type Item<'a> = &'a ()
        where Self: 'a;

    fn next(&mut self) -> Option<Self::Item<'_>> {
        Some(self.unit)
    }
}

fn usage<'a>(unit: &'a ()) {
    // this fails to compile
    It { unit }.map(|_| {});

    // this compiles
    let mut iter = It::<'a> { unit };
    while let Some(item) = iter.next() {
        (|_| {})(item);
    }
}

fn main() {
    usage(&())
}
Crazytieguy commented 11 months ago

BTW it's good to know polonius solves this! Makes me optimistic about the future of this crate.

DCNick3 commented 11 months ago

Thanks for the article!

I think my "concrete" example is a bit too domain-specific and just has too much stuff to actually be useful for understanding the problem that I have. But in its core I just want to return a pair: an item lended from an iterator (it only exists for one iteration, to re-use the same memory for it) and an item that is borrowed from the environment.

I ended up changing the design to use a less flexible interface: visitors, so now not the code that produces the items has to be implemented as a state machine, but the code that uses them. It's a bit more verbose than I would like, but it seems to work for me. If you are curious, here's what I came up with. Notice the re-use of path_buf to construct the file path during the iteration and the fact that all directories are bound to the lifetime of 'bump allocator (or maybe it doesn't matter here?). Unfortunately, I didn't save my attempt to make it work with the lending iterators