Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement two-way iterators and peekables #83

Open
wants to merge 1 commit into
base: master
Choose a base branch
from

Conversation

blt-r
Copy link

@blt-r blt-r commented Apr 10, 2023

Closes #82.

I decided to make a struct TwoWayPeekable which will contain the iterator and the peeked value, similar to std::iter::Peekable. This allows to eliminate code duplication and this way the peeking-related overhead will affect only those who need to peek. As a downside, this approach increases the API surface area. It introduces a new trait TwoWayIterator, which probably should be a standard library feature.

I named the method that makes iterator peekable .two_way_peekable(), because .peekable() would conflict with std::iter::Iterator::peekable. Admittedly, two_way_peekable is not a very good name, so it probably should be changed, but I don't know to what. We could just name it peekable, but that would be a breaking change, and would require a major version bump, so probably not worth it.

@cessen
Copy link
Owner

cessen commented Apr 11, 2023

It introduces a new trait TwoWayIterator, which probably should be a standard library feature.

Yeah, at some point I want to submit an RFC for that. It really feels like a missing API in Rust's iterators. It's also pretty surprising to me that DoubleEndedIterator is in std when something like TwoWayIterator isn't. The former seems really niche to me, whereas the latter seems more broadly useful. Dunno.

In any case, I don't think it makes much sense to create traits for these things in Ropey itself. The point of traits in this case would be interoperability, and bespoke traits in Ropey doesn't achieve that.

I think what probably makes the most sense is to implement std::iter::Peekable for forward peeking, and then just directly implement a .peek_prev() method on each of the iterators' impls. Basically, following the same pattern as .next() and .prev() already do.

Comment on lines +16 to +30
#[derive(Debug)]
enum Peeked<T> {
None,
Prev(Option<T>), // remember peeked value even if it was none
Next(Option<T>),
}

pub struct TwoWayPeekable<I>
where
I: TwoWayIterator,
I::Item: Copy,
{
itr: I,
peeked: Peeked<I::Item>,
}
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we're going to the trouble of storing peek values (rather than the simple "advance and then de-advance" approach discussed in #82), then instead of a single peeked field, I think we want both peeked_prev and peeked_next, both of which would be simple Options and would be maintained as the iterator progresses. And then the Peeked enum is unnecessary.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Initially, that's what I did, but two options seemed redundant since, you can never have both of them be Some variant. Because if both of them are Some, that would mean, that iterator is before prev_peeked and after next_peeked at the same time

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure I follow. I could be missing something, but isn't the idea that prev/next_peeked would simply store the value that peek_prev() and peek_next() would return? Or for that matter, what prev() and next() would return as well (since the only difference between them and the peek_* variants is whether the iterator gets advanced or not).

In other words, if we have the following rope, with the char iterator position represented with a vertical bar:

ab|cd

Then prev_peeked would contain Some('b') and next_peeked would contain Some('c').

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not caching what was returned by .next(), it is caching what was returned by .peek_next(), so that when we call .next(), we know that iterator is already advanced forward by the previous .peek_next() call, so instead of advancing it one more time, we returned the cached value, and set it to Peeked::None, so that next .next() call will just advance the iterator as normal. So if peeked == Peeked::Next('c'), it meens we already peeked at 'c' and iterator is now past it. But we still pretend that it is before it. This is the same way Peekable is implemented in std. Making it the way you described it would make it a lot more complicated, since we would need to keep track of where the iterator is after we moved it around for peeking.

@cessen
Copy link
Owner

cessen commented Apr 11, 2023

I think what probably makes the most sense is to implement std::iter::Peekable for forward peeking

Oops. I misremembered what std::iter::Peekable was. I thought it was a trait, but indeed it's a struct that wraps the iterator.

Hmm. That makes me wonder if this functionality even belongs directly in Ropey, then. Maybe a separate crate that provides a TwoWayPeekable struct and the relevant traits?

I mean, I'm still happy to have .peek_next() and .peek_prev() methods on Ropey's iterators, but I think just making them direct impls makes the most sense for Ropey's API.

@blt-r
Copy link
Author

blt-r commented Apr 11, 2023

Hmm. That makes me wonder if this functionality even belongs directly in Ropey, then. Maybe a separate crate that provides a TwoWayPeekable struct and the relevant traits?

Yeah, this whole thing is very generic and could be in a separate crate. (or the standard library?)

I mean, I'm still happy to have .peek_next() and .peek_prev() methods on Ropey's iterators, but I think just making them direct impls makes the most sense for Ropey's API.

I agree. We can put the peeked field and these methods directly on the iterators, but .prev(), .next(), .peek_prev(), and .peek_next() on TwoWayPeekable are like 100 lines of code in total and duplicating that 4 times seems not ideal to me. I think, when the peeking functionality is implemented like this, with remembering the peeked value, it makes more sense to have them in a separate struct, like it was done is std.

I think the best implementation for ropey would be if peek methods would actually get the value, but not advance the iterator. But that is not trivial to implement, and I don't have the knowledge to do that. If you think you might do that in the future, I can make a PR with the "placeholder" implementation

@cessen
Copy link
Owner

cessen commented Apr 23, 2023

I've been thinking about this more, and I'm actually thinking that maybe this functionality just doesn't belong directly in Ropey after all. The pattern in std is to have a separate wrapping iterator for peeking, and I think it's best to stick with that pattern. However, at the same time, having such a wrapper in Ropey itself doesn't feel right.

Ideally std will eventually have something like a BidirectionalIterator trait, and then a bidir peeking wrapper could be built on top of that. But at the moment no such trait exists in std.

So as a compromise in the mean time, I think the best thing to do would be to add a peeking wrapper to examples/, similar to the grapheme iterator/functions. That way there is a known good reference implementation for those that need the functionality.

Admittedly, this isn't ideal. But I think it's a reasonable compromise for the time being.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

peek methods on iterators.
2 participants