Skip to content

Commit a059255

Browse files
committed
Adjacent Pairs
1 parent f9369af commit a059255

File tree

5 files changed

+198
-0
lines changed

5 files changed

+198
-0
lines changed

Guides/AdjacentPairs.md

+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
# AdjacentPairs
2+
3+
* Author(s): [László Teveli](https://github.com/tevelee)
4+
5+
[[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/AdjacentPairsSequence.swift) |
6+
[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestAdjacentPairs.swift)]
7+
8+
The `adjacentPairs()` API serve the purpose of collecting adjacent values. This operation is available for any `AsyncSequence` by calling the `adjacentPairs()` method.
9+
10+
```swift
11+
extension AsyncSequence {
12+
public func adjacentPairs() -> AsyncAdjacentPairsSequence<Self>
13+
}
14+
```
15+
16+
## Detailed Design
17+
18+
The `adjacentPairs()` algorithm produces elements of tuple (size of 2), containing a pair of the original `Element` type.
19+
20+
The interface for this algorithm is available on all `AsyncSequence` types. The returned `AsyncAdjacentPairsSequence` conditionally conforms to `Sendable`.
21+
22+
Its iterator keeps track of the previous element returned in the `next()` function and updates it in every turn.
23+
24+
```swift
25+
for await (first, second) in (1...5).async.adjacentPairs() {
26+
print("First: \(first), Second: \(second)")
27+
}
28+
29+
// First: 1, Second: 2
30+
// First: 2, Second: 3
31+
// First: 3, Second: 4
32+
// First: 4, Second: 5
33+
```
34+
35+
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.
36+
37+
```swift
38+
Dictionary(uniqueKeysWithValues: url.lines.adjacentPairs())
39+
```
40+
41+
## Alternatives Considered
42+
43+
This functionality is often written as a `zip` of a sequence together with itself, dropping its first element (`zip(source, source.dropFirst())`).
44+
45+
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.
46+
47+
## Credits/Inspiration
48+
49+
The synchronous counterpart in [swift-algorithms](https://github.com/apple/swift-algorithms/blob/main/Guides/AdjacentPairs.md).

Guides/Effects.md

+1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
| Type | Throws | Sendablity |
22
|-----------------------------------------------------|--------------|-------------|
3+
| `AdjacentPairsSequence` | rethrows | Conditional |
34
| `AsyncBufferedByteIterator` | throws | Sendable |
45
| `AsyncBufferSequence` | rethrows | Conditional |
56
| `AsyncBufferSequence.Iterator` | rethrows | Conditional |

README.md

+1
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ This package is the home for these APIs. Development and API design take place o
3737
- [`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.
3838

3939
#### Other useful asynchronous sequences
40+
- [`adjacentPairs()`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/AdjacentPairs.md): Collects tuples of adjacent elements.
4041
- [`chunks(...)` and `chunked(...)`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Chunked.md): Collect values into chunks.
4142
- [`compacted()`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/Compacted.md): Remove nil values from an asynchronous sequence.
4243
- [`removeDuplicates()`](https://github.com/apple/swift-async-algorithms/blob/main/Guides/RemoveDuplicates.md): Remove sequentially adjacent duplicate values.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift Async Algorithms open source project
4+
//
5+
// Copyright (c) 2022 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
/// An `AsyncSequence` that iterates over the adjacent pairs of the original
13+
/// `AsyncSequence`.
14+
@frozen
15+
public struct AdjacentPairsSequence<Base: AsyncSequence>: AsyncSequence {
16+
public typealias Element = (Base.Element, Base.Element)
17+
18+
@usableFromInline
19+
let base: Base
20+
21+
@inlinable
22+
init(_ base: Base) {
23+
self.base = base
24+
}
25+
26+
/// The iterator for an `AdjacentPairsSequence` instance.
27+
@frozen
28+
public struct Iterator: AsyncIteratorProtocol {
29+
public typealias Element = (Base.Element, Base.Element)
30+
31+
@usableFromInline
32+
var base: Base.AsyncIterator
33+
34+
@usableFromInline
35+
internal var previousElement: Base.Element?
36+
37+
@inlinable
38+
init(_ base: Base.AsyncIterator) {
39+
self.base = base
40+
}
41+
42+
@inlinable
43+
public mutating func next() async rethrows -> (Base.Element, Base.Element)? {
44+
if previousElement == nil {
45+
previousElement = try await base.next()
46+
}
47+
48+
guard let previous = previousElement, let next = try await base.next() else {
49+
return nil
50+
}
51+
52+
previousElement = next
53+
return (previous, next)
54+
}
55+
}
56+
57+
@inlinable
58+
public func makeAsyncIterator() -> Iterator {
59+
Iterator(base.makeAsyncIterator())
60+
}
61+
}
62+
63+
extension AsyncSequence {
64+
/// An `AsyncSequence` that iterates over the adjacent pairs of the original
65+
/// original `AsyncSequence`.
66+
///
67+
/// ```
68+
/// for await (first, second) in (1...5).async.adjacentPairs() {
69+
/// print("First: \(first), Second: \(second)")
70+
/// }
71+
///
72+
/// // First: 1, Second: 2
73+
/// // First: 2, Second: 3
74+
/// // First: 3, Second: 4
75+
/// // First: 4, Second: 5
76+
/// ```
77+
///
78+
/// - Returns: An `AsyncSequence` where the element is a tuple of two adjacent elements
79+
/// or the original `AsyncSequence`.
80+
@inlinable
81+
public func adjacentPairs() -> AdjacentPairsSequence<Self> {
82+
AdjacentPairsSequence(self)
83+
}
84+
}
85+
86+
extension AdjacentPairsSequence: Sendable where Base: Sendable, Base.Element: Sendable, Base.AsyncIterator: Sendable { }
87+
extension AdjacentPairsSequence.Iterator: Sendable where Base: Sendable, Base.Element: Sendable, Base.AsyncIterator: Sendable { }
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//===----------------------------------------------------------------------===//
2+
//
3+
// This source file is part of the Swift Async Algorithms open source project
4+
//
5+
// Copyright (c) 2022 Apple Inc. and the Swift project authors
6+
// Licensed under Apache License v2.0 with Runtime Library Exception
7+
//
8+
// See https://swift.org/LICENSE.txt for license information
9+
//
10+
//===----------------------------------------------------------------------===//
11+
12+
@preconcurrency import XCTest
13+
import AsyncAlgorithms
14+
15+
final class TestAdjacentPairs: XCTestCase {
16+
func test_adjacentPairs() async {
17+
let source = 1...5
18+
let expected = [(1,2), (2,3), (3,4), (4,5)]
19+
let sequence = source.async.adjacentPairs()
20+
var actual: [(Int, Int)] = []
21+
for await item in sequence {
22+
actual.append(item)
23+
}
24+
XCTAssertEqual(expected, actual)
25+
}
26+
27+
func test_empty() async {
28+
let source = 0..<1
29+
let expected: [(Int, Int)] = []
30+
let sequence = source.async.adjacentPairs()
31+
var actual: [(Int, Int)] = []
32+
for await item in sequence {
33+
actual.append(item)
34+
}
35+
XCTAssertEqual(expected, actual)
36+
}
37+
38+
func test_cancellation() async {
39+
let source = Indefinite(value: 0)
40+
let sequence = source.async.adjacentPairs()
41+
let finished = expectation(description: "finished")
42+
let iterated = expectation(description: "iterated")
43+
let task = Task {
44+
var firstIteration = false
45+
for await _ in sequence {
46+
if !firstIteration {
47+
firstIteration = true
48+
iterated.fulfill()
49+
}
50+
}
51+
finished.fulfill()
52+
}
53+
// ensure the other task actually starts
54+
wait(for: [iterated], timeout: 1.0)
55+
// cancellation should ensure the loop finishes
56+
// without regards to the remaining underlying sequence
57+
task.cancel()
58+
wait(for: [finished], timeout: 1.0)
59+
}
60+
}

0 commit comments

Comments
 (0)