I’m creating a model object that should expose a shared stream of status updates:
actor MyClient: Client {
let statusUpdates: any AsyncSequence<Status, Never> & Sendable
init() {
let (stream, continuation) = AsyncStream<Status>.makeStream()
statusUpdates = stream.share()
}
}
But that means my protocol must require any AsyncSequence instead of using an associatedtype. It feels wrong to encode the implementation details in the protocol like this. But I can't figure out another way to store the result of share(), because it returns an opaque type.
I can use a lazy var:
lazy var statusUpdates: some AsyncSequence<Status, Never> & Sendable = _statusUpdates.stream.share()
private let _statusUpdates = AsyncStream<Status>.makeStream()
But on my actor, that means I have to await access to the property, even though it should not be mutated after initialization.
I could create my own AnyAsyncIterator type eraser that does conform to the protocol, but that feels like a less-than-optimal solution.
Basically, you'll quickly run into "error: property definition has inferred type , involving the 'some' return type of another declaration".
Swift doesn't let you do this for, I believe, ABI reasons — changing the concrete implementation of a some type is allowed, so nothing that would result in encoding the name of the current type into any other type, is allowed?
I don't see a way around this without an indirection — you could use a base class with a generic subclass, for example, or as you say, any AsyncSequence is fine. But I think any direct use of the result of .share() will fall afoul of the above error.
If swift-async-algorithms declared AsyncShareSequence as public, and removed the some return type from share() in favor of declaring the concrete type, the problem would go away. I don't see why they shouldn't do that; it doesn't impose much of a constraint on their long-term ABI evolution? But I don't know why they've opted for the current approach.
My gut is that this is a bug, since pretty much any other attempt to create a stored property of this type will result in an error…
Your original snippet would compile if you allowed your Client protocol to change to:
protocol Client {
var statusUpdates: any AsyncSequence<Status, Never> & Sendable { get }
}
Do you think there's enough of a performance reason not to do that?
That said, I also occasionally hit the wall with the ergonomics of opaque result types, precisely when there's need to carry the result around in a stored property. The language designers are clearly aware of the issue, and you can see it worked around via underscored attributes in the swiftinterface files for SwiftUI. Slava has even written some elaborate docs for it in his excellent documentation on Compiling Swift Generics; see esp. chapter 13.2, pp. 457–459.
Disclaimer: The following is not officially supported in Swift
The way to make it work is by applying the @_opaqueReturnTypeOf(mangling, index) attribute on a placeholder name such as __ (followed up with any generic arguments in angle brackets). I couldn't find a way to find the correct mangling for the opaque return type of the expression stream.share(), but if you move the whole setup into a function of your own, it all appears to work:
public struct Status: Sendable {}
protocol Client {
associatedtype StatusUpdates: AsyncSequence<Status, Never> & Sendable
var statusUpdates: StatusUpdates { get }
}
actor MyClient: Client {
typealias StatusUpdates = @_opaqueReturnTypeOf(
"$s8MyModule8MyClientC18shareStatusUpdatesQr_ScS12ContinuationVyAA0F0V_GtyFZ",
0
) __
private let continuation: AsyncStream<Status>.Continuation
let statusUpdates: StatusUpdates
init() {
(statusUpdates, continuation) = Self.shareStatusUpdates()
}
static func shareStatusUpdates() -> (
some AsyncSequence<Status, Never> & Sendable,
AsyncStream<Status>.Continuation
) {
let (stream, continuation) = AsyncStream<Status>.makeStream()
return (stream.share(), continuation)
}
}
The above assumes a module named MyModule. Apply nm command to your compiled library or executable to find your precise mangled name, and swift demangle --help for the opposite.
No, I don't think this to be a bug at all. For a declaration with initial value, which lazy var always is, the return type is always visible to the compiler, so no trouble inferring the opaque type. And lazy is needed here because the initial value expression needs to refer the other stored property stream.
Thanks for the tips! The reason I posted is because storing the result of .share() in a property seems like the primary way it would be used, enabling it to be accessed from multiple contexts. I imagined the library authors must have an intended approach for this.
It seems like my options are any in the protocol, a lazy var, or a lot of boilerplate (a type-erasing wrapper or @pyrtsa's amazing solution). Avoiding boilerplate was why I wanted to use .share() in the first place, to replace my custom multicast solution of storing Continuations in a Dictionary indexed by UUIDs.
For my use case, I decided I don't really need multicast after all. My Client has a single @Observed owner with a Task that stores the status in a property.
It sounds like in the era before opaque values and primary associated types the choice to return concrete values was out of necessity. But now that primary associated types are available it sounds like the general preference is to prefer opaque.
I don't think it's a bug for either of these reasons; I think it's a bug because the compiler goes out of its way (with explicit errors) to prevent the creation of any property or method with a type dependent on another some type.
So I guess the question is "is this disallowed because the problem is with writing the name, or is it disallowed because the problem is with using the type in this position?"
If the former, we should make an SE- to get a user-friendly typeof operator that we can use to spell opaque types, and the OP's problem will be solved by writing typeof<AsyncStream<Status>.share()> or similar.
If the latter, then typeof isn't a reasonable extension to the language, and lazy var working is certainly a bug.
I understand that people seem to have a general preference toward opaque types, and I can guess why (more flexibility to change the implementation later without affecting ABI). But given the significant downsides (as evidenced by this thread), and the relatively small cost of making the actual return type public (just be careful that each unique some gets a unique concrete type, if you want to preserve your ability to evolve the library ABI-compatibly), I'm not sure this is a reasonable decision. Particularly not for swift-async-algorithms, which doesn't have any ABI concerns.
My brain may still be turning on for the morning but is the Client protocol not already generic? It has an associated StatusUpdates type. Or are you saying that the MyClient type itself should be generic?