The new for loop works with all 3 of these. Your output shows that it queried .next() twice, and got a single Some(1) result back. Once it gets None, it never calls .next() again, whereas the 3 behaviors stated previously are exclusively concerned with what happens if you call .next() again after it has already returned None.
-Kevin P.S. I changed the email address that I'm subscribed to this list with, so apologies for any potential confusion. On Aug 4, 2013, at 6:18 AM, Jason Fager <jfa...@gmail.com> wrote: > The new for loop already assumes #2, right? > > let x = [1,2,3]; > let mut it = x.iter().peek_(|x| printfln!(*x)).scan(true, |st, &x| { if *st { > *st = false; Some(x) } else { None } }); > > for i in it { > printfln!("from for loop: %?", i); > } > > > Which produces: > > &1 > from for loop: 1 > &2 > > > > On Sun, Aug 4, 2013 at 1:49 AM, Daniel Micay <danielmi...@gmail.com> wrote: > On Sat, Aug 3, 2013 at 9:18 PM, Kevin Ballard <kball...@gmail.com> wrote: > > The iterator protocol, as I'm sure you're aware, is the protocol that > > defines the behavior of the Iterator trait. Unfortunately, at the moment the > > trait does not document what happens if you call `.next()` on an iterator > > after a previous call has returned `None`. According to Daniel Micay, the > > intention was that the iterator would return `None` forever. However, this > > is not guaranteed by at least one iterator adaptor (Scan), nor is it > > documented. Furthermore, no thought has been given to what happens if an > > iterator pipeline has side-effects. A trivial example of the side-effect > > problem is this: > > > > let x = [1,2,3]; > > let mut it = x.iter().peek_(|x| printfln!(*x)).scan(true, |st, &x| { if > > *st { *st = false; Some(x) } else { None } }); > > (it.next(), it.next(), it.next()) > > > > This results in `(Some(1), None, None)` but it prints out > > > > &1 > > &2 > > &3 > > > > After giving it some thought, I came up with 3 possible definitions for > > behavior in this case: > > > > 1. Once `.next()` has returned `None`, it will return None forever. > > Furthermore, calls to `.next()` after `None` has been returned will not > > trigger side-effects in the iterator pipeline. This means that once > > `.next()` has returned `None`, it becomes idempotent. > > > > This is most likely going to be what people will assume the iterator > > protocol defines, in the absence of any explicit statement. What's more, > > they probably won't even consider the side-effects case. > > > > Implementing this will require care be given to every single iterator and > > iterator adaptor. Most iterators will probably behave like this (unless they > > use a user-supplied closure), but a number of different iterator adaptors > > will need to track this explicitly with a bool flag. It's likely that > > user-supplied iterator adaptors will forget to enforce this and will > > therefore behave subtlely wrong in the face of side-effects. > > > > 2. Once `.next()` has returned `None`, it will return `None` forever. No > > statement is made regarding side-effects. > > > > This is what most people will think they're assuming, if asked. The > > danger here is that they will almost certainly actaully assume #1, and thus > > may write subtlely incorrect code if they're given an iterator pipeline with > > side-effects. > > > > This is easier to implement than #1. Most iterators will do this already. > > Iterator adaptors will generally only have to take care when they use a > > user-supplied closure (e.g. `scan()`). > > > > 3. The behavior of `.next()` after `None` has been returned is left > > undefined. Individual iterators may choose to define behavior here however > > they see fit. > > > > This is what we actually have implemented in the standard libraries > > today. It's also by far the easiest to implement, as iterators and adaptors > > may simply choose to not define any particular behavior. > > > > This is made more attractive by the fact that some iterators may choose > > to actually define behavior that's different than "return `None` forever". > > For example, a user may write an iterator that wraps non-blocking I/O, > > returning `None` when there's no data available and returning `Some(x)` > > again once more data comes in. Or if you don't like that example, they could > > write an iterator that may be updated to contain more data after being > > exhausted. > > > > The downside is that users may assume #1 when #3 holds, which is why this > > needs to be documented properly. > > > > --- > > > > I believe that #3 is the right behavior to define. This gives the most > > flexibility to individual iterators, and we can provide an iterator adaptor > > that gives any iterator the behavior defined by #1 (see Fuse in PR #8276). > > > > I am not strongly opposed to defining #1 instead, but I am mildly worried > > about the likelihood that users will implement iterators that don't have > > this guarantee, as this is not something that can be statically checked by > > the compiler. What's more, if an iterator breaks this guarantee, the problem > > will show up in the code that calls it, rather than in the iterator itself, > > which may make debugging harder. > > > > I am strongly opposed to #2. If we guarantee that an iterator that returns > > `None` once will return `None` forever, users will assume that this means > > that `.next()` becomes idempotent (with regards to side-effects) after > > `None` is returned, but this will not be true. Furthermore, users will > > probably not even realize they've made a bad assumption, as most users will > > not be thinking about side-effects when consuming iterators. > > > > I've already gone ahead and implemented #3 in pull request #8276. > > > > -Kevin > > I'm leaning towards #2 or #3, mostly because adaptors *not* > dispatching to the underlying next() implementation are too complex. > > I took a look at the behaviour of Python's iterators in these corner > cases as good baseline for comparison: > > ~~~ > >>> def peek(it): > ... for x in it: > ... print(x) > ... yield x > ... > >>> xs = [1, 2, 3] > >>> ys = [1, 2, 3, 4, 5] > ~~~ > > You can tell their `zip` function short-circuits, and simply > dispatches to the underlying implementations. Rust's `zip` is similar > but doesn't currently short-circuit (it might as well). > > ~~~ > >>> it = zip(peek(ys), xs) > >>> next(it) > 1 > (1, 1) > >>> next(it) > 2 > (2, 2) > >>> next(it) > 3 > (3, 3) > >>> next(it) > 4 > Traceback (most recent call last): > File "<stdin>", line 1, in <module> > StopIteration > >>> next(it) > 5 > Traceback (most recent call last): > File "<stdin>", line 1, in <module> > StopIteration > >>> next(it) > Traceback (most recent call last): > File "<stdin>", line 1, in <module> > StopIteration > >>> it = zip(xs, peek(ys)) > >>> next(it) > 1 > (1, 1) > >>> next(it) > 2 > (2, 2) > >>> next(it) > 3 > (3, 3) > >>> next(it) > Traceback (most recent call last): > File "<stdin>", line 1, in <module> > StopIteration > ~~~ > > It also makes no attempt to store whether it has stopped internally, > and will start yielding again if each iterator yields an element when > zip asks for them one by one (keeping in mind that it short-circuits). > > Most other language keep `hasNext` and `next` separate (D and Scala, > among others) leading to more corner cases, and they do not seem to > clearly define the semantics for side effects down the pipeline. > > http://dlang.org/phobos/std_range.html > http://www.scala-lang.org/api/current/scala/collection/Iterator.html > _______________________________________________ > Rust-dev mailing list > Rust-dev@mozilla.org > https://mail.mozilla.org/listinfo/rust-dev > > _______________________________________________ > Rust-dev mailing list > Rust-dev@mozilla.org > https://mail.mozilla.org/listinfo/rust-dev
_______________________________________________ Rust-dev mailing list Rust-dev@mozilla.org https://mail.mozilla.org/listinfo/rust-dev