Rfcs: Proposal for iter_index()

Created on 7 Aug 2020  ·  13Comments  ·  Source: rust-lang/rfcs

I want to discuss if the existence of an iterator that returns the index of the current state makes sense. I know the workaround with enumerate() that returns the value and the current index but the challenge with that is that you have to deal two values all the time instead of having a variable to the iterator with a method that returns the index whenever you need it.

I need it for a current project where I parse a file and need the current index to know which line is male formated. Because I have no known alternative than using enumerate I cannot reference to the iterator itself and use comfortable methods like peek() or next().

I would introduce a method that applies additional methods to the iterator like peekable() that gives you the command peek().

Maybe something like let it = list.iter().indexed() and you can execute let current_index = it.index()

T-libs

Most helpful comment

@lebensterben in fairness, this is just an issue, not a full PR with an RFC. It's entirely fair to open an issue with a suggestion as a way to gather opinions before going to the effort of doing a full RFC with a filled out template and PR.

@rudolfschmidt the main reason against this is that generally iterators are consumed in rust via a for loop. When they go into the for loop you can no longer refer to them, so you wouldn't be able to use the iter_index method anyway. That said, even if you did not use a for loop, checking the index makes a variable, and the current iter value is a variable, so even with your idea there's still two variables in play. Since you said this was your goal to avoid, which is why you didn't want to use enumerate, it feels like this plan doesn't actually avoid that particular issue.

compare:

for (current_index, val) in list.iter().enumerate() {
  // work here
}

with:

let mut it = list.iter();
while let Some(val) = it.next() {
  if val > 0 {
    let current_index = it.iter_index();
    // special case work
  }
  // work here
}

To most people, the first version is simpler.

All 13 comments

Couldn't you just write a simple wrapper type to do this. Something like this: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=38b9f579a4ca2aa7dd2d7cd0d12351db

Couldn't you just write a simple wrapper type to do this. Something like this: https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=38b9f579a4ca2aa7dd2d7cd0d12351db

Yes, I can even write assembly code or c, but this is not the point.
The point is to have a language that offers the best methods and concepts possible.
You can write a lot yourself, but it would be great if this would be supported by default without the need to write wrappers.

I don't understand how enumerate doesn't handle this situation though.

It does.. Actually enumerate provides a vastly more ergonomic solution because enumerate makes the index available both inside for loops and inside closures used by later iterator adapters, neither of which this supports.

It's maybe sensible if some microcrate provides a simple detachable counter however:

pub struct Counter<T: Clone+AddAssign<T>> {
    pub status: T,
    pub increment: T,
}

impl<T: Clone+AddAssign> Counter<T> {
    fn next(&mut self) -> T {
        let r = self.status.clone();
        self.status += self.increment;
        r
    }
}

It does.. Actually enumerate provides a vastly more ergonomic solution because enumerate makes the index available both inside for loops and inside closures used by later iterator adapters, neither of which this supports.

It's maybe sensible if some microcrate provides a simple detachable counter however:

pub struct Counter<T: Clone+AddAssign<T>> {
    pub status: T,
    pub increment: T,
}

impl<T: Clone+AddAssign> Counter<T> {
    fn next(&mut self) -> T {
        let r = self.status.clone();
        self.status += self.increment;
        r
    }
}
  1. I did not say you should replace enumerate by an indexed iterator
  2. Rust provides several ways to do the same, just because some ways are more comfortable and less verbose, match vs if let as example
  3. The point here is not to have workarounds or hack to make it work, I have done it. It is just a way to have a more comfortable way to have an iterator that gives you the current index if needed instead to deal with two variables enumerate produces.
  4. Writing Rust I wanted such iter to exist and I am sure other users exist that think the same.

It's maybe sensible if some microcrate provides a simple detachable counter however:

You should not have an external crate doing this, this feature should be default in Rust Standard.

I also cannot understand guys giving bad ratings here. The more optional default exists that makes life easier the better. Why having iterators at all if you can write everything in simple c'like while or for loops? It's about comfort.

Rust relay heavily on iterators and this is something good. But enumerate breaks the thinking around iterators. You only need enumerators to get an index so why not have the index coming from the Standard Iterator?

A hastily-proposed RFC can hurt its chances of acceptance. Low quality proposals, proposals for previously-rejected features, or those that don't fit into the near-term roadmap, may be quickly rejected, which can be demotivating for the unprepared contributor. Laying some groundwork ahead of the RFC can make the process smoother.

  • This RFC is not well-prepared. It doesn't comply with the requirements described here. Please read the guidelines.
  • At very least, you should use this template.

@lebensterben in fairness, this is just an issue, not a full PR with an RFC. It's entirely fair to open an issue with a suggestion as a way to gather opinions before going to the effort of doing a full RFC with a filled out template and PR.

@rudolfschmidt the main reason against this is that generally iterators are consumed in rust via a for loop. When they go into the for loop you can no longer refer to them, so you wouldn't be able to use the iter_index method anyway. That said, even if you did not use a for loop, checking the index makes a variable, and the current iter value is a variable, so even with your idea there's still two variables in play. Since you said this was your goal to avoid, which is why you didn't want to use enumerate, it feels like this plan doesn't actually avoid that particular issue.

compare:

