Skip to content

Commit 99913fc

Browse files
committed
Add Interspersed proposal
# Motivation Add a new evolution proposal for the interspersed algorithm.
1 parent e9d6216 commit 99913fc

File tree

3 files changed

+176
-20
lines changed

3 files changed

+176
-20
lines changed

Evolution/0011-interspersed.md

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
# Feature name
2+
3+
* Proposal: [SAA-0011](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0011-interspersed.md)
4+
* Authors: [Philippe Hausler](https://github.com/phausler)
5+
* Review Manager: [Franz Busch](https://github.com/FranzBusch)
6+
* Status: **Implemented**
7+
8+
* Implementation:
9+
[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift) |
10+
[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestLazy.swift)
11+
12+
## Motivation
13+
14+
A common transformation that is applied to async sequences is to intersperse the elements with
15+
a separator element.
16+
17+
## Proposed solution
18+
19+
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
21+
22+
```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+
}
47+
}
48+
```
49+
50+
## Detailed design
51+
52+
The bulk of the implementation of the new `interspersed` method is inside the new
53+
`AsyncInterspersedSequence` struct. It constructs an iterator to the base async sequence
54+
inside its own iterator. The `AsyncInterspersedSequence.Iterator.next()` is forwarding the demand
55+
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.
58+
59+
Below is the implementation of the `AsyncInterspersedSequence`.
60+
```swift
61+
/// An asynchronous sequence that presents the elements of a base asynchronous sequence of
62+
/// elements with a separator between each of those elements.
63+
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+
}
76+
77+
extension AsyncInterspersedSequence: AsyncSequence {
78+
public typealias Element = Base.Element
79+
80+
/// The iterator for an `AsyncInterspersedSequence` asynchronous sequence.
81+
public struct AsyncIterator: AsyncIteratorProtocol {
82+
@usableFromInline
83+
internal enum State {
84+
case start
85+
case element(Result<Base.Element, Error>)
86+
case separator
87+
}
88+
89+
@usableFromInline
90+
internal var iterator: Base.AsyncIterator
91+
92+
@usableFromInline
93+
internal let separator: Base.Element
94+
95+
@usableFromInline
96+
internal var state = State.start
97+
98+
@usableFromInline
99+
internal init(_ iterator: Base.AsyncIterator, separator: Base.Element) {
100+
self.iterator = iterator
101+
self.separator = separator
102+
}
103+
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+
}
127+
}
128+
}
129+
130+
@inlinable
131+
public func makeAsyncIterator() -> AsyncInterspersedSequence<Base>.AsyncIterator {
132+
AsyncIterator(base.makeAsyncIterator(), separator: separator)
133+
}
134+
}
135+
```

Sources/AsyncAlgorithms/AsyncInterspersedSequence.swift Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift

+18-4
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,21 @@
1010
//===----------------------------------------------------------------------===//
1111

1212
extension AsyncSequence {
13-
/// Returns an asynchronous sequence containing elements of this asynchronous sequence with
14-
/// the given separator inserted in between each element.
13+
/// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting
14+
/// the given separator between each element.
1515
///
16-
/// Any value of the asynchronous sequence's element type can be used as the separator.
16+
/// Any value of this asynchronous sequence's element type can be used as the separator.
17+
///
18+
/// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator:
19+
///
20+
/// ```
21+
/// let input = ["A", "B", "C"].async
22+
/// let interspersed = input.interspersed(with: "-")
23+
/// for await element in interspersed {
24+
/// print(element)
25+
/// }
26+
/// // Prints "A" "-" "B" "-" "C"
27+
/// ```
1728
///
1829
/// - Parameter separator: The value to insert in between each of this async
1930
/// sequence’s elements.
@@ -95,8 +106,11 @@ extension AsyncInterspersedSequence: AsyncSequence {
95106

96107
@inlinable
97108
public func makeAsyncIterator() -> AsyncInterspersedSequence<Base>.Iterator {
98-
Iterator(base.makeAsyncIterator(), separator: separator)
109+
AsyncIterator(base.makeAsyncIterator(), separator: separator)
99110
}
100111
}
101112

102113
extension AsyncInterspersedSequence: Sendable where Base: Sendable, Base.Element: Sendable { }
114+
115+
@available(*, unavailable)
116+
extension AsyncInterspersedSequence.Iterator: Sendable {}

Tests/AsyncAlgorithmsTests/TestInterspersed.swift Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift

+23-16
Original file line numberDiff line numberDiff line change
@@ -66,26 +66,33 @@ final class TestInterspersed: XCTestCase {
6666
func test_cancellation() async {
6767
let source = Indefinite(value: "test")
6868
let sequence = source.async.interspersed(with: "sep")
69-
let finished = expectation(description: "finished")
70-
let iterated = expectation(description: "iterated")
71-
let task = Task {
69+
let lockStepChannel = AsyncChannel<Void>()
7270

73-
var iterator = sequence.makeAsyncIterator()
74-
let _ = await iterator.next()
75-
iterated.fulfill()
71+
await withTaskGroup(of: Void.self) { group in
72+
group.addTask {
73+
var iterator = sequence.makeAsyncIterator()
74+
let _ = await iterator.next()
7675

77-
while let _ = await iterator.next() { }
76+
// Information the parent task that we are consuming
77+
await lockStepChannel.send(())
7878

79-
let pastEnd = await iterator.next()
80-
XCTAssertNil(pastEnd)
79+
while let _ = await iterator.next() { }
8180

82-
finished.fulfill()
81+
let pastEnd = await iterator.next()
82+
XCTAssertNil(pastEnd)
83+
84+
// Information the parent task that we finished consuming
85+
await lockStepChannel.send(())
86+
}
87+
88+
// Waiting until the child task started consuming
89+
_ = await lockStepChannel.first { _ in true }
90+
91+
// Now we cancel the child
92+
group.cancelAll()
93+
94+
// Waiting until the child task finished consuming
95+
_ = await lockStepChannel.first { _ in true }
8396
}
84-
// ensure the other task actually starts
85-
wait(for: [iterated], timeout: 1.0)
86-
// cancellation should ensure the loop finishes
87-
// without regards to the remaining underlying sequence
88-
task.cancel()
89-
wait(for: [finished], timeout: 1.0)
9097
}
9198
}

0 commit comments

Comments
 (0)