twittemb
(Thibault Wittemberg)
1
Hi,
Is there an interest in that repo to extend operators that involve cardinally to their variadic parameters counterpart (merge, zip, combineLatest) ?
It would involve being able to type erase AsyncSequence at some point. For merge, a possible implementation would be something like:
public func mergeMany<Base: AsyncSequence>(_ bases: Base...) -> AnyAsyncSequence<Base.Element>
where
Base: Sendable,
Base.Element: Sendable,
Base.AsyncIterator: Sendable {
guard let seed = bases.first?.eraseToAnyAsyncSequence() else { return AsyncEmptySequence<Base.Element>().eraseToAnyAsyncSequence() }
return bases[1..<bases.count].reduce(into: seed) { accumulator, base in
accumulator = merge(accumulator, base).eraseToAnyAsyncSequence()
}
}
where:
public struct AsyncEmptySequence<Element>: AsyncSequence {
public typealias Element = Element
public typealias AsyncIterator = Iterator
public init() {}
public func makeAsyncIterator() -> AsyncIterator {
Iterator()
}
public struct Iterator: AsyncIteratorProtocol {
public func next() async -> Element? {
nil
}
}
}
and
public extension AsyncSequence {
/// Type erase the AsyncSequence into an AnyAsyncSequence.
/// - Returns: A type erased AsyncSequence.
func eraseToAnyAsyncSequence() -> AnyAsyncSequence<Element> {
AnyAsyncSequence(self)
}
}
/// Type erased version of an AsyncSequence.
public struct AnyAsyncSequence<Element>: AsyncSequence {
public typealias Element = Element
public typealias AsyncIterator = Iterator
private let makeAsyncIteratorClosure: () -> AsyncIterator
public init<BaseAsyncSequence: AsyncSequence>(_ baseAsyncSequence: BaseAsyncSequence) where BaseAsyncSequence.Element == Element {
self.makeAsyncIteratorClosure = { Iterator(baseIterator: baseAsyncSequence.makeAsyncIterator()) }
}
public func makeAsyncIterator() -> AsyncIterator {
Iterator(baseIterator: self.makeAsyncIteratorClosure())
}
public struct Iterator: AsyncIteratorProtocol {
private let nextClosure: () async throws -> Element?
public init<BaseAsyncIterator: AsyncIteratorProtocol>(baseIterator: BaseAsyncIterator) where BaseAsyncIterator.Element == Element {
var baseIterator = baseIterator
self.nextClosure = { try await baseIterator.next() }
}
public func next() async throws -> Element? {
try await self.nextClosure()
}
}
}
Another implementation would be to use the existing code, but using an Array of PartialIteration<Base.AsyncIterator, Partial> as a state, and going through all its elements to compute the Tasks to give to Task.select for each call to next ?
What is your take on that ?
Thanks a lot.
Some of the initial reason why zip didn't make it in the standard library along side map et al was because of the lack of variadic generics. Requiring the bases to be all of the same type is a bit unfortunate - from my experience implementing and triaging Combine; vastly more often than not zip that family of combinator algorithms usually takes a heterogeneous set of inputs.
The problem with implementing AnyAsyncSequence is that it forces them all to throw. Ideally I think we really want any throws AsyncSequence<T> or some throws AsyncSequence<T> to pull that type of thing off. Also, similarly to AnyPublisher, the eraser types impact some distinct performance bottlenecks that opaque types do not suffer. We distinctly avoided AnyAsyncSequence for a reason; namely the work Holly, Pavel, and Slava are working on. That work got the first step to it, we just need to address the effects side of it next. Which that is no small task that is for sure.
3 Likes
olbo
(Oliver Atkinson)
3
There are many cases where the type being the same is not a problem, some cases even desired
As part of this, it would be nice to see support for arrays as well as variadics