Skip to content

[AsyncInterspersedSequence] Integrate review feedback #267

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Jun 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
348 changes: 266 additions & 82 deletions Evolution/0011-interspersed.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,33 +17,134 @@ 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<Self> {
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<Self> {
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<Self> {
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<Self> {
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<Self> {
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<Self> {
AsyncThrowingInterspersedSequence(self, every: every, separator: separator)
}
}
```

Expand All @@ -53,83 +154,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<Base: AsyncSequence> {
@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<Base.Element, Error>)
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
}
}

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)

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()
}
@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<Base>.AsyncIterator {
AsyncIterator(base.makeAsyncIterator(), separator: separator)
}
@inlinable
public func makeAsyncIterator() -> AsyncInterspersedSequence<Base>.Iterator {
Iterator(base.makeAsyncIterator(), every: every, separator: separator)
}
}
```
Loading