How to Express Varying Concurrency Isolation in Protocol

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:

protocol MyProtocol {
    func doSomething()
}

func myFunction(myParam: MyProtocol) {
    myParam.doSomething()
}

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:

  1. These functions need to stay synchronous. Unfortunately I can't just change them to be async at this time.
  2. 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.
  3. 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?
  4. 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.

Hope this helps,

1 Like

@ktoso Thanks a lot, this is so helpful!!

Sorry I am not the original poster, but I have a few doubts.

Based on Isolated Conformances proposal I have 2 doubts:

Following was used:

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() {}
}