Skip to content

Commit dd024db

Browse files
Proposal to add Hashable conformance to Async(Throwing)Stream.Continuation (#2700)
* Proposal to add `Hashable` conformance to `Async(Throwing)Stream.Continuation` * Apply suggestions from code review Co-authored-by: Frederick Kellison-Linn <[email protected]> * Apply review comments * Removed backdeployment considerations * Continuation hashable conformance is 0468 --------- Co-authored-by: Frederick Kellison-Linn <[email protected]> Co-authored-by: Freddy Kellison-Linn <[email protected]>
1 parent 21626f6 commit dd024db

File tree

1 file changed

+130
-0
lines changed

1 file changed

+130
-0
lines changed
Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
# `Hashable` conformance for `Async(Throwing)Stream.Continuation`
2+
3+
* Proposal: [SE-0468](0468-async-stream-continuation-hashable-conformance.md)
4+
* Authors: [Mykola Pokhylets](https://github.com/nickolas-pohilets)
5+
* Review Manager: [Freddy Kellison-Linn](https://github.com/Jumhyn)
6+
* Status: **Active review (March 12...25, 2025)**
7+
* Implementation: [swiftlang/swift#79457](https://github.com/swiftlang/swift/pull/79457)
8+
* Review: ([pitch](https://forums.swift.org/t/pitch-add-hashable-conformance-to-asyncstream-continuation/77897))
9+
10+
## Introduction
11+
12+
This proposal adds a `Hashable` conformance to `Async(Throwing)Stream.Continuation`
13+
to simplify working with multiple streams.
14+
15+
## Motivation
16+
17+
Use cases operating with multiple `AsyncStream`s may need to store multiple continuations.
18+
When handling `onTermination` callback, client code needs to remove the relevant continuation.
19+
20+
To identify the relevant continuation, client code needs to be able to compare continuations.
21+
22+
It is possible to associate a lookup key with each continuation, but this is inefficient.
23+
`AsyncStream.Continuation` already stores a reference to `AsyncStream._Storage`,
24+
whose identity can be used to provide simple and efficient `Hashable` conformance.
25+
26+
Consider this simple Observer pattern with an `AsyncSequence`-based API.
27+
To avoid implementing `AsyncSequence` from scratch it uses `AsyncStream` as a building block.
28+
To support multiple subscribers, a new stream is returned every time.
29+
30+
```swift
31+
@MainActor private class Sender {
32+
var value: Int = 0 {
33+
didSet {
34+
for c in continuations {
35+
c.yield(value)
36+
}
37+
}
38+
}
39+
40+
var values: some AsyncSequence<Int, Never> {
41+
AsyncStream<Int>(bufferingPolicy: .bufferingNewest(1)) { continuation in
42+
continuation.yield(value)
43+
self.continuations.insert(continuation)
44+
continuation.onTermination = { _ in
45+
DispatchQueue.main.async {
46+
self.continuations.remove(continuation)
47+
}
48+
}
49+
}
50+
}
51+
52+
private var continuations: Set<AsyncStream<Int>.Continuation> = []
53+
}
54+
```
55+
56+
Without a `Hashable` conformance, each continuation needs to be associated with an artificial identifier.
57+
E.g. wrapping continuation in a class, identity of the wrapper object can be used:
58+
59+
```swift
60+
@MainActor private class Sender {
61+
var value: Int = 0 {
62+
didSet {
63+
for c in continuations {
64+
c.value.yield(value)
65+
}
66+
}
67+
}
68+
69+
var values: some AsyncSequence<Int, Never> {
70+
AsyncStream<Int> { (continuation: AsyncStream<Int>.Continuation) -> Void in
71+
continuation.yield(value)
72+
let box = ContinuationBox(value: continuation)
73+
self.continuations.insert(box)
74+
continuation.onTermination = { _ in
75+
DispatchQueue.main.async {
76+
self.continuations.remove(box)
77+
}
78+
}
79+
}
80+
}
81+
82+
private var continuations: Set<ContinuationBox> = []
83+
84+
private final class ContinuationBox: Hashable, Sendable {
85+
let value: AsyncStream<Int>.Continuation
86+
87+
init(value: AsyncStream<Int>.Continuation) {
88+
self.value = value
89+
}
90+
91+
static func == (lhs: Sender.ContinuationBox, rhs: Sender.ContinuationBox) -> Bool {
92+
lhs === rhs
93+
}
94+
95+
func hash(into hasher: inout Hasher) {
96+
hasher.combine(ObjectIdentifier(self))
97+
}
98+
}
99+
}
100+
```
101+
102+
Note that capturing `continuation` or `box` in `onTermination` is safe, because `onTermination` is dropped after being called
103+
(and it is _always_ called, even if `AsyncStream` is discarded without being iterated).
104+
105+
## Proposed solution
106+
107+
Add a `Hashable` conformance to `Async(Throwing)Stream.Continuation`.
108+
109+
## Detailed design
110+
111+
Every time when the `build` closure of the `Async(Throwing)Stream.init()` is called,
112+
it receives a continuation distinct from all other continuations.
113+
All copies of the same continuation should compare equal.
114+
Yielding values or errors, finishing the stream, or cancelling iteration should not affect equality.
115+
Assigning `onTermination` closures should not affect equality.
116+
117+
## Source compatibility
118+
119+
This is an additive change.
120+
121+
Retroactive conformances are unlikely to exist, because current public API of the `Async(Throwing)Stream.Continuation`
122+
does not provide anything that could be reasonably used to implement `Hashable` or `Equatable` conformances.
123+
124+
## ABI compatibility
125+
126+
This is an additive change.
127+
128+
## Implications on adoption
129+
130+
Adopters will need a new version of the standard library.

0 commit comments

Comments
 (0)