diff --git a/Evolution/0001-zip.md b/Evolution/0001-zip.md index 1ba48205..2e1885c5 100644 --- a/Evolution/0001-zip.md +++ b/Evolution/0001-zip.md @@ -2,9 +2,9 @@ * Proposal: [SAA-0001](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0001-zip.md) * Authors: [Philippe Hausler](https://github.com/phausler) -* Status: **Implemented** +* Status: **Accepted** -* Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncZip2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncZip3Sequence.swift) | +* Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Zip/AsyncZip2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Zip/AsyncZip3Sequence.swift) | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestZip.swift)] * Decision Notes: * Bugs: @@ -45,8 +45,7 @@ public func zip: Sendable where Base1: Sendable, Base2: Sendable, - Base1.Element: Sendable, Base2.Element: Sendable, - Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable { + Base1.Element: Sendable, Base2.Element: Sendable { public typealias Element = (Base1.Element, Base2.Element) public struct Iterator: AsyncIteratorProtocol { @@ -59,8 +58,7 @@ public struct AsyncZip2Sequence: Sen public struct AsyncZip3Sequence: Sendable where Base1: Sendable, Base2: Sendable, Base3: Sendable - Base1.Element: Sendable, Base2.Element: Sendable, Base3.Element: Sendable - Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable, Base3.AsyncIterator: Sendable { + Base1.Element: Sendable, Base2.Element: Sendable, Base3.Element: Sendable { public typealias Element = (Base1.Element, Base2.Element, Base3.Element) public struct Iterator: AsyncIteratorProtocol { diff --git a/Evolution/0002-merge.md b/Evolution/0002-merge.md index ff15f23d..b3dacfc5 100644 --- a/Evolution/0002-merge.md +++ b/Evolution/0002-merge.md @@ -2,9 +2,9 @@ * Proposal: [SAA-0002](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0002-merge.md) * Authors: [Philippe Hausler](https://github.com/phausler) -* Status: **Implemented** +* Status: **Accepted** -* Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Asyncmerge2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncMerge3Sequence.swift) | +* Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Merge/AsyncMerge2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Merge/AsyncMerge3Sequence.swift) | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestMerge.swift)] * Decision Notes: * Bugs: @@ -46,8 +46,7 @@ public struct AsyncMerge2Sequence: S where Base1.Element == Base2.Element, Base1: Sendable, Base2: Sendable, - Base1.Element: Sendable, Base2.Element: Sendable, - Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable { + Base1.Element: Sendable, Base2.Element: Sendable { public typealias Element = Base1.Element public struct Iterator: AsyncIteratorProtocol { @@ -61,8 +60,7 @@ public struct AsyncMerge3Sequence: Sendable where Base1: Sendable, Base2: Sendable, - Base1.Element: Sendable, Base2.Element: Sendable, - Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable { + Base1.Element: Sendable, Base2.Element: Sendable { public typealias Element = (Base1.Element, Base2.Element) public struct Iterator: AsyncIteratorProtocol { @@ -61,8 +60,7 @@ public struct AsyncCombineLatest2Sequence: Sendable where Base1: Sendable, Base2: Sendable, Base3: Sendable - Base1.Element: Sendable, Base2.Element: Sendable, Base3.Element: Sendable - Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable, Base3.AsyncIterator: Sendable { + Base1.Element: Sendable, Base2.Element: Sendable, Base3.Element: Sendable { public typealias Element = (Base1.Element, Base2.Element, Base3.Element) public struct Iterator: AsyncIteratorProtocol { diff --git a/Evolution/0007-chain.md b/Evolution/0007-chain.md index 8df4804f..df2aa7c1 100644 --- a/Evolution/0007-chain.md +++ b/Evolution/0007-chain.md @@ -2,7 +2,7 @@ * Proposal: [SAA-0007](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0007-chain.md) * Authors: [Philippe Hausler](https://github.com/phausler) -* Status: **Implemented** +* Status: **Accepted** * Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncChain2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncChain3Sequence.swift) | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestChain.swift)] diff --git a/Evolution/0008-bytes.md b/Evolution/0008-bytes.md index 76e2f3a0..50e2cf28 100644 --- a/Evolution/0008-bytes.md +++ b/Evolution/0008-bytes.md @@ -2,7 +2,7 @@ * Proposal: [SAA-0008](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0008-bytes.md) * Authors: [David Smith](https://github.com/Catfish-Man), [Philippe Hausler](https://github.com/phausler) -* Status: **Implemented** +* Status: **Accepted** * Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncBufferedByteIterator.swift) | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestBufferedByteIterator.swift)] @@ -33,7 +33,7 @@ struct AsyncBytes: AsyncSequence { ## Detailed Design ```swift -public struct AsyncBufferedByteIterator: AsyncIteratorProtocol, Sendable { +public struct AsyncBufferedByteIterator: AsyncIteratorProtocol { public typealias Element = UInt8 public init( diff --git a/0009-async.md b/Evolution/0009-async.md similarity index 94% rename from 0009-async.md rename to Evolution/0009-async.md index 897e57b8..a29984ab 100644 --- a/0009-async.md +++ b/Evolution/0009-async.md @@ -1,6 +1,6 @@ # AsyncSyncSequence -* Proposal: [NNNN](NNNN-lazy.md) +* Proposal: [SAA-0009](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0009-async.md) * Authors: [Philippe Hausler](https://github.com/phausler) * Status: **Implemented** diff --git a/Evolution/0009-buffer.md b/Evolution/0010-buffer.md similarity index 98% rename from Evolution/0009-buffer.md rename to Evolution/0010-buffer.md index 0e2b2fd4..da56def7 100644 --- a/Evolution/0009-buffer.md +++ b/Evolution/0010-buffer.md @@ -1,8 +1,8 @@ # Buffer -* Proposal: [SAA-0009](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0009-buffer.md) +* Proposal: [SAA-0010](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0010-buffer.md) * Author(s): [Thibault Wittemberg](https://github.com/twittemb) -* Status: **Implemented** +* Status: **Accepted** * Implementation: [ [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Buffer/AsyncBufferSequence.swift) | [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestBuffer.swift) diff --git a/Evolution/0011-interspersed.md b/Evolution/0011-interspersed.md new file mode 100644 index 00000000..79cc02f2 --- /dev/null +++ b/Evolution/0011-interspersed.md @@ -0,0 +1,135 @@ +# Feature name + +* Proposal: [SAA-0011](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0011-interspersed.md) +* Authors: [Philippe Hausler](https://github.com/phausler) +* Review Manager: [Franz Busch](https://github.com/FranzBusch) +* Status: **Implemented** + +* Implementation: + [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift) | + [Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestLazy.swift) + +## Motivation + +A common transformation that is applied to async sequences is to intersperse the elements with +a separator element. + +## Proposed solution + +We propose to add a new method on `AsyncSequence` that allows to intersperse +a separator between each emitted element. This proposed API looks like this + +```swift +extension AsyncSequence { + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. + /// + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: "-") + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` + /// + /// - Parameter separator: The value to insert in between each of this async + /// sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + public func interspersed(with separator: Element) -> AsyncInterspersedSequence { + AsyncInterspersedSequence(self, separator: separator) + } +} +``` + +## Detailed design + +The bulk of the implementation of the new `interspersed` method is inside the new +`AsyncInterspersedSequence` struct. It constructs an iterator to the base async sequence +inside its own iterator. The `AsyncInterspersedSequence.Iterator.next()` is forwarding the demand +to the base iterator. +There is one special case that we have to call out. When the base async sequence throws +then `AsyncInterspersedSequence.Iterator.next()` will return the separator first and then rethrow the error. + +Below is the implementation of the `AsyncInterspersedSequence`. +```swift +/// An asynchronous sequence that presents the elements of a base asynchronous sequence of +/// elements with a separator between each of those elements. +public struct AsyncInterspersedSequence { + @usableFromInline + internal let base: Base + + @usableFromInline + internal let separator: Base.Element + + @usableFromInline + internal init(_ base: Base, separator: Base.Element) { + self.base = base + self.separator = separator + } +} + +extension AsyncInterspersedSequence: AsyncSequence { + public typealias Element = Base.Element + + /// The iterator for an `AsyncInterspersedSequence` asynchronous sequence. + public struct AsyncIterator: AsyncIteratorProtocol { + @usableFromInline + internal enum State { + case start + case element(Result) + case separator + } + + @usableFromInline + internal var iterator: Base.AsyncIterator + + @usableFromInline + internal let separator: Base.Element + + @usableFromInline + internal var state = State.start + + @usableFromInline + internal init(_ iterator: Base.AsyncIterator, separator: Base.Element) { + self.iterator = iterator + self.separator = separator + } + + public mutating func next() async rethrows -> Base.Element? { + // After the start, the state flips between element and separator. Before + // returning a separator, a check is made for the next element as a + // separator is only returned between two elements. The next element is + // stored to allow it to be returned in the next iteration. However, if + // the checking the next element throws, the separator is emitted before + // rethrowing that error. + switch state { + case .start: + state = .separator + return try await iterator.next() + case .separator: + do { + guard let next = try await iterator.next() else { return nil } + state = .element(.success(next)) + } catch { + state = .element(.failure(error)) + } + return separator + case .element(let result): + state = .separator + return try result._rethrowGet() + } + } + } + + @inlinable + public func makeAsyncIterator() -> AsyncInterspersedSequence.AsyncIterator { + AsyncIterator(base.makeAsyncIterator(), separator: separator) + } +} +``` diff --git a/Sources/AsyncAlgorithms/AsyncInterspersedSequence.swift b/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift similarity index 81% rename from Sources/AsyncAlgorithms/AsyncInterspersedSequence.swift rename to Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift index 43f8d974..7dd08c53 100644 --- a/Sources/AsyncAlgorithms/AsyncInterspersedSequence.swift +++ b/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift @@ -10,10 +10,21 @@ //===----------------------------------------------------------------------===// extension AsyncSequence { - /// Returns an asynchronous sequence containing elements of this asynchronous sequence with - /// the given separator inserted in between each element. + /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting + /// the given separator between each element. /// - /// Any value of the asynchronous sequence's element type can be used as the separator. + /// Any value of this asynchronous sequence's element type can be used as the separator. + /// + /// The following example shows how an async sequences of `String`s can be interspersed using `-` as the separator: + /// + /// ``` + /// let input = ["A", "B", "C"].async + /// let interspersed = input.interspersed(with: "-") + /// for await element in interspersed { + /// print(element) + /// } + /// // Prints "A" "-" "B" "-" "C" + /// ``` /// /// - Parameter separator: The value to insert in between each of this async /// sequence’s elements. @@ -95,8 +106,11 @@ extension AsyncInterspersedSequence: AsyncSequence { @inlinable public func makeAsyncIterator() -> AsyncInterspersedSequence.Iterator { - Iterator(base.makeAsyncIterator(), separator: separator) + Iterator(base.makeAsyncIterator(), separator: separator) } } extension AsyncInterspersedSequence: Sendable where Base: Sendable, Base.Element: Sendable { } + +@available(*, unavailable) +extension AsyncInterspersedSequence.Iterator: Sendable {} diff --git a/Tests/AsyncAlgorithmsTests/TestInterspersed.swift b/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift similarity index 71% rename from Tests/AsyncAlgorithmsTests/TestInterspersed.swift rename to Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift index 79c93f73..2f351ab0 100644 --- a/Tests/AsyncAlgorithmsTests/TestInterspersed.swift +++ b/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift @@ -66,26 +66,33 @@ final class TestInterspersed: XCTestCase { func test_cancellation() async { let source = Indefinite(value: "test") let sequence = source.async.interspersed(with: "sep") - let finished = expectation(description: "finished") - let iterated = expectation(description: "iterated") - let task = Task { + let lockStepChannel = AsyncChannel() - var iterator = sequence.makeAsyncIterator() - let _ = await iterator.next() - iterated.fulfill() + await withTaskGroup(of: Void.self) { group in + group.addTask { + var iterator = sequence.makeAsyncIterator() + let _ = await iterator.next() - while let _ = await iterator.next() { } + // Information the parent task that we are consuming + await lockStepChannel.send(()) - let pastEnd = await iterator.next() - XCTAssertNil(pastEnd) + while let _ = await iterator.next() { } - finished.fulfill() + let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) + + // Information the parent task that we finished consuming + await lockStepChannel.send(()) + } + + // Waiting until the child task started consuming + _ = await lockStepChannel.first { _ in true } + + // Now we cancel the child + group.cancelAll() + + // Waiting until the child task finished consuming + _ = await lockStepChannel.first { _ in true } } - // ensure the other task actually starts - wait(for: [iterated], timeout: 1.0) - // cancellation should ensure the loop finishes - // without regards to the remaining underlying sequence - task.cancel() - wait(for: [finished], timeout: 1.0) } }