First off, apologies if this question has already been answered elsewhere. I'm still new to Swift Concurrency, and I don't think I have all the proper vocabulary figured out yet, which makes searching difficult.
Say that I have a protocol, and I pass an instance of that protocol into a function:
However, I have two different implementations of MyProtocol: one that is non-isolated, and one that is isolated to the MainActor:
class MyRegularImpl: MyProtocol {
func doSomething() {}
}
@MainActor
class MyMainActorImpl: MyProtocol {
func doSomething() {}
}
Obviously in the MainActor version I get a warning that the isolated version of doSomething() can't be used to satisfy the protocol. However, I think it is actually "safe" because I only ever call myFunction() with MyMainActorImpl the from the MainActor. Also I don't hold onto the passed-in instance - it is only used within the scope of the function call. And I don't send it across any isolation boundaries.
Is there any way to express this restriction / nuance using Swift Concurrency? Or is there some different approach I should be considering?
Complicating Factors:
These functions need to stay synchronous. Unfortunately I can't just change them to be async at this time.
I think maybe you can express this somehow if the non-isolated version was instead also isolated to some actor. But unfortunately I can't do that either.
Ideally I would be able to stay in Swift 5 language mode. But upgrading to 6 might be possible if this provided a good solution. Would SE-0446 "non-escapable types" help with this?
In my real code, the function provided by MyProtocol is generic. In some other instances, I was able to get around this isolation problem by wrapping the call to doSomething() in a closure, and then passing in the closure instead of the protocol instance. This seemed to work fine, I guess since the closure parameter was non-escaping? But unfortunately closures can't be generic.
What you're describing is solved by a feature in Swift 6.2 (now in beta releases): isolated conformances.
Isolated conformances are conformances whose use is restricted to a particular global actor, so this sounds like what you're after here with your Impl type -- you'll only ever be calling it from the main actor, so you want that synchronous protocol requirement to work from there as well.
This is unrelated to "swift 6 language mode", you can use the feature without complete/strict concurrency checking that swift 6 language mode implies.
Swift: 6.2
Default Actor Isolation: MainActor
Strict Concurrency Checking: Complete
Doubt 1:
How to do the same for actors (non-main actor)?
actor MyMainActorImpl: MyProtocol {
func doSomething() {}
}
Doubt 2:
Compiler didn't show any warning, I was thinking I would have to write class MyMainActorImpl: @MainActor MyProtocol { but I didn't even have to specify @MainActor MyProtocol.
Just curious the requirement infer the @MainActor MyProtocol done in a different proposal?
@MainActor
class MyMainActorImpl: MyProtocol {
func doSomething() {}
}
The full name of the feature I linked to is "Global-actor isolated conformances" which should answer your question -- it's just not a thing for instance actors (at least right now).
Not sure what you're checking, because that's not even a warning but just a hard error. An actor cannot witness synchronous requirements unless with nonisolated members, or this isolated conformances feature. See here:
The reason I didn't have to mention @MainActor MyProtocol and yet compiler didn't complain was (I think) because of Default Actor Isolation: MainActor in the build settings. Changing it to non-isolated compiler throws the warning.
Just summarizing my understanding:
This feature allows us the same protocol to be used by @MainActor and non-isolated classes / structs
So presently 2 things are not possible:
Have same protocol conform to Main Actor and actors (non-main actor)
Main actor can't conform to protocol MyProtocol: Actor