Skip to content

Commit 07b8d74

Browse files
authored
[SAA-0002] Merge (apple#156)
1 parent 434591a commit 07b8d74

File tree

1 file changed

+103
-0
lines changed

1 file changed

+103
-0
lines changed

Diff for: Evolution/0002-merge.md

+103
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
# Merge
2+
3+
* Proposal: [SAA-0002](https://github.com/apple/swift-async-algorithms/blob/main/Evolution/0002-merge.md)
4+
* Authors: [Philippe Hausler](https://github.com/phausler)
5+
* Status: **Implemented**
6+
7+
* 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) |
8+
[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestMerge.swift)]
9+
* Decision Notes:
10+
* Bugs:
11+
12+
## Introduction
13+
14+
In the category of combinations of asynchronous sequences there are a few different potential behaviors. This category all take two or more `AsyncSequence` types and produce one `AsyncSequence`. One fundamental behavior is taking all values produced by the inputs and resuming the iteration of the singular downstream `AsyncSequence` with those values. This shape is called merge.
15+
16+
## Detailed Design
17+
18+
Merge takes two or more asynchronous sequences sharing the same element type and combines them into one singular asynchronous sequence of those elements.
19+
20+
```swift
21+
let appleFeed = URL(string: "http://www.example.com/ticker?symbol=AAPL")!.lines.map { "AAPL: " + $0 }
22+
let nasdaqFeed = URL(string:"http://www.example.com/ticker?symbol=^IXIC")!.lines.map { "^IXIC: " + $0 }
23+
24+
for try await ticker in merge(appleFeed, nasdaqFeed) {
25+
print(ticker)
26+
}
27+
```
28+
29+
Given some sample inputs the following merged events can be expected.
30+
31+
| Timestamp | appleFeed | nasdaqFeed | merged output |
32+
| ----------- | --------- | ---------- | --------------- |
33+
| 11:40 AM | 173.91 | | AAPL: 173.91 |
34+
| 12:25 AM | | 14236.78 | ^IXIC: 14236.78 |
35+
| 12:40 AM | | 14218.34 | ^IXIC: 14218.34 |
36+
| 1:15 PM | 173.00 | | AAPL: 173.00 |
37+
38+
This function family and the associated family of return types are prime candidates for variadic generics. Until that proposal is accepted, these will be implemented in terms of two- and three-base sequence cases.
39+
40+
```swift
41+
public func merge<Base1: AsyncSequence, Base2: AsyncSequence>(_ base1: Base1, _ base2: Base2) -> AsyncMerge2Sequence<Base1, Base2>
42+
43+
public func merge<Base1: AsyncSequence, Base2: AsyncSequence, Base3: AsyncSequence>(_ base1: Base1, _ base2: Base2, _ base3: Base3) -> AsyncMerge3Sequence<Base1, Base2, Base3>
44+
45+
public struct AsyncMerge2Sequence<Base1: AsyncSequence, Base2: AsyncSequence>: Sendable
46+
where
47+
Base1.Element == Base2.Element,
48+
Base1: Sendable, Base2: Sendable,
49+
Base1.Element: Sendable, Base2.Element: Sendable,
50+
Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable {
51+
public typealias Element = Base1.Element
52+
53+
public struct Iterator: AsyncIteratorProtocol {
54+
public mutating func next() async rethrows -> Element?
55+
}
56+
57+
public func makeAsyncIterator() -> Iterator
58+
}
59+
60+
public struct AsyncMerge3Sequence<Base1: AsyncSequence, Base2: AsyncSequence, Base3: AsyncSequence>: Sendable
61+
where
62+
Base1.Element == Base2.Element, Base1.Element == Base3.Element,
63+
Base1: Sendable, Base2: Sendable, Base3: Sendable
64+
Base1.Element: Sendable, Base2.Element: Sendable, Base3.Element: Sendable
65+
Base1.AsyncIterator: Sendable, Base2.AsyncIterator: Sendable, Base3.AsyncIterator: Sendable {
66+
public typealias Element = Base1.Element
67+
68+
public struct Iterator: AsyncIteratorProtocol {
69+
public mutating func next() async rethrows -> Element?
70+
}
71+
72+
public func makeAsyncIterator() -> Iterator
73+
}
74+
75+
```
76+
77+
The `merge(_:...)` function takes two or more asynchronous sequences as arguments and produces an `AsyncMergeSequence` which is an asynchronous sequence.
78+
79+
Since the bases comprising the `AsyncMergeSequence` must be iterated concurrently to produce the latest value, those sequences must be able to be sent to child tasks. This means that a prerequisite of the bases must be that the base asynchronous sequences, their iterators, and the elements they produce must be `Sendable`.
80+
81+
When iterating a `AsyncMergeSequence`, the sequence terminates when all of the base asynchronous sequences terminate, since this means there is no potential for any further elements to be produced.
82+
83+
The throwing behavior of `AsyncMergeSequence` is that if any of the bases throw, then the composed asynchronous sequence throws on its iteration. If at any point an error is thrown by any base, the other iterations are cancelled and the thrown error is immediately thrown to the consuming iteration.
84+
85+
### Naming
86+
87+
Since the inherent behavior of `merge(_:...)` merges values from multiple streams into a singular asynchronous sequence, the naming is intended to be quite literal. There are precedent terms of art in other frameworks and libraries (listed in the comparison section). Other naming takes the form of "withLatestFrom". This was disregarded since the "with" prefix is often most associated with the passing of a closure and some sort of contextual concept; `withUnsafePointer` or `withUnsafeContinuation` are prime examples.
88+
89+
### Comparison with other libraries
90+
91+
**ReactiveX** ReactiveX has an [API definition of Merge](https://reactivex.io/documentation/operators/merge.html) as a top level function for merging Observables.
92+
93+
**Combine** Combine has an [API definition of merge(with:)](https://developer.apple.com/documentation/combine/publisher/merge(with:)-7qt71/) as an operator style method for merging Publishers.
94+
95+
## Effect on API resilience
96+
97+
### `@frozen` and `@inlinable`
98+
99+
These types utilize rethrowing mechanisms that are awaiting an implementation in the compiler for supporting implementation based rethrows. So none of them are marked as frozen or marked as inlinable. This feature (discussed as `rethrows(unsafe)` or `rethrows(SourceOfRethrowyness)` has not yet been reviewed or implemented. The current implementation takes liberties with an internal protocol to accomplish this task. Future revisions will remove that protocol trick to replace it with proper rethrows semantics at the actual call site. The types are expected to be stable boundaries to prevent that workaround for the compilers yet to be supported rethrowing (or TaskGroup rethrowing) mechanisms. As soon as that feature is resolved; a more detailed investigation on performance impact of inlining and frozen should be done before 1.0.
100+
101+
## Alternatives considered
102+
103+
It was considered to have merge be shaped as an extension method on `AsyncSequence` however that infers a "primary-ness" of one `AsyncSequence` over another. Since the behavior of this as a global function (which infers no preference to one side or another) it was decided that having symmetry between the asynchronous version and the synchronous version inferred the right connotations.

0 commit comments

Comments
 (0)