Skip to content

Commit 936a68d

Browse files
authored
[AsyncInterspersedSequence] Integrate review feedback (#267)
* Integrate review feedback This integrates all of the feedback from the review thread. Here is a quick summary: - Change the trailing separator behaviour. We are no longer returning a separator before we are forwarding the error - Add a synchronous and asynchronous closure based `interspersed` method. - Support interspersing every n elements * Add AsyncThrowingInterspersedSequence * Update examples
1 parent 9274790 commit 936a68d

File tree

3 files changed

+829
-229
lines changed

3 files changed

+829
-229
lines changed

Evolution/0011-interspersed.md

+266-82
Original file line numberDiff line numberDiff line change
@@ -17,33 +17,134 @@ a separator element.
1717
## Proposed solution
1818

1919
We propose to add a new method on `AsyncSequence` that allows to intersperse
20-
a separator between each emitted element. This proposed API looks like this
20+
a separator between every n emitted element. This proposed API looks like this
2121

2222
```swift
23-
extension AsyncSequence {
24-
/// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting
25-
/// the given separator between each element.
26-
///
27-
/// Any value of this asynchronous sequence's element type can be used as the separator.
28-
///
29-
/// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator:
30-
///
31-
/// ```
32-
/// let input = ["A", "B", "C"].async
33-
/// let interspersed = input.interspersed(with: "-")
34-
/// for await element in interspersed {
35-
/// print(element)
36-
/// }
37-
/// // Prints "A" "-" "B" "-" "C"
38-
/// ```
39-
///
40-
/// - Parameter separator: The value to insert in between each of this async
41-
/// sequence’s elements.
42-
/// - Returns: The interspersed asynchronous sequence of elements.
43-
@inlinable
44-
public func interspersed(with separator: Element) -> AsyncInterspersedSequence<Self> {
45-
AsyncInterspersedSequence(self, separator: separator)
46-
}
23+
public extension AsyncSequence {
24+
/// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting
25+
/// the given separator between each element.
26+
///
27+
/// Any value of this asynchronous sequence's element type can be used as the separator.
28+
///
29+
/// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator:
30+
///
31+
/// ```
32+
/// let input = ["A", "B", "C"].async
33+
/// let interspersed = input.interspersed(with: "-")
34+
/// for await element in interspersed {
35+
/// print(element)
36+
/// }
37+
/// // Prints "A" "-" "B" "-" "C"
38+
/// ```
39+
///
40+
/// - Parameters:
41+
/// - every: Dictates after how many elements a separator should be inserted.
42+
/// - separator: The value to insert in between each of this async sequence’s elements.
43+
/// - Returns: The interspersed asynchronous sequence of elements.
44+
@inlinable
45+
func interspersed(every: Int = 1, with separator: Element) -> AsyncInterspersedSequence<Self> {
46+
AsyncInterspersedSequence(self, every: every, separator: separator)
47+
}
48+
49+
/// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting
50+
/// the given separator between each element.
51+
///
52+
/// Any value of this asynchronous sequence's element type can be used as the separator.
53+
///
54+
/// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator:
55+
///
56+
/// ```
57+
/// let input = ["A", "B", "C"].async
58+
/// let interspersed = input.interspersed(with: "-")
59+
/// for await element in interspersed {
60+
/// print(element)
61+
/// }
62+
/// // Prints "A" "-" "B" "-" "C"
63+
/// ```
64+
///
65+
/// - Parameters:
66+
/// - every: Dictates after how many elements a separator should be inserted.
67+
/// - separator: A closure that produces the value to insert in between each of this async sequence’s elements.
68+
/// - Returns: The interspersed asynchronous sequence of elements.
69+
@inlinable
70+
func interspersed(every: Int = 1, with separator: @Sendable @escaping () -> Element) -> AsyncInterspersedSequence<Self> {
71+
AsyncInterspersedSequence(self, every: every, separator: separator)
72+
}
73+
74+
/// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting
75+
/// the given separator between each element.
76+
///
77+
/// Any value of this asynchronous sequence's element type can be used as the separator.
78+
///
79+
/// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator:
80+
///
81+
/// ```
82+
/// let input = ["A", "B", "C"].async
83+
/// let interspersed = input.interspersed(with: "-")
84+
/// for await element in interspersed {
85+
/// print(element)
86+
/// }
87+
/// // Prints "A" "-" "B" "-" "C"
88+
/// ```
89+
///
90+
/// - Parameters:
91+
/// - every: Dictates after how many elements a separator should be inserted.
92+
/// - separator: A closure that produces the value to insert in between each of this async sequence’s elements.
93+
/// - Returns: The interspersed asynchronous sequence of elements.
94+
@inlinable
95+
func interspersed(every: Int = 1, with separator: @Sendable @escaping () async -> Element) -> AsyncInterspersedSequence<Self> {
96+
AsyncInterspersedSequence(self, every: every, separator: separator)
97+
}
98+
99+
/// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting
100+
/// the given separator between each element.
101+
///
102+
/// Any value of this asynchronous sequence's element type can be used as the separator.
103+
///
104+
/// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator:
105+
///
106+
/// ```
107+
/// let input = ["A", "B", "C"].async
108+
/// let interspersed = input.interspersed(with: "-")
109+
/// for await element in interspersed {
110+
/// print(element)
111+
/// }
112+
/// // Prints "A" "-" "B" "-" "C"
113+
/// ```
114+
///
115+
/// - Parameters:
116+
/// - every: Dictates after how many elements a separator should be inserted.
117+
/// - separator: A closure that produces the value to insert in between each of this async sequence’s elements.
118+
/// - Returns: The interspersed asynchronous sequence of elements.
119+
@inlinable
120+
public func interspersed(every: Int = 1, with separator: @Sendable @escaping () throws -> Element) -> AsyncThrowingInterspersedSequence<Self> {
121+
AsyncThrowingInterspersedSequence(self, every: every, separator: separator)
122+
}
123+
124+
/// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting
125+
/// the given separator between each element.
126+
///
127+
/// Any value of this asynchronous sequence's element type can be used as the separator.
128+
///
129+
/// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator:
130+
///
131+
/// ```
132+
/// let input = ["A", "B", "C"].async
133+
/// let interspersed = input.interspersed(with: "-")
134+
/// for await element in interspersed {
135+
/// print(element)
136+
/// }
137+
/// // Prints "A" "-" "B" "-" "C"
138+
/// ```
139+
///
140+
/// - Parameters:
141+
/// - every: Dictates after how many elements a separator should be inserted.
142+
/// - separator: A closure that produces the value to insert in between each of this async sequence’s elements.
143+
/// - Returns: The interspersed asynchronous sequence of elements.
144+
@inlinable
145+
public func interspersed(every: Int = 1, with separator: @Sendable @escaping () async throws -> Element) -> AsyncThrowingInterspersedSequence<Self> {
146+
AsyncThrowingInterspersedSequence(self, every: every, separator: separator)
147+
}
47148
}
48149
```
49150

@@ -53,83 +154,166 @@ The bulk of the implementation of the new `interspersed` method is inside the ne
53154
`AsyncInterspersedSequence` struct. It constructs an iterator to the base async sequence
54155
inside its own iterator. The `AsyncInterspersedSequence.Iterator.next()` is forwarding the demand
55156
to the base iterator.
56-
There is one special case that we have to call out. When the base async sequence throws
57-
then `AsyncInterspersedSequence.Iterator.next()` will return the separator first and then rethrow the error.
58157

59158
Below is the implementation of the `AsyncInterspersedSequence`.
60159
```swift
61160
/// An asynchronous sequence that presents the elements of a base asynchronous sequence of
62161
/// elements with a separator between each of those elements.
63162
public struct AsyncInterspersedSequence<Base: AsyncSequence> {
64-
@usableFromInline
65-
internal let base: Base
66-
67-
@usableFromInline
68-
internal let separator: Base.Element
69-
70-
@usableFromInline
71-
internal init(_ base: Base, separator: Base.Element) {
72-
self.base = base
73-
self.separator = separator
74-
}
75-
}
163+
@usableFromInline
164+
internal enum Separator {
165+
case element(Element)
166+
case syncClosure(@Sendable () -> Element)
167+
case asyncClosure(@Sendable () async -> Element)
168+
}
76169

77-
extension AsyncInterspersedSequence: AsyncSequence {
78-
public typealias Element = Base.Element
170+
@usableFromInline
171+
internal let base: Base
79172

80-
/// The iterator for an `AsyncInterspersedSequence` asynchronous sequence.
81-
public struct AsyncIterator: AsyncIteratorProtocol {
82173
@usableFromInline
83-
internal enum State {
84-
case start
85-
case element(Result<Base.Element, Error>)
86-
case separator
87-
}
174+
internal let separator: Separator
88175

89176
@usableFromInline
90-
internal var iterator: Base.AsyncIterator
177+
internal let every: Int
91178

92179
@usableFromInline
93-
internal let separator: Base.Element
180+
internal init(_ base: Base, every: Int, separator: Element) {
181+
precondition(every > 0, "Separators can only be interspersed ever 1+ elements")
182+
self.base = base
183+
self.separator = .element(separator)
184+
self.every = every
185+
}
94186

95187
@usableFromInline
96-
internal var state = State.start
188+
internal init(_ base: Base, every: Int, separator: @Sendable @escaping () -> Element) {
189+
precondition(every > 0, "Separators can only be interspersed ever 1+ elements")
190+
self.base = base
191+
self.separator = .syncClosure(separator)
192+
self.every = every
193+
}
97194

98195
@usableFromInline
99-
internal init(_ iterator: Base.AsyncIterator, separator: Base.Element) {
100-
self.iterator = iterator
101-
self.separator = separator
196+
internal init(_ base: Base, every: Int, separator: @Sendable @escaping () async -> Element) {
197+
precondition(every > 0, "Separators can only be interspersed ever 1+ elements")
198+
self.base = base
199+
self.separator = .asyncClosure(separator)
200+
self.every = every
102201
}
202+
}
203+
204+
extension AsyncInterspersedSequence: AsyncSequence {
205+
public typealias Element = Base.Element
206+
207+
/// The iterator for an `AsyncInterspersedSequence` asynchronous sequence.
208+
public struct Iterator: AsyncIteratorProtocol {
209+
@usableFromInline
210+
internal enum State {
211+
case start(Element?)
212+
case element(Int)
213+
case separator
214+
case finished
215+
}
216+
217+
@usableFromInline
218+
internal var iterator: Base.AsyncIterator
219+
220+
@usableFromInline
221+
internal let separator: Separator
222+
223+
@usableFromInline
224+
internal let every: Int
225+
226+
@usableFromInline
227+
internal var state = State.start(nil)
103228

104-
public mutating func next() async rethrows -> Base.Element? {
105-
// After the start, the state flips between element and separator. Before
106-
// returning a separator, a check is made for the next element as a
107-
// separator is only returned between two elements. The next element is
108-
// stored to allow it to be returned in the next iteration. However, if
109-
// the checking the next element throws, the separator is emitted before
110-
// rethrowing that error.
111-
switch state {
112-
case .start:
113-
state = .separator
114-
return try await iterator.next()
115-
case .separator:
116-
do {
117-
guard let next = try await iterator.next() else { return nil }
118-
state = .element(.success(next))
119-
} catch {
120-
state = .element(.failure(error))
121-
}
122-
return separator
123-
case .element(let result):
124-
state = .separator
125-
return try result._rethrowGet()
126-
}
229+
@usableFromInline
230+
internal init(_ iterator: Base.AsyncIterator, every: Int, separator: Separator) {
231+
self.iterator = iterator
232+
self.separator = separator
233+
self.every = every
234+
}
235+
236+
public mutating func next() async rethrows -> Base.Element? {
237+
// After the start, the state flips between element and separator. Before
238+
// returning a separator, a check is made for the next element as a
239+
// separator is only returned between two elements. The next element is
240+
// stored to allow it to be returned in the next iteration. However, if
241+
// the checking the next element throws, the separator is emitted before
242+
// rethrowing that error.
243+
switch state {
244+
case var .start(element):
245+
do {
246+
if element == nil {
247+
element = try await self.iterator.next()
248+
}
249+
250+
if let element = element {
251+
if every == 1 {
252+
state = .separator
253+
} else {
254+
state = .element(1)
255+
}
256+
return element
257+
} else {
258+
state = .finished
259+
return nil
260+
}
261+
} catch {
262+
state = .finished
263+
throw error
264+
}
265+
266+
case .separator:
267+
do {
268+
if let element = try await iterator.next() {
269+
state = .start(element)
270+
switch separator {
271+
case let .element(element):
272+
return element
273+
274+
case let .syncClosure(closure):
275+
return closure()
276+
277+
case let .asyncClosure(closure):
278+
return await closure()
279+
}
280+
} else {
281+
state = .finished
282+
return nil
283+
}
284+
} catch {
285+
state = .finished
286+
throw error
287+
}
288+
289+
case let .element(count):
290+
do {
291+
if let element = try await iterator.next() {
292+
let newCount = count + 1
293+
if every == newCount {
294+
state = .separator
295+
} else {
296+
state = .element(newCount)
297+
}
298+
return element
299+
} else {
300+
state = .finished
301+
return nil
302+
}
303+
} catch {
304+
state = .finished
305+
throw error
306+
}
307+
308+
case .finished:
309+
return nil
310+
}
311+
}
127312
}
128-
}
129313

130-
@inlinable
131-
public func makeAsyncIterator() -> AsyncInterspersedSequence<Base>.AsyncIterator {
132-
AsyncIterator(base.makeAsyncIterator(), separator: separator)
133-
}
314+
@inlinable
315+
public func makeAsyncIterator() -> AsyncInterspersedSequence<Base>.Iterator {
316+
Iterator(base.makeAsyncIterator(), every: every, separator: separator)
317+
}
134318
}
135319
```

0 commit comments

Comments
 (0)