Skip to content

Add Interspersed proposal #259

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 2 commits into from
May 10, 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
10 changes: 4 additions & 6 deletions Evolution/0001-zip.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -45,8 +45,7 @@ public func zip<Base1: AsyncSequence, Base2: AsyncSequence, Base3: AsyncSequence
public struct AsyncZip2Sequence<Base1: AsyncSequence, Base2: AsyncSequence>: 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 {
Expand All @@ -59,8 +58,7 @@ public struct AsyncZip2Sequence<Base1: AsyncSequence, Base2: AsyncSequence>: Sen
public struct AsyncZip3Sequence<Base1: AsyncSequence, Base2: AsyncSequence, Base3: AsyncSequence>: 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 {
Expand Down
10 changes: 4 additions & 6 deletions Evolution/0002-merge.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -46,8 +46,7 @@ public struct AsyncMerge2Sequence<Base1: AsyncSequence, Base2: AsyncSequence>: 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 {
Expand All @@ -61,8 +60,7 @@ public struct AsyncMerge3Sequence<Base1: AsyncSequence, Base2: AsyncSequence, Ba
where
Base1.Element == Base2.Element, Base1.Element == Base3.Element,
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

public struct Iterator: AsyncIteratorProtocol {
Expand Down
4 changes: 2 additions & 2 deletions Evolution/0003-compacted.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Compacted

* Proposal: [0003](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0003-compacted.md)
* Proposal: [SAA-0003](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0003-compacted.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/AsyncCompactedSequence.swift)
[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestCompacted.swift)
Expand Down
2 changes: 1 addition & 1 deletion Evolution/0004-joined.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Proposal: [SAA-0004](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0004-joined.md)
* Authors: [Philippe Hausler](https://github.com/phausler)
* Review Manager: [Franz Busch](https://github.com/FranzBusch)
* Status: **Implemented**
* Status: **Accepted**

* Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncJoinedSequence.swift) |
[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestJoin.swift)]
Expand Down
2 changes: 1 addition & 1 deletion Evolution/0005-adjacent-pairs.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
* Proposal: [SAA-0005](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0005-adjacent-pairs.md)
* Author(s): [László Teveli](https://github.com/tevelee)
* Review Manager: [Philippe Hausler](https://github.com/phausler)
* Status: **Implemented**
* Status: **Accepted**
* Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncAdjacentPairsSequence.swift) |
[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift)]
* Decision Notes:
Expand Down
10 changes: 4 additions & 6 deletions Evolution/0006-combineLatest.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,10 @@

* Proposal: [SAA-0006](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0006-combineLatest.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/AsyncCombineLatest2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AsyncCombineLatest3Sequence.swift) |
* Implementation: [[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest2Sequence.swift), [Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/CombineLatest/AsyncCombineLatest3Sequence.swift) |
[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestCombineLatest.swift)]

* Decision Notes:
Expand Down Expand Up @@ -47,8 +47,7 @@ public func combineLatest<Base1: AsyncSequence, Base2: AsyncSequence, Base3: Asy
public struct AsyncCombineLatest2Sequence<Base1: AsyncSequence, Base2: AsyncSequence>: 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 {
Expand All @@ -61,8 +60,7 @@ public struct AsyncCombineLatest2Sequence<Base1: AsyncSequence, Base2: AsyncSequ
public struct AsyncCombineLatest3Sequence<Base1: AsyncSequence, Base2: AsyncSequence, Base3: AsyncSequence>: 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 {
Expand Down
2 changes: 1 addition & 1 deletion Evolution/0007-chain.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)]

Expand Down
4 changes: 2 additions & 2 deletions Evolution/0008-bytes.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)]

Expand Down Expand Up @@ -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(
Expand Down
2 changes: 1 addition & 1 deletion 0009-async.md → Evolution/0009-async.md
Original file line number Diff line number Diff line change
@@ -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**

Expand Down
4 changes: 2 additions & 2 deletions Evolution/0009-buffer.md → Evolution/0010-buffer.md
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
135 changes: 135 additions & 0 deletions Evolution/0011-interspersed.md
Original file line number Diff line number Diff line change
@@ -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<Self> {
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<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
}
}

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<Base.Element, Error>)
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<Base>.AsyncIterator {
AsyncIterator(base.makeAsyncIterator(), separator: separator)
}
}
```
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -95,8 +106,11 @@ extension AsyncInterspersedSequence: AsyncSequence {

@inlinable
public func makeAsyncIterator() -> AsyncInterspersedSequence<Base>.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 {}
Original file line number Diff line number Diff line change
Expand Up @@ -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<Void>()

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)
}
}