AnyAsyncSequence

If you read swift-evolution/0358-primary-associated-types-in-stdlib.md at main · apple/swift-evolution · GitHub you will find out why it is not yet available.

3 Likes

Has there been any updates on this? This is something sorely needed. Especially if you use something the AsyncAlgorithms package.

For example, something simple like this already becomes super unweildly:

private var testSequence: AsyncMapSequence<AsyncCombineLatest2Sequence<AsyncStream<String>, AsyncStream<Bool>>, String> {
    let stream1 = AsyncStream {
        return "Test"
    }

    let stream2 = AsyncStream {
        return true
    }

    return AsyncCombineLatest2Sequence(stream1, stream2)
        .map { (string, bool) -> String in
            if bool {
                return "\(string) is true"
            } else {
                return "\(string) is false"
            }
        }
}

you could do something like this, but then you lose access to type information without casting:

private var testSequence: some AsyncSequence {
    let stream1 = AsyncStream {
        return "Test"
    }

    let stream2 = AsyncStream {
        return true
    }

    return AsyncCombineLatest2Sequence(stream1, stream2)
        .map { (string, bool) -> String in
            if bool {
                return "\(string) is true"
            } else {
                return "\(string) is false"
            }
        }
}

private func test() async throws {
    for try await value in testSequence {
        // value is (some AsyncSequence).Element
    }
}
4 Likes

I agree, for now it is very annoying to use AsyncSeqence crossing api boundaries. As a workaround, I always use AsyncStream (feeding it the values from another AsyncSequence) as a return type. I really hope we will get primary associated types for AsyncSequence soon.

5 Likes

Do we know whyAsyncSequence doesn't have a primary associated type? Is it hard to implement for that particular type or are there other concerns?

The problem is about error handling. @hborla summarized it well in the SE-0346 review thread:

See also the Pitch thread of SE-0358:

1 Like

Primary associated types have been helpful, but they're still a band-aid, and they don't even yet cover all synchronous cases that should be expressible with multiple somes. E.g.

import typealias Algorithms.StridingSequence

public extension Sequence {
  /// Distribute the elements as uniformly as possible, as if dealing one-by-one into shares.
  /// - Note: Later shares will be one smaller if the element count is not a multiple of `shareCount`.
  @inlinable func distributedUniformly(shareCount: Int)
  -> LazyMapSequence<Range<Int>, StridingSequence<DropFirstSequence<Self>>> {
    (0..<shareCount).lazy.map {
      dropFirst($0).striding(by: shareCount)
    }
  }
}

Array((1...10).distributedUniformly(shareCount: 3)).map(Array.init))
[[1, 4, 7, 10], [2, 5, 8], [3, 6, 9]]

We need full constraints for opaque return types.

It's not quite as elegant as the proposed var values: some AsyncSequence<String>, but you can do:

var values: AsyncMapSequence<some AsyncSequence, String> {
    // ...
}
2 Likes

If AsyncSequence had an primary associated type Element, you could write the code like this:

func getStrings() -> some AsyncSequence<String?> {
  AsyncStream<Data> { continuation in 
    // fetch data
  }.map { data in
    String(data: data, encoding: .utf8)
  }
}

As it stands, it doesn't, but you could easily work around this.

public protocol SomeAsyncSequence<Element>: AsyncSequence { }
extension AsyncMapSequence: SomeAsyncSequence { }

func getStrings() -> some SomeAsyncSequence<String?> {
  AsyncStream<Data> { continuation in 
    // fetch data
  }.map { data in
    String(data: data, encoding: .utf8)
  }
}

There are probably other issues, but importantly, @rethrows won't propagate.

import AsyncAlgorithms
let sequence = [()].async
var collected = await Array(sequence)
let someSequence: some SomeAsyncSequence<Void> = sequence
collected = await .init(someSequence) // Call can throw but is not marked with 'try'

Then you could just change SomeAsyncSequence to be a rethrows protocol.

@rethrows public protocol SomeAsyncSequence<Element>: AsyncSequence { }

No, that’s precisely what I was saying doesn’t work.

I wonder if that has to do with the protocol inheritance or the some type. I think a rethrows protocol is supposed to determine whether it throws based on how the requirements are fulfilled.

I too find this strong typing hard to deal with, especially for us developing APIs using AsyncSequence. Say we have a protocol

public protocol CustomFeed {
    /// The type of the sequence of events emitted by this feed.
    associatedtype Events: AsyncSequence & Sendable where Events.Element == CustomFeedEvent
    
    /// The events emitted by this feed.
    var events: Events { get }
}

When a user implement their type that conforms to the CustomFeed protocol, they have to provide a var events: AsyncStream<CustomFeedEvent> - Not AsyncMapSequence<AsyncStream<Int>, CustomFeedEvent> (such as they may want to create some mock data to test it), not AsyncThrowingCompactMapSequence<AsyncLineSequence<URL.AsyncBytes>, CustomFeedEvent> (such as they may need to parse some file line-by-line to generate the feed).

It makes us designing the API hard, as we cannot provide all the combinatorial amount of APIs to handle the input types, so it is the user's responsibility to fit their input into our typing requirement.

That is also hard, because there isn't a built-in way to map an AsyncStream<T1> into an AsyncStream<T2> with a transforming function T1T2 , as it works for mapping Array<T1> into Array<T2>.

It can be done without too much code.

extension AsyncStream {
  func map2(transform: (Element) async -> Element) -> AsyncStream<Element> {
    AsyncStream { continuation in 
      for await element in self {
        await continuation.yield(transform(element))
      }
      continuation.finish()
    }
  }
}

That is not as efficient as AsyncMapSequence though.