Skip to content

Commit d0521fd

Browse files
committed
Merge pull request swiftlang#213 from PatrickPijnappel/patch-1
Create proposal to change IteratorType post-nil guarantee
2 parents fcc50db + d34c723 commit d0521fd

File tree

1 file changed

+121
-0
lines changed

1 file changed

+121
-0
lines changed
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
# Change IteratorType post-nil guarantee
2+
3+
* Proposal: [SE-NNNN](https://github.com/apple/swift-evolution/blob/master/proposals/NNNN-name.md)
4+
* Author(s): [Patrick Pijnappel](https://github.com/PatrickPijnappel)
5+
* Status: **Awaiting review**
6+
* Review manager: TBD
7+
8+
## Introduction
9+
10+
Currently, the documentation for `IteratorType.next()` has the precondition
11+
that when calling `next()`, no preceding call to `next()` should have returned
12+
`nil`, and in fact encourages implementations to raise a `preconditionFailure()`
13+
for violations of this requirement. However, all current 27 `IteratorType`
14+
implementations in the standard library return `nil` indefinitely. Many users
15+
are likely unaware of the precondition, expecting all iterators to return
16+
`nil` indefinitely and writing code that might rely on this assumption. Such
17+
code will usually run fine, until someone does in fact pass in an iterator not
18+
repeating `nil` (it's a silent corner case).
19+
20+
Swift-evolution thread: [\[Proposal\] Change guarantee for GeneratorType.next() to always return nil past end](http://thread.gmane.org/gmane.comp.lang.swift.evolution/8519)
21+
22+
Pull-request: [#1702](https://github.com/apple/swift/pull/1702)
23+
24+
## Motivation
25+
26+
While not overwhelmingly common, it is relatively easy to write code based on the
27+
assumption `nil` will be returned indefinitely:
28+
29+
``` swift
30+
// Example based on standard library code (Sequence.swift)
31+
while let element = iterator.next() {
32+
if condition(element) {
33+
foo(element) // call foo on first element satisfying condition
34+
break
35+
}
36+
}
37+
while let element = iterator.next() {
38+
bar(element) // call bar on remaining elements
39+
}
40+
41+
// Another example
42+
switch (iterator.next(), iterator.next()) {
43+
// ...
44+
}
45+
```
46+
47+
Even though this can be trivially rewritten to not rely on post-`nil` behavior,
48+
the user won't perform this rewrite if they are unaware of the precondition. In
49+
their testing the code will work fine, and likely will in almost every case,
50+
except when passing the rare iterator that doesn't repeat `nil`.
51+
52+
## Proposed solution
53+
54+
Bring the guarantee in line with the common expectation, and require iterators
55+
to return `nil` indefinitely.
56+
57+
Requiring `nil` to be returned indefinitely does require the implementors of
58+
custom `IteratorType` conformances to respect this, but this is likely already
59+
the expectation for most users. Most iterators already get this as a natural
60+
consequence of their implementation (as is the case with all current standard
61+
library iterators), but otherwise they can simply track a `done` flag to do so.
62+
It should be noted that this requirement would also affect closures passed to
63+
`AnyIterator`.
64+
65+
### Performance considerations
66+
The original rationale for introducing the precondition was because of concerns
67+
it might add storage and performance burden to some implementations of
68+
`IteratorType` (see [here](http://article.gmane.org/gmane.comp.lang.swift.evolution/8532)).
69+
70+
However, in light of implementation experience, there are a few observations we
71+
can make:
72+
- These cases are rare. The standard library currently has no iterators that
73+
require extra state or branches to return `nil` indefinitely. The iterator for
74+
the proposed `takeWhile()` ([SE-0045](https://github.com/apple/swift-evolution/blob/master/proposals/0045-scan-takewhile-dropwhile.md))
75+
would be the first occurance in the standard library.
76+
- Even in such cases, in the common case the calling code doesn't rely on
77+
post-`nil` behavior (e.g. `for in`, `map`, etc.) this extra storage and
78+
branching can usually optimized away.
79+
- Not having the post-`nil` guarantee can sometimes add storage and performance
80+
burden for the caller instead, e.g. when an iterator somehow buffers it's
81+
underlying iterator. This in contrast can usually not be optimized away. For
82+
example, the standard library's UTF-8/UTF-16 decoding has 4 instead of 3 branches
83+
per character for ASCII because of this.
84+
85+
## Detailed design
86+
87+
Original guarantee:
88+
89+
``` swift
90+
/// Advance to the next element and return it, or `nil` if no next
91+
/// element exists.
92+
///
93+
/// - Precondition: `next()` has not been applied to a copy of `self`
94+
/// since the copy was made, and no preceding call to `self.next()`
95+
/// has returned `nil`. Specific implementations of this protocol
96+
/// are encouraged to respond to violations of this requirement by
97+
/// calling `preconditionFailure("...")`.
98+
```
99+
100+
Proposed guarantee:
101+
102+
``` swift
103+
/// Advance to the next element and return it, or `nil` if no next element
104+
/// exists. Once `nil` has been returned, all subsequent calls return `nil`.
105+
///
106+
/// - Precondition: `next()` has not been applied to a copy of `self`
107+
/// since the copy was made.
108+
```
109+
110+
## Impact on existing code
111+
112+
All `IteratorType` implementations in the standard library already comply with
113+
the new guarantee. It is likely most existing custom iterators will as well,
114+
however some might be rendered in violation of their guarantee by the change.
115+
116+
## Alternatives considered
117+
118+
- Require `IteratorType` to not crash but keep the return value up to specific
119+
implementations. This allows them to use it for other behavior e.g. repeating
120+
the sequence after `nil` is returned. This however retains most of the problems
121+
of the original guaranteee described in this proposal.

0 commit comments

Comments
 (0)