How to use `AsyncSequence` on macOS 14.5 in Xcode 16 beta? Need help with availability check since `Failure` is unavailb.e

(I'm using Xcode 16 beta on macOS 14.5)

How do I get this to compile for both macOS 15 and macOS 14? :

public final class EmitterOfEvents {
	public typealias Element = Int
	public func notifications() -> some AsyncSequence<Element, Never> {
		AsyncStream<Element>.makeStream().stream
	}
}

I get error:

I'm well aware of SE-0421, and I'm excited about the change. Hopefully I'm just a bit tired right now and what I want is possible, but it feels like something is missing?

I cannot use:

	@available(macOS 15.0, *)
	public func notifications() -> some AsyncSequence<Element, Never> {
		AsyncStream<Element>.makeStream().stream
	}

Since the method notifications() must exist and work on macOS 14 too.

I cannot use if #available(macOS 15.0, *) { since that is put inside the body, and my problem is the return type...

I try:

#if swift(>=6.0)
	public typealias Notifications = AsyncSequence<Element, Never>
#else
	public typealias Notifications = AsyncSequence<Element>
#endif

That does not work either:

So Swift version is the "wrong dimension".

I guess I want #if os(macOS 15.0, *), which is discussed here, but that does not exist.

So I try to find some macOS 15 only new SDK but could not find any... so I cannot use the canImport(NewFancyMacOS15OnlyKit) "trick" either. hmm...

This is in an SPM proj btw, can I do something clever in Package.swift, to create some macOS version dependent variable?

So can I use @available and create two different versions of notifications() with different return types? Hmm does not seem to be possible to spell "else" for @available ?

So what to do? :man_shrugging: hopefully I've missed something :)

1 Like

Existing (but unanswered) topic on this subject.

1 Like

Might #if compiler work? :face_with_monocle: will try tommorow

Have you been able to resolve this issue?

Please try the following solution:

macOS 15's AsyncSequence defines a Failure type, which is not available in earlier versions.

Therefore, we utilize the associatedtype keyword and create a new protocol FailureableAsyncSequence that associates a new Failure type. We make AsyncStream conform to both protocols.

When the AsyncSequence in older systems does not define Failure , it will automatically associate with the Failure type from our new protocol.

Although this does not perfectly solve your problem, it is a feasible solution.

public protocol SomeAsyncSequence: AsyncSequence {
    
}

public protocol FailureableAsyncSequence {
    associatedtype Failure: Error
}

public typealias CustomAsyncSequence = SomeAsyncSequence & FailureableAsyncSequence

extension AsyncStream: CustomAsyncSequence where Element == Int {
    public typealias Failure = Never
}

public final class EmitterOfEvents {
    public typealias Element = Int
    public func notifications() -> some CustomAsyncSequence {
        AsyncStream<Element>.makeStream().stream
    }
}
1 Like

Since I have it in protocol, I've managed to workaround it by just constraining Element as preserving this type was the most important part:

#if swift(>=6.0)
    associatedtype ChangesStream: AsyncSequence<Change, Never>
#else
    associatedtype ChangesStream: AsyncSequence where ChangesStream.Element == Change
#endif

With some ugly generic on implementation side in that case (yet not that critical) it is working and keeping type information, while still forcing to catch error at usage side.

Anyway, some #if available could've been useful for such cases, especially given that on Apple platforms that happens a lot.

The AsyncExtensions library, which is based on AsyncSequence, encountered a similar issue. The solution also involved using associatedtype and protocol splitting, allowing both the Element and Failure generics to function properly. You can refer to it here:

AsyncSubject.swift

If you want to implement advanced features like #if available, I recommend using Swift Macro. It allows for very complex code processing but requires more sophisticated programming.

1 Like

When using protocol spliting, or declaring Failure as new associated type or type ailas cause protocol witness table crash in runtime which is before swift 6.
So you shouldn't reference the Failure type directly when you actually run in before swift 6 runtime.

Below code do compile, but using it will cause abort or protocol witness table crash.

public protocol TypedAsyncIteratorProtocol<Element, Err>: ~Copyable {
    
    associatedtype Element
    associatedtype Err: Error
    
    // hack for compiler but crash on runtime, you have to remove this line or Crash!
    typealias Failure = Err
    
    @inlinable
    mutating func next(isolation actor: isolated (any Actor)?) async throws(Err) -> Element?

}


public protocol TypedAsyncSequence<Element, Err>:AsyncSequence where AsyncIterator: TypedAsyncIteratorProtocol{


    /// The type of errors produced when iteration over the sequence fails.
    associatedtype Err = AsyncIterator.Err where Err == AsyncIterator.Err

    /// Creates the asynchronous iterator that produces elements of this
    /// asynchronous sequence.
    ///
    /// - Returns: An instance of the `AsyncIterator` type used to produce
    /// elements of the asynchronous sequence.
    @inlinable
    func makeAsyncIterator() -> AsyncIterator
}

I think these issue has some relation to this kind of thing.
AsyncIterator Failure crash

Are you trying to make this code compile with multiple versions of the compiler/SDK, or are you trying to get the snippet to work with the Swift 6 compiler but a deployment target lower than macOS 15?

The main problem with the code as written is that it specifies a some AsyncSequence type. AsyncSequence only started specifying primary associated types Element and Failure in the Swift 6 standard library and therefore you can only spell a some AsyncSequence type in code that both compiles with a Swift 6 compiler and runs on OSes aligned with Swift 6 or later.

If your code needs to be backward compatible with either older SDKs or older runtimes, then refactoring the code to use a concrete type conforming to AsyncSequence instead is your best bet I think:

public struct EventSequence<Element>: AsyncSequence {
  let underlyingStream: AsyncStream<Element>

  public func makeAsyncIterator() -> AsyncStream<Element>.AsyncIterator {
    underlyingStream.makeAsyncIterator()
  }
}

public final class EmitterOfEvents {
  public typealias Element = Int

  // no need for @available or #if
  public func notifications() -> EventSequence<Element> {
    let stream = AsyncStream<Element>.makeStream().stream
    return EventSequence(underlyingStream: stream)
  }
}

The latter. Which I think is situation many Swift devs are in: Not ready to install macOS 15 beta, but keen on using Xcode 16 beta.

2 Likes

Ok, code that runs on a deployment target lower than macOS 15 can’t use a some AsyncSequence type.

This seems to compile from 6.0 and deploys (runs) on macOS 14.

public protocol EmitterProtocol<Element, Failure> {
  associatedtype Element where Self.Sequence.Element == Element
  @available(macOS 15.0, *) associatedtype Failure : Error = any Error where Self.Sequence.Failure == Failure
  associatedtype Sequence : AsyncSequence
  
  func notifications() -> Sequence
}

public final class Emitter : EmitterProtocol {
  let (stream, continuation) = AsyncStream<Int>.makeStream()
  
  public func notifications() -> AsyncStream<Int> {
    self.stream
  }
}

public final class Listener {
  public func listen(to emitter: some EmitterProtocol) async throws {
    try await self.listen(to: emitter.notifications())
  }
  
  func listen<S>(to sequence: S) async throws where S : AsyncSequence {
    for try await value in sequence {
      print(value)
    }
  }
}

func main() async throws {
  let emitter = Emitter()
  emitter.continuation.yield(1)
  emitter.continuation.yield(2)
  emitter.continuation.yield(3)
  emitter.continuation.finish()
  
  let listener = Listener()
  try await listener.listen(to: emitter)
}

try await main()

Maybe a workaround from a different POV is to not think about returning an opaque sequence type from a concrete Emitter… what if we return a concrete sequence type from an opaque Emitter?

1 Like

I would avoid this trick if possible. Consider this example:

public protocol P1 {
  //associatedtype Failure
}

public protocol P2 {
  associatedtype Failure
}

public func sameTypeRequirement<T: P1 & P2>(_: T)
    where T.Failure == Never {}

The mangled name of sameTypeRequirement() is

s1u19sameTypeRequirementyyxAA2P1RzAA2P2Rzs5NeverO7FailureAaDPRtzlF

However, if you uncomment the associated type declaration in P1, it changes to

s1u19sameTypeRequirementyyxAA2P1RzAA2P2Rzs5NeverO7FailureAaCPRtzlF

Now imagine that P1 is AsyncSequence and P2 is your protocol. Building the code on the old SDK vs new SDK will change the mangled name of our function, which is a problem if you are building a shared library that you intend on distributing. (EDIT: Unless (<<your module name>>, P2) follows (_Concurrency, AsyncSequence) in lexicographic order; then the mangled name won't change).

1 Like