Why the design of operators transform AsyncSequence result long generic names?

If we transform an AsyncStream with map and filter, the result type is AsyncFilterSequence<AsyncMapSequence<AsyncStream<T>, T>>

The result type is so long and ugly when there are several transformations, without any readability and hard to understand.

So, why the design is like this instead of keeping all the transformed results equal to AsyncStream, just like Reactive frameworks.

It’s possible to do this, for example:

extension AsyncStream {
  func map<T>(_ conversion: @escaping (Element) async -> T) -> AsyncStream<T> {
    var iterator = self.makeAsyncIterator()
    return .init {
      if let value = await iterator.next() {
        return await conversion(value)
      }
      return nil
    }
  }
}

What I can infer is that it’s easy to implement map , filter and all the operators in the extension of AsyncSequence , and if doing this you will find it can’t return a new concrete type to present the transformed result type.

1 Like

Type erasure usually requires an extra level of indirection. Note that your example dynamically allocates a new closure, while ideally the stronger type information present in the stdlib approach allows the compiler to devirtualize the call to .next().

3 Likes

For the record, this is what opaque result types are for. The type system can keep the specific type information without erasure (for better optimization), while still giving good human-readable abstractions (-> some AsyncSequence<T>)

2 Likes

FWIW it looks like we might be blocked on an easy solution to return some AsyncSequence if we are deploying to older OS versions.

1 Like

some is still not available in protocol and the most important thing with AsyncSequence is it lost the Element type because AsyncSequence has no primary associate type.

I'm writing business code like EventsProvider to fetch events: func fetchEvents(predicates: Predicate<Events>) -> any AsyncSequence, and it should be public in a protocol layer EventsProvidable for better independency and easier to mock. But in this protocol the function's return type any AsyncSequence lost the element's type.

For temporary solution, I'm using the eraseToStream() function to convert any AsyncSequence to AsyncStream<T>, which let me think about the design of the operators of transform AsyncSequence.

As mentioned in the preceding message, this was fixed in Swift 6 by taking advantage of new language features (specifically, typed throws)

1 Like

I know, so my original question is why not just using AsyncStream as the mapped result, becasue from the language user's view it's easier to use and understand.

I cannot provide a definitive answer, but my understanding is that AsyncStream is merely one specific type conforming to the AsyncSequence protocol. There are numerous other conforming types (such as TaskGroup), and it seems the designers aimed to avoid redundant implementations of the same algorithm across these various concrete types. Instead, they opted to formalize these operations as default implementations on the protocol, necessitating the use of wrapped types such as AsyncFilterSequence and AsyncMapSequence.

A similar design pattern is evident in the Sequence and LazySequence protocols, where it is more apparent. Numerous types conform to Sequence, and duplicating implementation would be inefficient. For example, similar operations on LazySequence also yield types like LazyMapSequence and LazyFilterSequence.

2 Likes