rfcs
rfcs copied to clipboard
first() in iterator
std::iter::Iterator has a method to get the last element, last(). But why isn't there a first() to get the first element?
There are times when first() comes handy, especially after filtering and/or sorting the items. Though it's possible to use nth(0) in such cases, first() would be cleaner.
There is such a method; it is just named next().
Ah! next() is all over the documentation, yet I failed to relate to it. Not very intuitive IMHO.
It might be sufficient to add a note about next in the documentation of last.
it is not possible for all iterators to access the first element at any time.
@aristarh2704 are you making a distinction between the “first ever” and the “first remaining”? Your statement is true for the former, but not really the latter. The latter is what seems to be being discussed here.
Even for the former, you could have an iterator adapter that saves the first ever seen element and use that on top of any other iterator.
@pnkfelix I somehow disagree. Iterator::next doesn’t consume the iterator. I guess Iterator::first should be defined in terms of:
trait Iterator {
// …
fn first(self) -> Option<Self::Item> {
self.next()
}
}
@phaazon why would you want that though?
IMO adding the following to the documentation of last should be enough:
Note that you can also get the first of the remaining elements of the iterator using
next.
Someone searching in the doc how to get the first element should find that easily.
I like @phaazon's suggestion to add first. That would be more intuitive and cleaner than adding a note to next in the doc. However,
- What's the norm here about multiple methods doing the same thing?
- Is
nextcommon in other languages, more common thanfirstthat it's a widely understood? (in which case clarifying it in doc is more appropriate than duplicating functionality)
last consumes the iterator. Would a consuming first make sense? Would it make way for additional optimization as opposed to non-consuming next?
@phaazon why would you want that though?
IMO adding the following to the documentation of
lastshould be enough:Note that you can also get the first of the remaining elements of the iterator using
next.Someone searching in the doc how to get the first element should find that easily.
I don’t want to do that, I support doing that. last already exists and consumes. It seems weird to suggest people to have a different interface for first.
Also, most containers in Rust have a first() method. Why not iterators? Especially with such a trivial implementation, non-breaking change addition?
Keep in mind that this bloats every Rust binary that uses dyn Iterator, so I'd be careful adding stuff that is technically not needed.
@CryZe That is not the case, because the methods are marked as Self: Sized, so they are not included in the trait object:
https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=4372db0df1f24f8652e33c5473480341
Also… I’m not even sure it’s a real and useful argument. If the Rust team cared about that, the Iterator trait would be split into several traits. It’s one of the trait in std that has the more methods.
Honestly, the only version of first that might make sense is one that works like last but calls next_back instead. Probably a hot take.
Ideally, first would make sense if it returns the very first element of the iterator, irrespective of the time of call. This also fits nicely with next and last, as next can fetch any value from first to last, last always gets the last value, and first should always get the first value.
There is no way to know the very first element of an iterators, and for many iterators (like IterMut) returning the very first element after it has been yielded before would be UB.
I.e. iterators can't track it's history in any way in general.
Also .last() doesn't return the very last value if you've called .next_back().
fn main() {
let mut m = [1, 2, 3, 4, 5].iter();
m.next_back();
dbg!(m.last()); // Some(4)
}
What about something more along the lines of peekable? You'd have a separate struct, something like KnownRange, that remembers the iterator's state when it's created and has methods like first_ever, last_ever, and maybe nth_ever that return values based on that.
Of course, you might want to have both those and peek on the same iterator, and then things get more complicated. Maybe you could do that with corresponding traits for each: impl<I> PeekableIterator for KnownRange<I> where I: PeekableIterator
@Saffith at that point you may as well just collect the iterator into a Vec
You could clone the iterator to "remember the initial state".
let mut m = [1, 2, 3, 4, 5].iter();
let m_original = m.clone();
m.next_back();
assert_eq!(m_original.last(), Some(&5));
The PeekableIterator would be
struct PeekableIterator<I> {
original: I,
current: I,
}
impl<I: Clone> PeekableIterator<I> {
pub fn new(iter: I) -> Self {
Self {
original: iter.clone(),
current: iter,
}
}
pub fn reset(&mut self) {
self.current = self.original.clone();
}
}
impl<I: Iterator> Iterator for PeekableIterator<I> {
type Item = I::Item;
fn next(&mut self) -> Option<I::Item> {
self.current.next()
}
}
// delegate all other traits to `self.current`.
@Pauan
@CryZeThat is not the case, because the methods are marked asSelf: Sized, so they are not included in the trait object:https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=4372db0df1f24f8652e33c5473480341
Self: Sized methods still take up space in vtables, they are just set to zero. This makes it easy to go from the nth method to the correct position in the vtable: (3+n)*size_of::<usize>()
An idea for an alternative way to approach this: https://internals.rust-lang.org/t/idea-diagnostic-aliases-attribute/13366?u=scottmcm
As a newcomer to rust, I suggest that if you cannot always get the very first element of the iterator upon its creation, then the suggested implementation of .first() would just be confusing and not what I would expect.
If it does not exist in the data structure, then we should not cover up that fact.
A #[doc(alias = "first")] would fix newcomers confusion, as when you do iter.firs<tab>, the LSP will suggest iter.next().
No new methods that do the exact same thing would be added to the trait, either. A win-win.