I have the following code snippet and I am making it Swift 6. In the specific case, 'xxxx' is CLLocation, and the SDK documentation marks it as @unchecked Sendable. Is that not the same as any Sendable? What is the compiler error trying to tell me?
extension AsyncStream where Self.Element == Sendable {
var nilTerminating: AsyncStream<Self.Element?> {
return AsyncStream<Self.Element?> { continuation in
let t = Task {
for await value in self {
continuation.yield(value)
}
continuation.yield(nil)
continuation.finish()
}
continuation.onTermination = { _ in
t.cancel()
}
}
}
}
What’s the reason you’re using Self.Element == Sendable (desugared into Self.Element == any Sendable) instead of Self.Element: Sendable?
The way your extension is currently written, it will only work for AsyncStream of any Sendable existentials as elements and not concrete types conforming to Sendable (like CLLocation).
LHS: RHS is a requirement that LHS (a type) conforms to or subclassesRHS. In this context, RHS may either be the name of a protocol (in which case it's a conformance requirement) or it may be a type name (in which case it's a subclass relationship, and the thing on the right must be a class type)†.
LHS == RHS is a requirement that LHS (a type) is exactly the same type asRHS.
† For simplicity, I'm ignoring some other situations here, like enum raw value syntax which is its own special case, and layout constraints.
In #2, this means that RHS is interpreted as a type, never a protocol. If RHS is the name of a protocol, then it must be interpreted as the existential type any RHS. In other words, SomeType == SomeProtocol is equivalent to SomeType == any SomeProtocol. The existential type any SomeProtocol can hold values of any type that conforms to SomeProtocol, but that means that you've discarded the other type information about what it actually is. More importantly (with few hardcoded exceptions, like Error), the existential type any SomeProtocol does not itself conform to SomeProtocol).
There's an ExistentialAny upcoming feature that forces existentials to be written with the explicit any keyword, which would have made the mistake in the original post fail to compile since it would expect you to have written Self.Element == any Sendable. That may have shed some light on the problem; for example:
<source>:1:34: warning: use of protocol 'Sendable' as a type must be written 'any Sendable'; this will be an error in a future Swift language mode
1 | extension Array where Element == Sendable {}
| `- warning: use of protocol 'Sendable' as a type must be written 'any Sendable'; this will be an error in a future Swift language mode
Although, in a requirement context, a good enhancement for that feature might be to detect T == SomeProtocol and also offer a second diagnostics, like "note: or did you mean the conformance requirement 'Element: Sendable'?"
Not that this solves your original question but there are other more efficient and general ways of solving what you originally needed: AsyncAlgorithms have a few things that might help.
For example:
@available(macOS 10.15, *)
extension AsyncSequence {
@available(macOS 15.0, *)
var nilTerminating: some AsyncSequence<Element?, Failure> {
chain( // splice two AsyncSequences together one after another
map { Optional.some($0) }, // convert the AsyncSequence of Element to an AsyncSequence of Element?
CollectionOfOne(nil as Element?).async // a single nil Element? as an AsyncSequence
)
}
}
Would more generally do what you needed and would result in a much more performant output.
What's the behaviour when cancelling the first sequence(map { Optional.some($0) })? Would chain still be nilTerminating? The reason I need nilTerminating, because combineLatest waits for all streams to finish. However, I need a combineLatest that terminates when one of the streams ends.
To answer my own question, the .chain works great with finite sequence . But, it will not work(nilTerminating) with infinite sequence and task cancellation.
import SwiftUI
import AsyncAlgorithms
@available(macOS 10.15, *)
extension AsyncSequence {
@available(macOS 15.0, *)
var nilTerminating: some AsyncSequence<Element?, Failure> {
chain( // splice two AsyncSequences together one after another
map { Optional.some($0) }, // convert the AsyncSequence of Element to an AsyncSequence of Element?
CollectionOfOne(nil as Element?).async // a single nil Element? as an AsyncSequence
)
}
}
struct ContentView: View {
@State
var value: String = ""
@State
var task: Task<Void, Never>? = nil
var body: some View {
VStack {
Button {
self.task?.cancel()
} label: {
Image(systemName: "stop.circle")
.imageScale(.large)
.foregroundStyle(.tint)
}
Text(value)
}
.onAppear {
let t = Task {
let stream = self.makeSequence().nilTerminating
for await item in stream {
self.value.append("\(String(describing: item))\n")
}
}
self.task = t
}
.padding()
}
func makeSequence() -> AsyncStream<Int> {
AsyncStream { continuation in
continuation.onTermination = { _ in
continuation.finish()
}
Task {
for i in 1...10 {
continuation.yield(i)
try? await Task.sleep(nanoseconds: 1_000_000_000)
}
continuation.finish()
}
}
}
}
#Preview {
ContentView()
}
I really appropriate another way of implementing the .nilTerminating only if there's a way I can early exit from for .combineLatest(nilTerminatingStream, B) then I wouldn't need this special value to signal exit