Swift Regret: Sequence
Swift regret: Sequence
— Jordan Rose (@UINT_MIN) August 13, 2021
Sequence is the base of Swift's Collection protocol hierarchy. It has a single requirement, makeIterator(), and it's also "the thing that works with for/in loops". It's not good at either of those things.
Part of the Swift Regrets series.
Sequence is the base of Swift’s Collection protocol hierarchy. It has a single requirement, makeIterator()
, and it’s also “the thing that works with for/in loops”. It’s not good at either of those things.
If you’re familiar with Rust’s IntoIterator, Sequence is similar. A bunch of the operations that Rust puts on iterators are included on Sequence directly, but the idea is the same. Except…Swift doesn’t have move-only types. So it lets you call makeIterator()
multiple times. Which means from the start we had the question “after I call makeIterator()
, what happens to the original Sequence”? Can you still use it? If you call makeIterator()
again, does it continue? Start over? Crash?
There’s an additional question: even if it’s a “single-pass” Sequence, is it guaranteed to be finite? Maybe there should have been a separate trait for that, for extension methods like contains(_:)
. (Rust does not have a separate Iterator trait for this, though.)
Finally, is an Iterator itself a Sequence? Early Swift couldn’t support that, but arguably it is useful, again for extension methods like contains(_:)
. We never changed that, though, except for AnyIterator. (In Rust, Iterators do support IntoIterator by returning themselves.)
All of these somewhat call Sequence into question. But it’s still important for things like for loops, right?
Well…it turns out that the iterator API, returning Optional<Element>
, does not automatically result in fast assembly code. So the optimizer has to turn it back into an indexing loop anyway. And in the real world, 99% of all Sequences are Collections. So in retrospect, I think the right answer would have been for for-in loops to operate on Collections, and for while let next = iter.next()
to be good enough for other sequences. No Sequence protocol at all. There’d be a little duplication of APIs on Iterator (still useful sometimes) and Collection (so you don’t have to always call makeIterator()
to get at reduce). Maybe that’d be how to do lazy operations instead of .lazy
(pretty sure the Rust people all think we’re weird for this).
But alas, Sequence was such a core protocol that even when Swift was still being massively reformed we didn’t have the chance to rethink it or remove it. Now we (will) have AsyncSequence, which can’t work like Collection but which people still want to have a for-loop over. Still, some of Sequence’s worst issues were fixed in SE-0234, and for that I’m very glad. (Thanks to Ben Cohen and others!)
Sequence has been discussed a lot on the forums; you can look around there for more depth on this.