diff --git a/Guides/AdjacentPairs.md b/Guides/AdjacentPairs.md new file mode 100644 index 00000000..13765e3b --- /dev/null +++ b/Guides/AdjacentPairs.md @@ -0,0 +1,49 @@ +# AdjacentPairs + +* Author(s): [László Teveli](https://github.com/tevelee) + +[[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)] + +The `adjacentPairs()` API serve the purpose of collecting adjacent values. This operation is available for any `AsyncSequence` by calling the `adjacentPairs()` method. + +```swift +extension AsyncSequence { + public func adjacentPairs() -> AsyncAdjacentPairsSequence +} +``` + +## Detailed Design + +The `adjacentPairs()` algorithm produces elements of tuple (size of 2), containing a pair of the original `Element` type. + +The interface for this algorithm is available on all `AsyncSequence` types. The returned `AsyncAdjacentPairsSequence` conditionally conforms to `Sendable`. + +Its iterator keeps track of the previous element returned in the `next()` function and updates it in every turn. + +```swift +for await (first, second) in (1...5).async.adjacentPairs() { + print("First: \(first), Second: \(second)") +} + +// First: 1, Second: 2 +// First: 2, Second: 3 +// First: 3, Second: 4 +// First: 4, Second: 5 +``` + +It composes well with the [Dictionary.init(_:uniquingKeysWith:)](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Collections.md) API that deals with `AsyncSequence` of tuples. + +```swift +Dictionary(uniqueKeysWithValues: url.lines.adjacentPairs()) +``` + +## Alternatives Considered + +This functionality is often written as a `zip` of a sequence together with itself, dropping its first element (`zip(source, source.dropFirst())`). + +It's such a dominant use-case, the [swift-algorithms](https://github.com/apple/swift-algorithms) package also [introduced](https://github.com/apple/swift-algorithms/pull/119) it to its collection of algorithms. + +## Credits/Inspiration + +The synchronous counterpart in [swift-algorithms](https://github.com/apple/swift-algorithms/blob/main/Guides/AdjacentPairs.md). diff --git a/Guides/Effects.md b/Guides/Effects.md index 09c1b28a..4cf348e1 100644 --- a/Guides/Effects.md +++ b/Guides/Effects.md @@ -1,5 +1,6 @@ | Type | Throws | Sendablity | |-----------------------------------------------------|--------------|-------------| +| `AsyncAdjacentPairsSequence` | rethrows | Conditional | | `AsyncBufferedByteIterator` | throws | Sendable | | `AsyncBufferSequence` | rethrows | Conditional | | `AsyncBufferSequence.Iterator` | rethrows | Conditional | diff --git a/README.md b/README.md index 51344b1b..b9deec09 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,7 @@ This package is the home for these APIs. Development and API design take place o - [`AsyncBufferedByteIterator`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/BufferedBytes.md): A highly efficient iterator useful for iterating byte sequences derived from asynchronous read functions. #### Other useful asynchronous sequences +- [`adjacentPairs()`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/AdjacentPairs.md): Collects tuples of adjacent elements. - [`chunks(...)` and `chunked(...)`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Chunked.md): Collect values into chunks. - [`compacted()`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Compacted.md): Remove nil values from an asynchronous sequence. - [`removeDuplicates()`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/RemoveDuplicates.md): Remove sequentially adjacent duplicate values. diff --git a/Sources/AsyncAlgorithms/AdjacentPairsSequence.swift b/Sources/AsyncAlgorithms/AdjacentPairsSequence.swift new file mode 100644 index 00000000..2186e364 --- /dev/null +++ b/Sources/AsyncAlgorithms/AdjacentPairsSequence.swift @@ -0,0 +1,87 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +/// An `AsyncSequence` that iterates over the adjacent pairs of the original +/// `AsyncSequence`. +@frozen +public struct AsyncAdjacentPairsSequence: AsyncSequence { + public typealias Element = (Base.Element, Base.Element) + + @usableFromInline + let base: Base + + @inlinable + init(_ base: Base) { + self.base = base + } + + /// The iterator for an `AsyncAdjacentPairsSequence` instance. + @frozen + public struct Iterator: AsyncIteratorProtocol { + public typealias Element = (Base.Element, Base.Element) + + @usableFromInline + var base: Base.AsyncIterator + + @usableFromInline + internal var previousElement: Base.Element? + + @inlinable + init(_ base: Base.AsyncIterator) { + self.base = base + } + + @inlinable + public mutating func next() async rethrows -> (Base.Element, Base.Element)? { + if previousElement == nil { + previousElement = try await base.next() + } + + guard let previous = previousElement, let next = try await base.next() else { + return nil + } + + previousElement = next + return (previous, next) + } + } + + @inlinable + public func makeAsyncIterator() -> Iterator { + Iterator(base.makeAsyncIterator()) + } +} + +extension AsyncSequence { + /// An `AsyncSequence` that iterates over the adjacent pairs of the original + /// original `AsyncSequence`. + /// + /// ``` + /// for await (first, second) in (1...5).async.adjacentPairs() { + /// print("First: \(first), Second: \(second)") + /// } + /// + /// // First: 1, Second: 2 + /// // First: 2, Second: 3 + /// // First: 3, Second: 4 + /// // First: 4, Second: 5 + /// ``` + /// + /// - Returns: An `AsyncSequence` where the element is a tuple of two adjacent elements + /// or the original `AsyncSequence`. + @inlinable + public func adjacentPairs() -> AsyncAdjacentPairsSequence { + AsyncAdjacentPairsSequence(self) + } +} + +extension AsyncAdjacentPairsSequence: Sendable where Base: Sendable, Base.Element: Sendable, Base.AsyncIterator: Sendable { } +extension AsyncAdjacentPairsSequence.Iterator: Sendable where Base: Sendable, Base.Element: Sendable, Base.AsyncIterator: Sendable { } diff --git a/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift b/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift new file mode 100644 index 00000000..ade9c8f4 --- /dev/null +++ b/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift @@ -0,0 +1,60 @@ +//===----------------------------------------------------------------------===// +// +// This source file is part of the Swift Async Algorithms open source project +// +// Copyright (c) 2022 Apple Inc. and the Swift project authors +// Licensed under Apache License v2.0 with Runtime Library Exception +// +// See https://swift.org/LICENSE.txt for license information +// +//===----------------------------------------------------------------------===// + +@preconcurrency import XCTest +import AsyncAlgorithms + +final class TestAdjacentPairs: XCTestCase { + func test_adjacentPairs() async { + let source = 1...5 + let expected = [(1,2), (2,3), (3,4), (4,5)] + let sequence = source.async.adjacentPairs() + var actual: [(Int, Int)] = [] + for await item in sequence { + actual.append(item) + } + XCTAssertEqual(expected, actual) + } + + func test_empty() async { + let source = 0..<1 + let expected: [(Int, Int)] = [] + let sequence = source.async.adjacentPairs() + var actual: [(Int, Int)] = [] + for await item in sequence { + actual.append(item) + } + XCTAssertEqual(expected, actual) + } + + func test_cancellation() async { + let source = Indefinite(value: 0) + let sequence = source.async.adjacentPairs() + let finished = expectation(description: "finished") + let iterated = expectation(description: "iterated") + let task = Task { + var firstIteration = false + for await _ in sequence { + if !firstIteration { + firstIteration = true + iterated.fulfill() + } + } + finished.fulfill() + } + // 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) + } +}