Skip to content

Adjacent Pairs #111

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 1 commit into from
Mar 29, 2022
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
49 changes: 49 additions & 0 deletions Guides/AdjacentPairs.md
Original file line number Diff line number Diff line change
@@ -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<Self>
}
```

## 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).
1 change: 1 addition & 0 deletions Guides/Effects.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
| Type | Throws | Sendablity |
|-----------------------------------------------------|--------------|-------------|
| `AsyncAdjacentPairsSequence` | rethrows | Conditional |
| `AsyncBufferedByteIterator` | throws | Sendable |
| `AsyncBufferSequence` | rethrows | Conditional |
| `AsyncBufferSequence.Iterator` | rethrows | Conditional |
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
87 changes: 87 additions & 0 deletions Sources/AsyncAlgorithms/AdjacentPairsSequence.swift
Original file line number Diff line number Diff line change
@@ -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<Base: AsyncSequence>: 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<Self> {
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 { }
60 changes: 60 additions & 0 deletions Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}