Skip to content

Commit a85cf74

Browse files
committed
docs: Add comprehensive guide for flatMapLatest
- Added FlatMapLatest.md documentation guide - Authored by Peter Friese - Includes introduction, code samples, and use cases - Covers search-as-you-type, location-based data, and dynamic config examples - Compares with similar operators in ReactiveX and Combine
1 parent d191e99 commit a85cf74

File tree

1 file changed

+139
-0
lines changed

1 file changed

+139
-0
lines changed
Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,139 @@
1+
# FlatMapLatest
2+
3+
* Author(s): [Peter Friese](https://github.com/peterfriese)
4+
5+
Transforms elements into asynchronous sequences, emitting elements from only the most recent inner sequence.
6+
7+
[[Source](https://github.com/apple/swift-async-algorithms/blob/main/Sources/AsyncAlgorithms/FlatMapLatest/AsyncFlatMapLatestSequence.swift) |
8+
[Tests](https://github.com/apple/swift-async-algorithms/blob/main/Tests/AsyncAlgorithmsTests/TestFlatMapLatest.swift)]
9+
10+
```swift
11+
let searchQuery = AsyncStream<String> { continuation in
12+
// User types into search field
13+
continuation.yield("swi")
14+
try? await Task.sleep(for: .milliseconds(100))
15+
continuation.yield("swift")
16+
try? await Task.sleep(for: .milliseconds(100))
17+
continuation.yield("swift async")
18+
continuation.finish()
19+
}
20+
21+
let searchResults = searchQuery.flatMapLatest { query in
22+
performSearch(query) // Returns AsyncSequence<SearchResult>
23+
}
24+
25+
for try await result in searchResults {
26+
print(result) // Only shows results from "swift async"
27+
}
28+
```
29+
30+
## Introduction
31+
32+
When transforming elements of an asynchronous sequence into new asynchronous sequences, you often want to abandon previous sequences when new data arrives. This is particularly useful for scenarios like search-as-you-type, where each keystroke triggers a new search request and you only care about results from the most recent query.
33+
34+
The `flatMapLatest` operator solves this by cancelling iteration on the previous inner sequence whenever a new element arrives from the outer sequence.
35+
36+
## Proposed Solution
37+
38+
The `flatMapLatest` algorithm transforms each element from the base `AsyncSequence` into a new inner `AsyncSequence` using the provided `transform` closure. When a new element is produced by the base sequence, iteration on the current inner sequence is cancelled, and iteration begins on the newly created sequence.
39+
40+
```swift
41+
extension AsyncSequence where Self: Sendable {
42+
public func flatMapLatest<T: AsyncSequence & Sendable>(
43+
_ transform: @escaping @Sendable (Element) -> T
44+
) -> AsyncFlatMapLatestSequence<Self, T>
45+
}
46+
```
47+
48+
This creates a concise way to express switching behavior:
49+
50+
```swift
51+
userInput.flatMapLatest { input in
52+
fetchDataFromNetwork(input)
53+
}
54+
```
55+
56+
In this case, each new user input cancels any ongoing network request and starts a fresh one, ensuring only the latest data is delivered.
57+
58+
## Detailed Design
59+
60+
The type that implements the algorithm emits elements from the inner sequences. It throws when either the base type or any inner sequence throws.
61+
62+
```swift
63+
public struct AsyncFlatMapLatestSequence<Base: AsyncSequence & Sendable, Inner: AsyncSequence & Sendable>: AsyncSequence, Sendable
64+
where Base.Element: Sendable, Inner.Element: Sendable {
65+
public typealias Element = Inner.Element
66+
67+
public struct Iterator: AsyncIteratorProtocol, Sendable {
68+
public func next() async throws -> Element?
69+
}
70+
71+
public func makeAsyncIterator() -> Iterator
72+
}
73+
```
74+
75+
The implementation uses a state machine to ensure thread-safe operation and generation tracking to prevent stale values from cancelled sequences.
76+
77+
### Behavior
78+
79+
- **Switching**: When a new element arrives from the base sequence, the current inner sequence is cancelled immediately
80+
- **Completion**: The sequence completes when the base sequence finishes and the final inner sequence completes
81+
- **Error Handling**: Errors from either the base or inner sequences are propagated immediately
82+
- **Cancellation**: Cancelling iteration cancels both the base and current inner sequence
83+
84+
## Use Cases
85+
86+
### Search as You Type
87+
88+
```swift
89+
let searchField = AsyncStream<String> { continuation in
90+
// Emit search queries as user types
91+
continuation.yield("s")
92+
continuation.yield("sw")
93+
continuation.yield("swift")
94+
}
95+
96+
let results = searchField.flatMapLatest { query in
97+
searchAPI(for: query)
98+
}
99+
```
100+
101+
Only the results for "swift" will be emitted, as earlier queries are cancelled.
102+
103+
### Location-Based Data
104+
105+
```swift
106+
let locationUpdates = CLLocationManager.shared.locationUpdates
107+
108+
let nearbyPlaces = locationUpdates.flatMapLatest { location in
109+
fetchNearbyPlaces(at: location)
110+
}
111+
```
112+
113+
Each location update triggers a new search, cancelling any ongoing requests for previous locations.
114+
115+
### Dynamic Configuration
116+
117+
```swift
118+
let settings = userSettingsStream
119+
120+
let data = settings.flatMapLatest { config in
121+
loadData(with: config)
122+
}
123+
```
124+
125+
When settings change, data loading is restarted with the new configuration.
126+
127+
## Comparison with Other Operators
128+
129+
**`map`**: Transforms elements synchronously without producing sequences.
130+
131+
**`flatMap`** (if it existed): Would emit elements from all inner sequences concurrently, not cancelling previous ones.
132+
133+
**`switchToLatest`** (Combine): Equivalent operator in Combine framework - `flatMapLatest` is the `AsyncSequence` equivalent.
134+
135+
## Comparison with Other Libraries
136+
137+
**ReactiveX** ReactiveX has an [API definition of switchMap](https://reactivex.io/documentation/operators/flatmap.html) which performs the same operation for Observables.
138+
139+
**Combine** Combine has an [API definition of switchToLatest()](https://developer.apple.com/documentation/combine/publisher/switchtolatest()) which flattens a publisher of publishers by subscribing to the most recent one.

0 commit comments

Comments
 (0)