Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Is there any reason not to use DoubleEndedIterator for iterators that don't iterate over a range of things?

Tags:

rust

As I understand it, DoubleEndedIterator is meant for iterators that iterate over a range of items and can start at either end. For example, (1..10) could implement this to be able to go from 1 to 10, or from 10 to 1. However, I have a situation where it would be useful to use this trait for an iterator that doesn't iterate over a range of things, but instead can go forwards or backwards from the same position, like this:

struct ExtremeIterator {
    number: i32
}

impl Iterator for ExtremeIterator {
    type Item = i32;

    fn next(&mut self) -> Option<i32> {
        let result = self.number;
        self.number += 1;
        Some(result)
    }
}

impl DoubleEndedIterator for ExtremeIterator {
    fn next_back(&mut self) -> Option<i32> {
        let result = self.number;
        self.number -= 1;
        Some(result)
    }
}

fn main() {
    let iter = ExtremeIterator { number: 10 };

    for i in iter.rev().take(5) {
        println!("{}", i);
    }
}

So here's my questions: is there anything semantically wrong with using DoubleEndedIterator this way? Is there some reason why it would be a bad idea to use it for a purpose that is different from what the documentation says it is for? Also, is there a better way to achieve the same thing?

like image 565
Adrian Avatar asked Aug 04 '15 20:08

Adrian


People also ask

Are Rust iterators lazy?

In Rust, iterators are lazy, meaning they have no effect until you call methods that consume the iterator to use it up. For example, the code in Listing 13-10 creates an iterator over the items in the vector v1 by calling the iter method defined on Vec<T> . This code by itself doesn't do anything useful.

What does ITER do in Rust?

An iterator helps to iterate over a collection of values such as arrays, vectors, maps, etc. Iterators implement the Iterator trait that is defined in the Rust standard library. The iter() method returns an iterator object of the collection. Values in an iterator object are called items.

How do you know if an iterator is empty in Rust?

You can make your iterator peekable and peek the first item; if it's None , then the iterator is empty. peek doesn't consume the item1, so the next call to next will return it.

Does Rust use iterators?

Iterators in Rust You might be familiar with loops like “for loop,” “while loop,” and “for each loop.” In Rust, iterators help us achieve the process of looping. In other languages, you can just start looping through an array of values. In Rust, however, you have to declare an iterator first.


1 Answers

First, let's clear up this passing remark in your question:

For example, (1..10) could implement this to be able to go from 1 to 10, or from 10 to 1.

1..10 is of type std::ops::Range<T> and it iterates to the sequence 1, 2, 3, 4, 5, 6, 7, 8, 9 regardless if you iterate it from the back or the front. From the back, you get 9, 8, ..., from the front you get, 1, 2, ...; it is a start inclusive, end exclusive range, also called a half-open range.

If you would iterate the range a bit from the front and then from the back, it will stop where the ends meet:

let mut range = 1..10;

for i in &mut range {
    // Produce 1, 2, 3, 4, 5
    // then break.
    if i == 5 { break; }
}

for j in range.rev() {
    // Produces 9, 8, 7, 6
}

And that shows what a well behaved double ended iterator is like. (rust playpen link)


Now, for the question of DoubleEndedIterator “abuse”:

It is problematic.

The DoubleEndedIterator documentation is clear:

A range iterator able to yield elements from both ends

A DoubleEndedIterator can be thought of as a deque in that next() and next_back() exhaust elements from the same range, and do not work independently of each other.

You must understand this as: Regardless if using next, next_back, or combinations of these, any traversal must produce the same range of elements if you keep track of them (and keep track of which end they came from).

However, there is the case of infinite ranges...

In an infinitely long range, the ends never meet. An example of such an iterator is repeat. For this trivial example it's easy to both see why the iterator is infinite, and how both next() and next_back() are logically defined.

So this is a loophole where you could specify the iterator's sequence as infinitely long, although double ended. I think it is questionable to try to fulfill this interface correctly with our fixed bit width integers.

Even with the dubious philosophical motivation, it might be a very confusing iterator that defies reasonable expectations. I think it would be horrible to use the trait wrong (say having two completely disagreeing ends of an iterator), but with an inexhaustible range you won't actually break any trait properties.

like image 114
bluss Avatar answered Sep 18 '22 17:09

bluss