apple / swift-async-algorithms

Async Algorithms for Swift
Apache License 2.0
2.95k stars 147 forks source link

Type erasure for `AsyncSequence` #309

Closed alex-vasenin closed 6 months ago

alex-vasenin commented 6 months ago

I think type erasure for complicated AsyncSequence would be a nice addition to this package. There are some discussions about this feature on the net, for example, here and here.

I personally encountered this need while searching for solution for merging two AsyncSequence from NotificationCenter. Currently this code produces warning with SWIFT_STRICT_CONCURRENCY = Complete even though I use the solution recommended in NotificationCenter.notifications(named:object:) documentation:

import AsyncSequence

extension NotificationCenter {
    func notifications(_ name1: Notification.Name, _ name2: Notification.Name) -> AsyncMerge2Sequence<AsyncMapSequence<NotificationCenter.Notifications, Notification.Name>, AsyncMapSequence<NotificationCenter.Notifications, Notification.Name>> {
        let first = notifications(named: name1).map { $0.name }
        let second = notifications(named: name2).map { $0.name }
        return merge(first, second) // Warning: Conformance of 'Notification' to 'Sendable' is unavailable
    }
}

And the type annotations are obviously quite ugly.

pyrtsa commented 6 months ago

I think you'll have to wait for Swift 6.0 where, with https://github.com/apple/swift-evolution/blob/main/proposals/0421-generalize-async-sequence.md implemented, you'll be able to spell the return type as:

some AsyncSequence<Notification, any Error>

or maybe even

some AsyncSequence<Notification, Never>

assuming that the associated type NotificationCenter.Notifications.Failure gets defined by Apple as Never as I believe it should.

alex-vasenin commented 6 months ago

Thank you for promptly reply. So much is going on in Swift Evolution! It's good they eventually include it in the language itself.

In the meanwhile, do you think this naive solution will suffice (performance is not an issue):

extension NotificationCenter {
    private func typeErasedNotifications(_ name: Notification.Name) -> AsyncStream<Notification.Name> {
        AsyncStream { continuation in
            let task = Task {
                for await notification in self.notifications(named: name) {
                    continuation.yield(notification.name)
                }
            }
            continuation.onTermination = { _ in task.cancel() }
        }
    }
}
FranzBusch commented 6 months ago

I would not recommend that pattern since it removes the back pressure propagation and buffers potentially unlimited data in the intermediate stream. Instead you can write yourself an AnyAsynStream wrapper.

alex-vasenin commented 6 months ago

Instead you can write yourself an AnyAsyncStream wrapper.

I tried to wrap my head around Swift type system for about an hour and it wasn't fruitful. Could you point me to a good example of similar implementation?

FranzBusch commented 6 months ago

Something like this should work: https://github.com/vsanthanam/AnyAsyncSequence

alex-vasenin commented 6 months ago

That's great! Thank you!

alex-vasenin commented 6 months ago

TLDR some AsyncSequence will be available in Swift 6 In the meanwhile we can use something like https://github.com/vsanthanam/AnyAsyncSequence