swiftlang / swift

The Swift Programming Language
https://swift.org
Apache License 2.0
67.59k stars 10.36k forks source link

`@MainActor class` is not `protocol Actor` #62632

Open inamiy opened 1 year ago

inamiy commented 1 year ago

Description

protocol P: Actor { // This doesn't work for @MainActor
    func hello()
}

// ERROR: Non-actor type 'Main' cannot conform to the 'Actor' protocol
@MainActor class Main: P {
    func hello() {}
}

or more shortly:

// ERROR: Non-actor type 'Main' cannot conform to the 'Actor' protocol
@MainActor class Main: Actor {}

Expected behavior

@MainActor class (or any global-actor-ed class) should be treated as Actor.

Side note: Although we can use async-ed protocol version as below, we still want to allow protocol P: Actor style too.

protocol P { // This works as alternative for now
    func hello() async
}

@MainActor class Main: P {
    func hello() {}
}

Environment

Additional context https://twitter.com/inamiy/status/1603652396294410240

ktoso commented 1 year ago

I'd ague this is correct; the Actor protocol specifically is designed to NOT be implemented by anything else than implicitly by an actor declaration. Allowing this would muddy the waters quite a bit and I don't think "just" allowing this would even be correct -- there is no synthesized unowned executor in such global actor conforming class type etc.

inamiy commented 1 year ago

To share some more context, I wanted to abstract @MainActor class MyService with protocol Service: Actor so that caller can only reference any Service without knowing its impl is running on MainActor or just a plain actor.

Of course this problem can be solved by having a different protocol without conforming to Actor and instead let all methods turn into async, but I personally find this a bit too abstract.

For example:

protocol Service: Actor {
    var state: State { get }
    func fetch() async // this async comes from network
}

will be turned into async methods if not conforming to Actor:

protocol Service {
    var state: State { get async } // this async comes from actor 
    func fetch() async // this async comes from both network & actor 
}

So, asynchrony from both network & (local) actor are merged as a single async in the latter, which makes readability a bit obscure and too abstract (which can also be used for non-actors as well).

@ktoso If I understand correctly, a class with @globalActor can be considered as referencing globalActor's executor?

@MainActor class MyService { ... }

// We can assume this is synthesized
extension MyService: Actor {
    nonisolated var unownedExecutor: UnownedSerialExecutor {
        MainActor.shared.unownedExecutor
    }
}