Skip to content

Commit

Permalink
[AsyncInterspersedSequence] Integrate review feedback (#267)
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
FranzBusch authored Jun 23, 2023
1 parent 9274790 commit 936a68d
Show file tree
Hide file tree
Showing 3 changed files with 829 additions and 229 deletions.
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

0 comments on commit 936a68d

Please sign in to comment.