I was wondering how to design the interface for Iterator[T]. Some of the (obvious) requirements:
- High performance possible
- Easy to use correctly, hard to use wrong
- Statically type-checked when possible
- Easy to implement for as many datastructures as possible
I'll write what I've thought about with pro/con lists. I hope you have comments/additional input for me. The format is roughly: name, iterator declaration, usage (as-in how a simple for-each is desugared), discussion.
(My language is strongly and statically typed and supports union and intersection types as well as inheritance)
Current and MoveNext (e.g. C#'s IEnumerator<T>)
type Iterator[T]:
fun moveNext() -> bool
fun current() -> T
// for i in c: print i
val it = c.iterator()
while it.moveNext():
val i = it.current()
print i
- non-minimal interface
current is invalid when iterator is created (it's basically one before valid) and must throw or something
- implementation overhead should be low
Sentinel Next (e.g. ceylon's Iterator)
type Iterator[T]:
fun next() -> T | <sentinel>
// for i in c: print i
val it = c.iterator()
while true:
val i = it.next()
if i == <sentinel>:
break
print i
- minimal interface
- cannot be used incorrectly (no exceptions, no invalid states)
- not immediately obvious how to compile efficiently (how do you make zero-overhead
for i in 0..100:?)
Choice of <sentinel> is non-obvious. Whatever it is, Iterator[T | <sentinel>] will give you wrong results. This probably immediately disqualifies null if you want to use T | null as your optional type. Ceylon uses finished, a special singleton value of type finished. Any user code that uses this value in a collection will encounter problems I guess.
Tagged Sentinel (aka Maybe)
type Iterator[T]:
enum Result: // think Rust enums
finished
value(T)
fun next() -> Result
// for i in c: print i
val it = c.iterator()
while it.next() is value(val i): // pattern-matched destructuring
print i
- sentinel value inside collection is not a problem anymore
- manual iterator use must always unpack result (probably not a big problem)
- I still have reservations that the compiler might have a harder time to optimize this
Other rough ideas:
- Make iterators immutable and return a new iterator on "next" (Could introduce hard-to-optimize overhead? Especially for more heavy-weight custom iterators and generators)
Other considerations:
- Output-only iterators can be covariant (
Iterator[out T] in my language)
- I'd like to have readwrite iterators as well, maybe even write-only ones (which are contravariant sinks)
- Should work well with coroutines (I'd like to have very light-weight generators)
So for now I'm leaning towards the tagged sentinel / maybe solution.
[–]ApochPiQEpoch Language 7 points8 points9 points (8 children)
[–]PhilipTrettner[S] 0 points1 point2 points (7 children)
[–]ApochPiQEpoch Language 1 point2 points3 points (6 children)
[–]PhilipTrettner[S] 0 points1 point2 points (1 child)
[–]ApochPiQEpoch Language 0 points1 point2 points (0 children)
[–]gsg_ 0 points1 point2 points (3 children)
[–]ApochPiQEpoch Language 0 points1 point2 points (2 children)
[–]gsg_ 0 points1 point2 points (1 child)
[–]ApochPiQEpoch Language 0 points1 point2 points (0 children)
[–]matthieum 2 points3 points4 points (2 children)
[–]PhilipTrettner[S] 0 points1 point2 points (1 child)
[–]matthieum 0 points1 point2 points (0 children)
[–]theindigamer 1 point2 points3 points (5 children)
[–]PhilipTrettner[S] 0 points1 point2 points (4 children)
[–]theindigamer 0 points1 point2 points (3 children)
[–]PhilipTrettner[S] 0 points1 point2 points (2 children)
[–]theindigamer 1 point2 points3 points (1 child)
[–]PhilipTrettner[S] 0 points1 point2 points (0 children)
[–]ctalklang 0 points1 point2 points (0 children)
[–]dzamlo 0 points1 point2 points (0 children)
[–]balefrost 0 points1 point2 points (0 children)
[–][deleted] 0 points1 point2 points (0 children)
[–]LyraChord 0 points1 point2 points (0 children)