From 8738abd19e07111120c5579cf98bd1f520367365 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Thu, 1 Jun 2023 13:35:38 +0100 Subject: [PATCH 1/3] 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 --- Evolution/0011-interspersed.md | 298 +++++++++++++----- .../AsyncInterspersedSequence.swift | 297 ++++++++++++----- .../Interspersed/TestInterspersed.swift | 185 +++++++---- 3 files changed, 548 insertions(+), 232 deletions(-) diff --git a/Evolution/0011-interspersed.md b/Evolution/0011-interspersed.md index 79cc02f2..31956236 100644 --- a/Evolution/0011-interspersed.md +++ b/Evolution/0011-interspersed.md @@ -17,33 +17,84 @@ 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 +a separator between every n 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) - } +public 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" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: The value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + func interspersed(every: Int = 1, with separator: Element) -> AsyncInterspersedSequence { + AsyncInterspersedSequence(self, every: every, separator: separator) + } + + /// 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" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + func interspersed(every: Int = 1, with separator: @Sendable @escaping () -> Element) -> AsyncInterspersedSequence { + AsyncInterspersedSequence(self, every: every, separator: separator) + } + + /// 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" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + func interspersed(every: Int = 1, with separator: @Sendable @escaping () async -> Element) -> AsyncInterspersedSequence { + AsyncInterspersedSequence(self, every: every, separator: separator) + } } ``` @@ -53,83 +104,166 @@ The bulk of the implementation of the new `interspersed` method is inside the ne `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 - } -} + @usableFromInline + internal enum Separator { + case element(Element) + case syncClosure(@Sendable () -> Element) + case asyncClosure(@Sendable () async -> Element) + } -extension AsyncInterspersedSequence: AsyncSequence { - public typealias Element = Base.Element + @usableFromInline + internal let base: Base - /// The iterator for an `AsyncInterspersedSequence` asynchronous sequence. - public struct AsyncIterator: AsyncIteratorProtocol { @usableFromInline - internal enum State { - case start - case element(Result) - case separator - } + internal let separator: Separator @usableFromInline - internal var iterator: Base.AsyncIterator + internal let every: Int @usableFromInline - internal let separator: Base.Element + internal init(_ base: Base, every: Int, separator: Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .element(separator) + self.every = every + } @usableFromInline - internal var state = State.start + internal init(_ base: Base, every: Int, separator: @Sendable @escaping () -> Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .syncClosure(separator) + self.every = every + } @usableFromInline - internal init(_ iterator: Base.AsyncIterator, separator: Base.Element) { - self.iterator = iterator - self.separator = separator + internal init(_ base: Base, every: Int, separator: @Sendable @escaping () async -> Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .asyncClosure(separator) + self.every = every } +} - 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() - } +extension AsyncInterspersedSequence: AsyncSequence { + public typealias Element = Base.Element + + /// The iterator for an `AsyncInterspersedSequence` asynchronous sequence. + public struct Iterator: AsyncIteratorProtocol { + @usableFromInline + internal enum State { + case start(Element?) + case element(Int) + case separator + case finished + } + + @usableFromInline + internal var iterator: Base.AsyncIterator + + @usableFromInline + internal let separator: Separator + + @usableFromInline + internal let every: Int + + @usableFromInline + internal var state = State.start(nil) + + @usableFromInline + internal init(_ iterator: Base.AsyncIterator, every: Int, separator: Separator) { + self.iterator = iterator + self.separator = separator + self.every = every + } + + 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 var .start(element): + do { + if element == nil { + element = try await self.iterator.next() + } + + if let element = element { + if every == 1 { + state = .separator + } else { + state = .element(1) + } + return element + } else { + state = .finished + return nil + } + } catch { + state = .finished + throw error + } + + case .separator: + do { + if let element = try await iterator.next() { + state = .start(element) + switch separator { + case let .element(element): + return element + + case let .syncClosure(closure): + return closure() + + case let .asyncClosure(closure): + return await closure() + } + } else { + state = .finished + return nil + } + } catch { + state = .finished + throw error + } + + case let .element(count): + do { + if let element = try await iterator.next() { + let newCount = count + 1 + if every == newCount { + state = .separator + } else { + state = .element(newCount) + } + return element + } else { + state = .finished + return nil + } + } catch { + state = .finished + throw error + } + + case .finished: + return nil + } + } } - } - @inlinable - public func makeAsyncIterator() -> AsyncInterspersedSequence.AsyncIterator { - AsyncIterator(base.makeAsyncIterator(), separator: separator) - } + @inlinable + public func makeAsyncIterator() -> AsyncInterspersedSequence.Iterator { + Iterator(base.makeAsyncIterator(), every: every, separator: separator) + } } ``` diff --git a/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift b/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift index 18398f9f..a5e67742 100644 --- a/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift +++ b/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift @@ -9,108 +9,245 @@ // //===----------------------------------------------------------------------===// -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) - } +public 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" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: The value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + func interspersed(every: Int = 1, with separator: Element) -> AsyncInterspersedSequence { + AsyncInterspersedSequence(self, every: every, separator: separator) + } + + /// 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" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + func interspersed(every: Int = 1, with separator: @Sendable @escaping () -> Element) -> AsyncInterspersedSequence { + AsyncInterspersedSequence(self, every: every, separator: separator) + } + + /// 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" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + func interspersed(every: Int = 1, with separator: @Sendable @escaping () async -> Element) -> AsyncInterspersedSequence { + AsyncInterspersedSequence(self, every: every, separator: separator) + } } /// 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 - init(_ base: Base, separator: Base.Element) { - self.base = base - self.separator = separator - } -} + @usableFromInline + internal enum Separator { + case element(Element) + case syncClosure(@Sendable () -> Element) + case asyncClosure(@Sendable () async -> Element) + } -extension AsyncInterspersedSequence: AsyncSequence { - public typealias Element = Base.Element + @usableFromInline + internal let base: Base - /// The iterator for an `AsyncInterspersedSequence` asynchronous sequence. - public struct Iterator: AsyncIteratorProtocol { @usableFromInline - internal enum State { - case start - case element(Result) - case separator - } + internal let separator: Separator @usableFromInline - internal var iterator: Base.AsyncIterator + internal let every: Int @usableFromInline - internal let separator: Base.Element + internal init(_ base: Base, every: Int, separator: Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .element(separator) + self.every = every + } @usableFromInline - internal var state = State.start + internal init(_ base: Base, every: Int, separator: @Sendable @escaping () -> Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .syncClosure(separator) + self.every = every + } @usableFromInline - init(_ iterator: Base.AsyncIterator, separator: Base.Element) { - self.iterator = iterator - self.separator = separator + internal init(_ base: Base, every: Int, separator: @Sendable @escaping () async -> Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .asyncClosure(separator) + self.every = every } +} - 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() - } +extension AsyncInterspersedSequence: AsyncSequence { + public typealias Element = Base.Element + + /// The iterator for an `AsyncInterspersedSequence` asynchronous sequence. + public struct Iterator: AsyncIteratorProtocol { + @usableFromInline + internal enum State { + case start(Element?) + case element(Int) + case separator + case finished + } + + @usableFromInline + internal var iterator: Base.AsyncIterator + + @usableFromInline + internal let separator: Separator + + @usableFromInline + internal let every: Int + + @usableFromInline + internal var state = State.start(nil) + + @usableFromInline + internal init(_ iterator: Base.AsyncIterator, every: Int, separator: Separator) { + self.iterator = iterator + self.separator = separator + self.every = every + } + + 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 var .start(element): + do { + if element == nil { + element = try await self.iterator.next() + } + + if let element = element { + if every == 1 { + state = .separator + } else { + state = .element(1) + } + return element + } else { + state = .finished + return nil + } + } catch { + state = .finished + throw error + } + + case .separator: + do { + if let element = try await iterator.next() { + state = .start(element) + switch separator { + case let .element(element): + return element + + case let .syncClosure(closure): + return closure() + + case let .asyncClosure(closure): + return await closure() + } + } else { + state = .finished + return nil + } + } catch { + state = .finished + throw error + } + + case let .element(count): + do { + if let element = try await iterator.next() { + let newCount = count + 1 + if every == newCount { + state = .separator + } else { + state = .element(newCount) + } + return element + } else { + state = .finished + return nil + } + } catch { + state = .finished + throw error + } + + case .finished: + return nil + } + } } - } - @inlinable - public func makeAsyncIterator() -> AsyncInterspersedSequence.Iterator { - Iterator(base.makeAsyncIterator(), separator: separator) - } + @inlinable + public func makeAsyncIterator() -> AsyncInterspersedSequence.Iterator { + Iterator(base.makeAsyncIterator(), every: every, separator: separator) + } } -extension AsyncInterspersedSequence: Sendable where Base: Sendable, Base.Element: Sendable { } +extension AsyncInterspersedSequence: Sendable where Base: Sendable, Base.Element: Sendable {} +extension AsyncInterspersedSequence.Separator: Sendable where Base: Sendable, Base.Element: Sendable {} @available(*, unavailable) extension AsyncInterspersedSequence.Iterator: Sendable {} diff --git a/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift b/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift index 2f351ab0..9f26146f 100644 --- a/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift +++ b/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift @@ -9,90 +9,135 @@ // //===----------------------------------------------------------------------===// -import XCTest import AsyncAlgorithms +import XCTest final class TestInterspersed: XCTestCase { - func test_interspersed() async { - let source = [1, 2, 3, 4, 5] - let expected = [1, 0, 2, 0, 3, 0, 4, 0, 5] - let sequence = source.async.interspersed(with: 0) - var actual = [Int]() - var iterator = sequence.makeAsyncIterator() - while let item = await iterator.next() { - actual.append(item) + func test_interspersed() async { + let source = [1, 2, 3, 4, 5] + let expected = [1, 0, 2, 0, 3, 0, 4, 0, 5] + let sequence = source.async.interspersed(with: 0) + var actual = [Int]() + var iterator = sequence.makeAsyncIterator() + while let item = await iterator.next() { + actual.append(item) + } + let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) } - let pastEnd = await iterator.next() - XCTAssertNil(pastEnd) - XCTAssertEqual(actual, expected) - } - - func test_interspersed_empty() async { - let source = [Int]() - let expected = [Int]() - let sequence = source.async.interspersed(with: 0) - var actual = [Int]() - var iterator = sequence.makeAsyncIterator() - while let item = await iterator.next() { - actual.append(item) + + func test_interspersed_every() async { + let source = [1, 2, 3, 4, 5, 6, 7, 8] + let expected = [1, 2, 3, 0, 4, 5, 6, 0, 7, 8] + let sequence = source.async.interspersed(every: 3, with: 0) + var actual = [Int]() + var iterator = sequence.makeAsyncIterator() + while let item = await iterator.next() { + actual.append(item) + } + let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) } - let pastEnd = await iterator.next() - XCTAssertNil(pastEnd) - XCTAssertEqual(actual, expected) - } - - func test_interspersed_with_throwing_upstream() async { - let source = [1, 2, 3, -1, 4, 5] - let expected = [1, 0, 2, 0, 3, 0] - var actual = [Int]() - let sequence = source.async.map { - try throwOn(-1, $0) - }.interspersed(with: 0) - - var iterator = sequence.makeAsyncIterator() - do { - while let item = try await iterator.next() { - actual.append(item) - } - XCTFail() - } catch { - XCTAssertEqual(Failure(), error as? Failure) + + func test_interspersed_closure() async { + let source = [1, 2, 3, 4, 5] + let expected = [1, 0, 2, 0, 3, 0, 4, 0, 5] + let sequence = source.async.interspersed(with: { 0 }) + var actual = [Int]() + var iterator = sequence.makeAsyncIterator() + while let item = await iterator.next() { + actual.append(item) + } + let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) } - let pastEnd = try! await iterator.next() - XCTAssertNil(pastEnd) - XCTAssertEqual(actual, expected) - } - - func test_cancellation() async { - let source = Indefinite(value: "test") - let sequence = source.async.interspersed(with: "sep") - let lockStepChannel = AsyncChannel() - - await withTaskGroup(of: Void.self) { group in - group.addTask { + + func test_interspersed_async_closure() async { + let source = [1, 2, 3, 4, 5] + let expected = [1, 0, 2, 0, 3, 0, 4, 0, 5] + let sequence = source.async.interspersed { + try! await Task.sleep(nanoseconds: 1000) + return 0 + } + var actual = [Int]() var iterator = sequence.makeAsyncIterator() - let _ = await iterator.next() + while let item = await iterator.next() { + actual.append(item) + } + let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) + } - // Information the parent task that we are consuming - await lockStepChannel.send(()) + func test_interspersed_empty() async { + let source = [Int]() + let expected = [Int]() + let sequence = source.async.interspersed(with: 0) + var actual = [Int]() + var iterator = sequence.makeAsyncIterator() + while let item = await iterator.next() { + actual.append(item) + } + let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) + } - while let _ = await iterator.next() { } + func test_interspersed_with_throwing_upstream() async { + let source = [1, 2, 3, -1, 4, 5] + let expected = [1, 0, 2, 0, 3] + var actual = [Int]() + let sequence = source.async.map { + try throwOn(-1, $0) + }.interspersed(with: 0) - let pastEnd = await iterator.next() + var iterator = sequence.makeAsyncIterator() + do { + while let item = try await iterator.next() { + actual.append(item) + } + XCTFail() + } catch { + XCTAssertEqual(Failure(), error as? Failure) + } + let pastEnd = try! await iterator.next() XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) + } + + func test_cancellation() async { + let source = Indefinite(value: "test") + let sequence = source.async.interspersed(with: "sep") + let lockStepChannel = AsyncChannel() + + await withTaskGroup(of: Void.self) { group in + group.addTask { + var iterator = sequence.makeAsyncIterator() + let _ = await iterator.next() + + // Information the parent task that we are consuming + await lockStepChannel.send(()) + + while let _ = await iterator.next() {} + + let pastEnd = await iterator.next() + XCTAssertNil(pastEnd) - // Information the parent task that we finished consuming - await lockStepChannel.send(()) - } + // Information the parent task that we finished consuming + await lockStepChannel.send(()) + } - // Waiting until the child task started consuming - _ = await lockStepChannel.first { _ in true } + // Waiting until the child task started consuming + _ = await lockStepChannel.first { _ in true } - // Now we cancel the child - group.cancelAll() + // Now we cancel the child + group.cancelAll() - // Waiting until the child task finished consuming - _ = await lockStepChannel.first { _ in true } + // Waiting until the child task finished consuming + _ = await lockStepChannel.first { _ in true } + } } - } } From 9f4b76831671c7584e5051e5abc8205c72329696 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Thu, 1 Jun 2023 18:18:23 +0100 Subject: [PATCH 2/3] Add AsyncThrowingInterspersedSequence --- Evolution/0011-interspersed.md | 50 ++++ .../AsyncInterspersedSequence.swift | 255 +++++++++++++++--- .../Interspersed/TestInterspersed.swift | 43 +++ 3 files changed, 316 insertions(+), 32 deletions(-) diff --git a/Evolution/0011-interspersed.md b/Evolution/0011-interspersed.md index 31956236..cfc99737 100644 --- a/Evolution/0011-interspersed.md +++ b/Evolution/0011-interspersed.md @@ -95,6 +95,56 @@ public extension AsyncSequence { func interspersed(every: Int = 1, with separator: @Sendable @escaping () async -> Element) -> AsyncInterspersedSequence { AsyncInterspersedSequence(self, every: every, separator: separator) } + + /// 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" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + public func interspersed(every: Int = 1, with separator: @Sendable @escaping () throws -> Element) -> AsyncThrowingInterspersedSequence { + AsyncThrowingInterspersedSequence(self, every: every, separator: separator) + } + + /// 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" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + public func interspersed(every: Int = 1, with separator: @Sendable @escaping () async throws -> Element) -> AsyncThrowingInterspersedSequence { + AsyncThrowingInterspersedSequence(self, every: every, separator: separator) + } } ``` diff --git a/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift b/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift index a5e67742..b8e1cda9 100644 --- a/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift +++ b/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift @@ -9,7 +9,7 @@ // //===----------------------------------------------------------------------===// -public extension AsyncSequence { +extension AsyncSequence { /// Returns a new asynchronous sequence containing the elements of this asynchronous sequence, inserting /// the given separator between each element. /// @@ -31,7 +31,7 @@ public extension AsyncSequence { /// - separator: The value to insert in between each of this async sequence’s elements. /// - Returns: The interspersed asynchronous sequence of elements. @inlinable - func interspersed(every: Int = 1, with separator: Element) -> AsyncInterspersedSequence { + public func interspersed(every: Int = 1, with separator: Element) -> AsyncInterspersedSequence { AsyncInterspersedSequence(self, every: every, separator: separator) } @@ -56,7 +56,7 @@ public extension AsyncSequence { /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. /// - Returns: The interspersed asynchronous sequence of elements. @inlinable - func interspersed(every: Int = 1, with separator: @Sendable @escaping () -> Element) -> AsyncInterspersedSequence { + public func interspersed(every: Int = 1, with separator: @Sendable @escaping () -> Element) -> AsyncInterspersedSequence { AsyncInterspersedSequence(self, every: every, separator: separator) } @@ -81,9 +81,59 @@ public extension AsyncSequence { /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. /// - Returns: The interspersed asynchronous sequence of elements. @inlinable - func interspersed(every: Int = 1, with separator: @Sendable @escaping () async -> Element) -> AsyncInterspersedSequence { + public func interspersed(every: Int = 1, with separator: @Sendable @escaping () async -> Element) -> AsyncInterspersedSequence { AsyncInterspersedSequence(self, every: every, separator: separator) } + + /// 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" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + public func interspersed(every: Int = 1, with separator: @Sendable @escaping () throws -> Element) -> AsyncThrowingInterspersedSequence { + AsyncThrowingInterspersedSequence(self, every: every, separator: separator) + } + + /// 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" + /// ``` + /// + /// - Parameters: + /// - every: Dictates after how many elements a separator should be inserted. + /// - separator: A closure that produces the value to insert in between each of this async sequence’s elements. + /// - Returns: The interspersed asynchronous sequence of elements. + @inlinable + public func interspersed(every: Int = 1, with separator: @Sendable @escaping () async throws -> Element) -> AsyncThrowingInterspersedSequence { + AsyncThrowingInterspersedSequence(self, every: every, separator: separator) + } } /// An asynchronous sequence that presents the elements of a base asynchronous sequence of @@ -163,74 +213,210 @@ extension AsyncInterspersedSequence: AsyncSequence { } 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 var .start(element): + switch self.state { + case .start(var element): do { if element == nil { element = try await self.iterator.next() } if let element = element { - if every == 1 { - state = .separator + if self.every == 1 { + self.state = .separator } else { - state = .element(1) + self.state = .element(1) } return element } else { - state = .finished + self.state = .finished return nil } } catch { - state = .finished + self.state = .finished throw error } case .separator: do { if let element = try await iterator.next() { - state = .start(element) - switch separator { - case let .element(element): + self.state = .start(element) + switch self.separator { + case .element(let element): return element - case let .syncClosure(closure): + case .syncClosure(let closure): return closure() - case let .asyncClosure(closure): + case .asyncClosure(let closure): return await closure() } } else { - state = .finished + self.state = .finished + return nil + } + } catch { + self.state = .finished + throw error + } + + case .element(let count): + do { + if let element = try await iterator.next() { + let newCount = count + 1 + if self.every == newCount { + self.state = .separator + } else { + self.state = .element(newCount) + } + return element + } else { + self.state = .finished + return nil + } + } catch { + self.state = .finished + throw error + } + + case .finished: + return nil + } + } + } + + @inlinable + public func makeAsyncIterator() -> Iterator { + Iterator(self.base.makeAsyncIterator(), every: self.every, separator: self.separator) + } +} + +/// An asynchronous sequence that presents the elements of a base asynchronous sequence of +/// elements with a separator between each of those elements. +public struct AsyncThrowingInterspersedSequence { + @usableFromInline + internal enum Separator { + case syncClosure(@Sendable () throws -> Element) + case asyncClosure(@Sendable () async throws -> Element) + } + + @usableFromInline + internal let base: Base + + @usableFromInline + internal let separator: Separator + + @usableFromInline + internal let every: Int + + @usableFromInline + internal init(_ base: Base, every: Int, separator: @Sendable @escaping () throws -> Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .syncClosure(separator) + self.every = every + } + + @usableFromInline + internal init(_ base: Base, every: Int, separator: @Sendable @escaping () async throws -> Element) { + precondition(every > 0, "Separators can only be interspersed ever 1+ elements") + self.base = base + self.separator = .asyncClosure(separator) + self.every = every + } +} + +extension AsyncThrowingInterspersedSequence: AsyncSequence { + public typealias Element = Base.Element + + /// The iterator for an `AsyncInterspersedSequence` asynchronous sequence. + public struct Iterator: AsyncIteratorProtocol { + @usableFromInline + internal enum State { + case start(Element?) + case element(Int) + case separator + case finished + } + + @usableFromInline + internal var iterator: Base.AsyncIterator + + @usableFromInline + internal let separator: Separator + + @usableFromInline + internal let every: Int + + @usableFromInline + internal var state = State.start(nil) + + @usableFromInline + internal init(_ iterator: Base.AsyncIterator, every: Int, separator: Separator) { + self.iterator = iterator + self.separator = separator + self.every = every + } + + public mutating func next() async throws -> Base.Element? { + switch self.state { + case .start(var element): + do { + if element == nil { + element = try await self.iterator.next() + } + + if let element = element { + if self.every == 1 { + self.state = .separator + } else { + self.state = .element(1) + } + return element + } else { + self.state = .finished + return nil + } + } catch { + self.state = .finished + throw error + } + + case .separator: + do { + if let element = try await iterator.next() { + self.state = .start(element) + switch self.separator { + case .syncClosure(let closure): + return try closure() + + case .asyncClosure(let closure): + return try await closure() + } + } else { + self.state = .finished return nil } } catch { - state = .finished + self.state = .finished throw error } - case let .element(count): + case .element(let count): do { if let element = try await iterator.next() { let newCount = count + 1 - if every == newCount { - state = .separator + if self.every == newCount { + self.state = .separator } else { - state = .element(newCount) + self.state = .element(newCount) } return element } else { - state = .finished + self.state = .finished return nil } } catch { - state = .finished + self.state = .finished throw error } @@ -241,13 +427,18 @@ extension AsyncInterspersedSequence: AsyncSequence { } @inlinable - public func makeAsyncIterator() -> AsyncInterspersedSequence.Iterator { - Iterator(base.makeAsyncIterator(), every: every, separator: separator) + public func makeAsyncIterator() -> Iterator { + Iterator(self.base.makeAsyncIterator(), every: self.every, separator: self.separator) } } extension AsyncInterspersedSequence: Sendable where Base: Sendable, Base.Element: Sendable {} extension AsyncInterspersedSequence.Separator: Sendable where Base: Sendable, Base.Element: Sendable {} +extension AsyncThrowingInterspersedSequence: Sendable where Base: Sendable, Base.Element: Sendable {} +extension AsyncThrowingInterspersedSequence.Separator: Sendable where Base: Sendable, Base.Element: Sendable {} + @available(*, unavailable) extension AsyncInterspersedSequence.Iterator: Sendable {} +@available(*, unavailable) +extension AsyncThrowingInterspersedSequence.Iterator: Sendable {} diff --git a/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift b/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift index 9f26146f..6e09e84d 100644 --- a/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift +++ b/Tests/AsyncAlgorithmsTests/Interspersed/TestInterspersed.swift @@ -72,6 +72,49 @@ final class TestInterspersed: XCTestCase { XCTAssertEqual(actual, expected) } + func test_interspersed_throwing_closure() async { + let source = [1, 2] + let expected = [1] + var actual = [Int]() + let sequence = source.async.interspersed(with: { throw Failure() }) + + var iterator = sequence.makeAsyncIterator() + do { + while let item = try await iterator.next() { + actual.append(item) + } + XCTFail() + } catch { + XCTAssertEqual(Failure(), error as? Failure) + } + let pastEnd = try! await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) + } + + func test_interspersed_async_throwing_closure() async { + let source = [1, 2] + let expected = [1] + var actual = [Int]() + let sequence = source.async.interspersed { + try await Task.sleep(nanoseconds: 1000) + throw Failure() + } + + var iterator = sequence.makeAsyncIterator() + do { + while let item = try await iterator.next() { + actual.append(item) + } + XCTFail() + } catch { + XCTAssertEqual(Failure(), error as? Failure) + } + let pastEnd = try! await iterator.next() + XCTAssertNil(pastEnd) + XCTAssertEqual(actual, expected) + } + func test_interspersed_empty() async { let source = [Int]() let expected = [Int]() From 974758a806390d91660c6c93300178148ded25d6 Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Fri, 2 Jun 2023 16:50:50 +0100 Subject: [PATCH 3/3] Update examples --- .../Interspersed/AsyncInterspersedSequence.swift | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift b/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift index b8e1cda9..9932e77e 100644 --- a/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift +++ b/Sources/AsyncAlgorithms/Interspersed/AsyncInterspersedSequence.swift @@ -44,7 +44,7 @@ extension AsyncSequence { /// /// ``` /// let input = ["A", "B", "C"].async - /// let interspersed = input.interspersed(with: "-") + /// let interspersed = input.interspersed(with: { "-" }) /// for await element in interspersed { /// print(element) /// } @@ -69,7 +69,7 @@ extension AsyncSequence { /// /// ``` /// let input = ["A", "B", "C"].async - /// let interspersed = input.interspersed(with: "-") + /// let interspersed = input.interspersed(with: { "-" }) /// for await element in interspersed { /// print(element) /// } @@ -94,7 +94,7 @@ extension AsyncSequence { /// /// ``` /// let input = ["A", "B", "C"].async - /// let interspersed = input.interspersed(with: "-") + /// let interspersed = input.interspersed(with: { "-" }) /// for await element in interspersed { /// print(element) /// } @@ -119,7 +119,7 @@ extension AsyncSequence { /// /// ``` /// let input = ["A", "B", "C"].async - /// let interspersed = input.interspersed(with: "-") + /// let interspersed = input.interspersed(with: { "-" }) /// for await element in interspersed { /// print(element) /// }