skip to Main Content

What is the rationale behind Swift compiler being overly strict about Protocol conformances? For instance, consider the following code:

protocol TestProtocol {
    func testFunction()
}


actor TestClass: TestProtocol {
    func testFunction() {
    
    }
}

The build fails with the error saying Actor-isolated instance method 'testFunction()' cannot be used to satisfy nonisolated protocol requirement.

But isn’t the whole purpose of actor thread safety and preventing data races? If so, where is the data race in the above code or conformance to protocol in general?

Edit: Based on the answers received from @Sweeper and @paulw11, here is the code I wrote and it crashes at runtime with error Incorrect actor executor assumption

import Foundation

protocol TestProtocol {
    func increment()
    func decrement()
}

actor TestActor: @preconcurrency TestProtocol {
    var i = 5
    
    func increment() {
       i = i + 1
        print("incremented (i)")
    }
    
    func decrement() {
        i = i - 1
        print("decremented (i)")
    }
}

class TestClass {
    let testActor = TestActor()
    
    func g(x: any TestProtocol) {
        x.increment() // this synchronously calls an isolated method!
        x.decrement()
    }
    
    func f() {
        print("inside f")
        g(x: testActor)
    }
}
  

2

Answers


  1. You can refer to protocols in your code with the concrete type being determined at runtime.

    Consider:

    protocol TestProtocol: {
        func testFunction()
    }
    
    func doSomething(x:TestProtocol) {
        x.testFunction()
    }
    

    The compiler is happy with this because it knows that any object that conforms to TestProtocol will implement testFunction and therefore everything will be fine at runtime.

    But functions in an actor need to be asynchronous, which means that they need to be awaited.

    If I was permitted to define

    actor TestActor: TestProtocol
    

    There would be a problem with doSomething – It isn’t expecting an asynchronous function.

    To prevent this, the compiler will not allow an actor to implement a non-isolated protocol.

    You need your protocol to inherit from Actor:

    Once you do that, the compiler will tell you that testFunction needs to be async

    protocol TestProtocol: Actor {
        func testFunction() async
    }
    
    actor TestClass: TestProtocol {
        func testFunction() async {
            print("Test function")
        }
    }
    

    And now, the compiler knows that invocations of testFunction must be asynchronous:

    func doSomething(x:TestProtocol) {
        Task {
            await x.testFunction()
        }
    }
    
    Login or Signup to reply.
  2. where is the data race in the above code or conformance to protocol in general?

    Normally, if you want to call an actor’s isolated method from a context not isolated to that actor instance, the call must be asynchronous (you need to write await). You are submitting a job to the serial executor, which the serial executor will execute when it’s become free. This is why this is an asynchronous call, and this is how an actor synchronises access to its state.

    Your code, if it were allowed, allows you to call an actor’s method synchronously!

    let instance = TestClass()
    func f() {
        g(x: instance)
    }
    
    func g(x: any TestProtocol) {
        x.testFunction() // this synchronously calls an isolated method!
    }
    

    Now imagine many different threads concurrently calling g(x: instance), and that testFunction mutates some state that should have been protected by the actor. The actor cannot serialise these calls because they are not asynchronously waiting for the serial executor to become free, and you end up with data races.


    You can change the protocol to inherit from Actor:

    protocol TestProtocol: Actor {
        func testFunction()
    }
    

    Now the compiler understands that testFunction must be a function isolated to some actor, so the isolated testFunction in TestClass can satisfy the protocol requirement. Calling it synchronously in g will now produce an error.

    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search