Skip to content

Commit 505e2ca

Browse files
author
Tim Vermeulen
authored
[Proposal] Add indexed() and Collection conformances for enumerated() and zip(_:_:) (#1326)
* Add proposal for `indexed()` and `enumerated()` and `zip(_:_:)` collections
1 parent a6acd5f commit 505e2ca

File tree

1 file changed

+152
-0
lines changed

1 file changed

+152
-0
lines changed
Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
# Add `indexed()` and `Collection` conformances for `enumerated()` and `zip(_:_:)`
2+
3+
* Proposal: [SE-0312](0312-indexed-and-enumerated-zip-collections.md)
4+
* Author: [Tim Vermeulen](https://github.com/timvermeulen)
5+
* Review Manager: [Ben Cohen](https://github.com/airspeedswift)
6+
* Status: **Active Review (Apr 27 - May 7 2021)**
7+
* Implementation: [apple/swift#36851](https://github.com/apple/swift/pull/36851)
8+
9+
## Introduction
10+
This proposal aims to fix the lack of `Collection` conformance of the sequences returned by `zip(_:_:)` and `enumerated()`, preventing them from being used in a context that requires a `Collection`. Also included is the addition of the `indexed()` method on `Collection` as a more ergonomic, efficient, and correct alternative to `c.enumerated()` and `zip(c.indices, c)`.
11+
12+
Swift-evolution thread: [Pitch](https://forums.swift.org/t/pitch-add-indexed-and-collection-conformances-for-enumerated-and-zip/47288)
13+
14+
## Motivation
15+
Currently, the `Zip2Sequence` and `EnumeratedSequence` types conform to `Sequence`, but not to any of the collection protocols. Adding these conformances was impossible before [SE-0234 Remove `Sequence.SubSequence`](https://github.com/apple/swift-evolution/blob/main/proposals/0234-remove-sequence-subsequence.md), and would have been an ABI breaking change before the language allowed `@available` annotations on protocol conformances ([PR](https://github.com/apple/swift/pull/34651)). Now we can add them!
16+
17+
Conformance to the collection protocols can be beneficial in a variety of ways, for example:
18+
* `(1000..<2000).enumerated().dropFirst(500)` becomes a constant time operation.
19+
* `zip("abc", [1, 2, 3]).reversed()` will return a `ReversedCollection` rather than allocating a new array.
20+
* SwiftUI’s `List` and `ForEach` views will be able to directly take an enumerated or zipped collection as their data.
21+
22+
This proposal also includes the addition of the `indexed()` method (which can already be found in the [Swift Algorithms](https://github.com/apple/swift-algorithms) package) as an alternative for many use cases of `zip(_:_:)` and `enumerated()`. When the goal is to iterate over a collection’s elements and indices at the same time, `enumerated()` is often inadequate because it provides an offset, not a true index. For many collections this integer offset is different from the `Index` type, and in the case of `ArraySlice` in particular this offset is a common source of bugs when the slice’s `startIndex` isn’t `0`. `zip(c.indices, c)` solves these problems, but it is less ergonomic than `indexed()` and potentially less performant when traversing the indices of a collection is computationally expensive.
23+
24+
## Detailed design
25+
Conditionally conform `Zip2Sequence` to `Collection`, `BidirectionalCollection`, and `RandomAccessCollection`.
26+
27+
> **Note**: OS version 9999 is a placeholder and will be replaced with whatever actual OS versions this functionality will be introduced in.
28+
29+
```swift
30+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
31+
extension Zip2Sequence: Collection
32+
where Sequence1: Collection, Sequence2: Collection
33+
{
34+
// ...
35+
}
36+
37+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
38+
extension Zip2Sequence: BidirectionalCollection
39+
where Sequence1: BidirectionalCollection, Sequence2: BidirectionalCollection
40+
{
41+
// ...
42+
}
43+
44+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
45+
extension Zip2Sequence: RandomAccessCollection
46+
where Sequence1: RandomAccessCollection, Sequence2: RandomAccessCollection {}
47+
```
48+
49+
Conditionally conform `EnumeratedSequence` to `Collection`, `BidirectionalCollection`, `RandomAccesCollection`, and `LazyCollectionProtocol`.
50+
51+
```swift
52+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
53+
extension EnumeratedSequence: Collection where Base: Collection {
54+
// ...
55+
}
56+
57+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
58+
extension EnumeratedSequence: BidirectionalCollection
59+
where Base: BidirectionalCollection
60+
{
61+
// ...
62+
}
63+
64+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
65+
extension EnumeratedSequence: RandomAccessCollection
66+
where Base: RandomAccessCollection {}
67+
68+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
69+
extension EnumeratedSequence: LazySequenceProtocol
70+
where Base: LazySequenceProtocol {}
71+
72+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
73+
extension EnumeratedSequence: LazyCollectionProtocol
74+
where Base: LazyCollectionProtocol {}
75+
```
76+
77+
Add an `indexed()` method to `Collection` that returns a collection over (index, element) pairs of the original collection.
78+
79+
```swift
80+
extension Collection {
81+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
82+
public func indexed() -> Indexed<Self> {
83+
Indexed(_base: self)
84+
}
85+
}
86+
87+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
88+
public struct Indexed<Base: Collection> {
89+
// ...
90+
}
91+
92+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
93+
extension Indexed: Collection {
94+
// ...
95+
}
96+
97+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
98+
extension Indexed: BidirectionalCollection where Base: BidirectionalCollection {
99+
// ...
100+
}
101+
102+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
103+
extension Indexed: RandomAccessCollection where Base: RandomAccessCollection {}
104+
105+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
106+
extension Indexed: LazySequenceProtocol where Base: LazySequenceProtocol {}
107+
108+
@available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
109+
extension Indexed: LazyCollectionProtocol where Base: LazyCollectionProtocol {}
110+
```
111+
112+
## Source compatibility
113+
Adding `LazySequenceProtocol` conformance for `EnumeratedSequence` is a breaking change for code that relies on the `enumerated()` method currently not propagating `LazySequenceProtocol` conformance in a lazy chain:
114+
115+
```swift
116+
extension Sequence {
117+
func everyOther_v1() -> [Element] {
118+
let x = self.lazy
119+
.enumerated()
120+
.filter { $0.offset.isMultiple(of: 2) }
121+
.map(\.element)
122+
123+
// error: Cannot convert return expression of type 'LazyMapSequence<...>' to return type '[Self.Element]'
124+
return x
125+
}
126+
127+
func everyOther_v2() -> [Element] {
128+
// will keep working, the eager overload of `map` is picked
129+
return self.lazy
130+
.enumerated()
131+
.filter { $0.offset.isMultiple(of: 2) }
132+
.map(\.element)
133+
}
134+
}
135+
```
136+
137+
All protocol conformances of an existing type to an existing protocol are potentially source breaking because users could have added the exact same conformances themselves. However, given that `Zip2Sequence` and `EnumeratedSequence` do not expose their underlying sequences, there is no reasonable way anyone could have conformed either type to `Collection` themselves. The only sensible conformance that could conflict with one of the conformances added in this proposal is the conformance of `EnumeratedSequence` to `LazySequenceProtocol`.
138+
139+
## Effect on ABI stability
140+
This proposal does not affect ABI stability.
141+
142+
## Alternatives considered
143+
#### Don’t add `LazyCollectionProtocol` conformance for `EnumeratedSequence` for the sake of source compatibility.
144+
We consider it a bug that `enumerated()` currently does not propagate laziness in a lazy chain.
145+
146+
#### Only conform `Zip2Sequence` and `EnumeratedSequence` to `BidirectionalCollection` when the base collections conform to `RandomAccessCollection`.
147+
Traversing an `EnumeratedSequence` backwards requires computing the `count` of the collection upfront in order to determine the correct offsets, which is an O(count) operation when the base collection does not conform to `RandomAccessCollection`. This is a one-time cost incurred when `c.index(before: c.endIndex)` is called, and does not affect the overall time complexity of an entire backwards traversal. Besides, `index(before:)` does not have any performance requirements that need to be adhered to.
148+
149+
Similarly, `Zip2Sequence` requires finding the index of the longer of the two collections that corresponds to the end index of the shorter collection, when doing a backwards traversal. As with `EnumeratedSequence`, this adds a one-time O(n) cost that does not violate any performance requirements.
150+
151+
#### Keep `EnumeratedSequence` the way it is and add an `enumerated()` overload to `Collection` that returns a `Zip2Sequence<Range<Int>, Self>`.
152+
This is tempting because `enumerated()` is little more than `zip(0..., self)`, but this would cause an unacceptable amount of source breakage due to the lack of `offset` and `element` tuple labels that `EnumeratedSequence` provides.

0 commit comments

Comments
 (0)