Missing type erasure?

While working on some streams, I realized that the current APIs are missing any type of type erasure for the streams. The "ideal" solution would be to have some AsyncSequence<Value, Failure>, but that's a bit far away as the language is missing typed throws which would be required to properly expose the Failure type through the protocol. On the other hand Combine already has an eraseToAnyPublisher() method for that purpose.

In my particular case I had to write the following extension for convenience purposes.

extension AsyncSequence {
  func eraseToAsyncThrowingStream() -> AsyncThrowingStream<Element, Error> {
    AsyncThrowingStream { continuation in
      Task {
        do {
          for try await value in self {
            continuation.yield(value)
          }
          continuation.finish()
        } catch {
          continuation.finish(throwing: error)
        }
      }
    }
  }
}


// example
someAsyncStream
  .map { ... }
  .filter { ... }
  .eraseToAsyncThrowingStream() // use as a common return type

Would be nice if the APIs could be extended with something similar, but officially.

5 Likes

AsyncSequences haven't adopted primary associated types yet since there is an ongoing discussion around the nature of typed throws. Exactly what you brought up basically because if AsyncSequences adopt a primary associated type you want to be able to spell out the throwing effect as well.

Hypothetical code for this: func foo() -> some/any throwing AsyncSequence<Int>

In general, I agree that we really need to come to a decision on this topic though because being able to spell some/any AsyncSequence in whatever form is a fundamental tool for API design.

10 Likes

Where is that ongoing discussion happening? I know it was discussed when the primary associated types proposal was in review, but no decision was reached back then, and I haven’t seen any further progress there.

This problem is, in my opinion, the main blocker for a more widespread usage of the, otherwise amazing, async sequence algorithms library.

3 Likes

How does your team manage the propagation of multiple error types along a series of calls?

We generally don't expose typed errors in our interfaces, i.e. we use Swift.Error.

We did start by exposing typed errors across our codebase, but it turned out to be quite a burden with no obvious benefits.

Combining and mapping reactive streams, propagating errors from underlying services and so on led to having many different error types with other or underlying cases, or specific cases for all possible errors from other components. It was just not very ergonomic.

On top of that, at the point of use, we could almost never handle all possible error cases. Usually few cases would be handled in a specific way, while all other errors just got piped into a generic error UI.

Those two points made us stop exposing typed errors going forward.

That being said, it would be nice not to lose the error type, but the language would need to provide an ergonomic solution for combining and propagating multiple error types.

If a function needs to throw multiple error types, or if we need to combine multiple streams with different error types, there needs to be a built in solution that infers the combined error type, maybe as an anonymous sum type or something similar, for typed errors / throws to be ergonomic enough to work with, in my opinion.

2 Likes

The possible solution for that problem was already mention by John McCall. We will still only allow to throw a single error. You have to throw a common error type though. If you throw errors A and B and they have no relation to another then you'd throw Error. If you throw X and Y errors and both are subtypes of error S, then you throw S instead of error. If you throw C and D errors and D is a subtype of C, then you throw C.

If that meant that:

protocol SomeService {
  var events: any throwing AsyncSequence<Event> { get }
}

Could be legally implemented by:

final class MyService: SomeService {
  enum MyServiceError: Error { ... }
  var events: any throwing(MyServiceError) AsyncSequence<Event> { ... }
}

I think that would be a fairly decent compromise. In practice I've found that typed throws (via Result/Publisher) tends to 'encoding the dependency tree'. However, I accept that they'd be useful in limited circumstances, particularly for local control flow.

In the end, I'll just be happy to see a decision reached so that the rough edges around asynchronous sequences can be finally ironed out.