No. Rather, we're learning from Swift's prior art (IteratorProtocol), which just like Rust's Iterator, is based around returning one element at a time. This is simpler, but without optimization can be a real problem for performance. I think it is tempting here to think of "simple" and "elegant" as the same thing – but this isn't the case here. Rather, the simplicity leads to inelgance in the code that is generated. A wooden plank is simple, but inelegant as a way of crossing anything wider than a stream.
Ultimately, you want loops to compile down to something radically different from how they logically appear in the surface language. In the ideal and common case of a single contiguous blob of storage, you want to be able to eliminate things like bounds checks and function calls entirely and have a loop become just advancing a pointer over direct memory access until it reaches the end. Depending on what you're doing in the loop, the compiler may use pointers to objects in memory, or it may load things into registers, or even vectorize the loop using SIMD instructions.
Those assembly instructions will likely, and hopefully, bear little resemblance to the on the face of it much higher level "one at a time, call a function that returns an optional of the element type, unwrap that optional, and use the unwrapped value". Achieving this uses the well-known compiler technique of 1) inline the heck out of everything, and 2) throw the often hundreds of lines of IR that spills out at the optimizer, and cross your fingers. Unfortunately this technique is prone to failure, and you can end up with something closer to the surface-level logic even at the compiled binary.
So while this "seems simple, but it's not really what you want at the machine level" model mostly works out, there are lots of cases where it doesn't and you fall off the performance cliff:
- In debug mode, where the optimizer doesn't eliminate many abstractions;
- In complex code, where the optimizer can't chew through enough of the abstractions; and
- With dynamically dispatched sequences, where you're not able to inline calls to
next into the caller.
Now this last one is super important. It doesn't apply as much to Rust, which doesn't have a stable ABI or unspecialized generics, though you still can have dyn Iterator sometimes. But in Swift, it's really important to be able to write non-inlinable code in an ABI-stable framework that takes some BorrowingSequence and not have terrible performance when you pass it an Array.
This is the big difference with the bulk iteration approach of BorrowingSequence because it serves up a concrete type of Span<Element>. The call to makeBorrowingIterator and nextSpan may be dynamically dispatched, but once you have the span, you get to specialize iteration of it on the other side of the boundary. In the case of a single contiguously-stored collection, you only have to do that once.
This is what the increased complexity of serving up spans instead of elements gets you – a much more optimizable design. It also opens up more possibilities for e.g. optimizing the span operations even in debug mode, where more complex intraprocedural optimizations are undesirable (e.g. because they interfere with things like stepping in the debugger).
Note however that the complexity is not very "in your face" for many users.
In the case of consumers of the interface, nearly all code will continue to just be written against for loops, which just happen to desugar differently unbeknownst to the user. Yes there are some (relatively uncommon) cases where you cannot write a for loop with Sequence and have to reach directly for the iterator, and those algorithms are made more complex by the provision of spans. My feeling here is we should see how much of a problem this is, and provide a helper that re-creates the single-element next() operation if needed (you basically just call nextSpan(maximumCount: 1) and return the single element). Whether this is worth it probably will follow from real-world adoption feedback.
In the case of conformances: In a lot of cases, a user-defined type conforming to Sequence is a wrapper on top of another sequence-conforming type, and implementing the conformance will involve delegating to the wrapped type. In these cases, conforming to BorrowingSequence will take similar code, just forwarding on to nextSpan instead of next.
The proposal outlines an adaptor that allows a Sequence to also conform to BorrowingSequence. This makes it trivial for types that serve up elements one at a time to conform. The future direction of re-parenting Sequence on top of BorrowingSequence will mean this happens automatically.