for (current_index, val) in list.iter().enumerate() {
  // work here
}

with:

let mut it = list.iter();
while let Some(val) = it.next() {
  if val > 0 {
    let current_index = it.iter_index();
    // special case work
  }
  // work here
}

To most people, the first version is simpler.

@rudolfschmidt If you haven't checked it out, there's this type Cursor defined for LinkedList. It both allows you to retrieve the current position and seek forward and backward. I think it's almost what you actually want.

I also have implemented one for Vec but as a separate crate.
https://github.com/crlf0710/vec-cursor

It's maybe sensible if some microcrate provides a simple detachable counter however:

Actually I'm wrong, this exists in core since Range<A>: Iterator and RangeFrom<A>: Iterator, so core covers this request quite well via enumerate and Range* currently.

Yet, there is an ongoing discussion about whether whether Range* should be IntoIterator instead of Iterator. If a future edition changed this, then rust would cover this issue less well. We should cite those discussions to remove this from core here perhaps.

@lebensterben in fairness, this is just an issue, not a full PR with an RFC. It's entirely fair to open an issue with a suggestion as a way to gather opinions before going to the effort of doing a full RFC with a filled out template and PR.

@rudolfschmidt the main reason against this is that generally iterators are consumed in rust via a for loop. When they go into the for loop you can no longer refer to them, so you wouldn't be able to use the iter_index method anyway.

Why I have to refer to it? It is the same idea like peekable(). peekable() gives the iterator additional methods like peek(), the same should be for index(). Every next() (that is called in a for loop anyway) increments the counter and the counter is returned by index().

That said, even if you did not use a for loop, checking the index makes a variable, and the current iter value is a variable, so even with your idea there's still two variables in play. Since you said this was your goal to avoid, which is why you didn't want to use enumerate, it feels like this plan doesn't actually avoid that particular issue.

this would be my vision:

let iter = list.into_iter().indexed();
some_method(iter);

fn some_method(iter:Iterator) {
  while let Some(variable) = iter.next() {
    if some_condition_that_need_index() {
      println!("current index : {}", iter.index());
    }
  }
}

vs

for (index, value) in list.into_iter().enumerate() {
  some_method(index,value);
}
fn some_method(index:usize,value:Type) {
  // dealing with two variables instead one from Type Iterator
}

Why should we care how an Iterator works internally? What I am interested in is only the API and how it works internally should not be the most interesting part. I just would like to use it in this way. I agree that you can hack your own Iterator using enumerate or a similar mechanism to save the index and the value in a custom Type but this is a proposal to make it standard in the default API to not be forced to write something yourself, to make it easier for others to use.

The idea is to have a coding language that does not require the user to hack around technical constraints but rather lets him focus on business logic instead and provides everything that is the most idiomatic way to use it.

I believe such an indexed iterator would be one step closer to it.

To most people, the first version is simpler.

Even its true, why not let the user decide what he prefers? Like every human is different, every developer is different and his own preferences about code.

@rudolfschmidt If you haven't checked it out, there's this type Cursor defined for LinkedList. It both allows you to retrieve the current position and seek forward and backward. I think it's almost what you actually want.

I also have implemented one for Vec but as a separate crate.
https://github.com/crlf0710/vec-cursor

It goes in the direction I pointed.

But I would prefer to have it in the Iterator itself rather using a custom list implementation

Well, Iterator isn't a type, it's a trait, so if _the entire trait_ has this ability added, then all people ever need to suddenly support this. That is an unacceptable code break.

So this would only ever be accepted in the first place if there was some custom iterator adapter involved.

So what you should do is make a crate that offers this ability, then people will use that as they need to.

I'm surprised https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=f617466315ebcee50bc3a93e5575b693 works! It appears NLL has gotten quite good. :) It does not however work in for loops, which make the trick kinda painful.

Anyways if you need some ad hoc counter then just do this:

    let mut counter = 0u32..;
    let mut counter = || counter.next().unwrap();
let iter = list.into_iter().indexed();
some_method(iter);

fn some_method(iter:Iterator) {
  while let Some(variable) = iter.next() {
    if some_condition_that_need_index() {
      println!("current index : {}", iter.index());
    }
  }
}

vs

rust for (index, value) in list.into_iter().enumerate() { some_method(index,value); } fn some_method(index:usize,value:Type) { // dealing with two variables instead one from Type Iterator }

But those snippets aren't isomorphic! Writing your "indexed" example with enumerate and actual types would look like this:

let iter = list.into_iter();
some_method(iter);

fn some_method<T>(iter: impl Iterator<Item=T>) {
  // or `while let` if you really want
  for (idx, val) in iter.enumerate() {
    if some_condition_that_need_index() {
      println!("current index : {}", idx);
    }
  }
}

My some_method accepts any iterator whatsoever, whereas your some_method with indexed can only accept iterators that implement the Indexed trait (practically meaning the caller has to call .indexed()) in order to fulfill an entirely internal requirement of some_method. The function's implementation details needlessly leak out.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

rust-highfive picture rust-highfive  ·  4Comments

3442853561 picture 3442853561  ·  3Comments

steveklabnik picture steveklabnik  ·  4Comments

p-avital picture p-avital  ·  3Comments

mahkoh picture mahkoh  ·  3Comments