[Pitch] Generalize `AsyncSequence` and `AsyncIteratorProtocol`

Thanks for taking this part of the Typed throws in the Concurrency module pitch. This is definitely one of the more complicated pieces of the pitch and I agree that having a dedicated pitch for it is very sensible.

Now to some feedback on the pitch.

mutating func nextElement(_ actor: isolated (any Actor)?) async throws(Failure) -> Element?

This is the first method where we are adopting the isolated parameter in the standard library if I am not mistaken. I don't think we have written any API design guidelines around the naming of isolated parameters and I find the proposed call site spelling a bit confusing:

var iterator: some AsyncIteratorProtocol<Int, Never> = ...
let element = await iterator.nextElement(nil)

Just from looking at the code it isn't obvious what nil means here. Similarly if I would be passing self if I am inside an actor. What if we add an external parameter label here e.g. nextElement(isolatedTo actor: ...)?

To avoid silently allowing conformances that implement neither requirement, and to facilitate the transition of conformances from next() to nextElement() , we add a new availability rule where the witness checker diagnoses a protocol conformance that uses an deprecated, obsoleted, or unavailable default witness implementation. Deprecated implementations will produce a warning, while obsoleted and unavailable implementations will produce an error.

This is really cool and something that makes evolving protocols forward with minimal adopter friction amazing! @ktoso maybe we can use this trick for the SerialExecutor protocols as well once it landed.

Once the concrete AsyncIteratorProtocol types in the standard library, such as Async{Throwing}Stream.Iterator , implement nextElement() directly, code that iterates over those concrete AsyncSequence types in an actor-isolated context may exhibit fewer hops to the generic executor at runtime.

This is quite an important part of the pitch that has a larger impacted then just fewer hops and IMO deserves a bit more discussion. With SE-0306 we have changed the execution semantics of non isolated methods to eagerly hop to the global concurrent executor since we noticed that users of Swift Concurrency were often hogging the MainActor with non-isolated methods. With this pitch we introduce the first method in the standard library where we aggressively try to stick to the current context's isolation; hence, we introduce a new area where developers might hog the MainActor.
Personally, I don't think this is a bad thing but it might result in performance changes for applications once they recompile with a newer Swift version and the nextElement(_:) implementation gets picked by the compiler.

Note that the use of an existential type (any Actor)? means that embedded Swift would need to support class existentials in order to use nextElement() .

Do we think this is a reasonable thing for Embedded Swift to support in the future? It would be sad if we would limit the possible usage of AsyncSequence for Embedded because of the usage of any Actor here.

Lastly, the original pitch of @ktoso and me included changes to the various async sequence algorithms in the standard library. Do we foresee any problems if we are only landing the protocol/compiler changes for typed throws but not adopt them in the algorithms?

4 Likes