SE-0316 (second review): Global Actors

Several thoughts after reading the proposal:

  • We may need a formal definition of the nonisolated modifier. Previously this modifier was only used to annotate properties and methods of an actor type, but since we use this modifier in global actor isolated types, I think we should define its semantics and usage in a formal way.

  • From the semantics perspective, the global actor attribute is used to annotate a function/property/type so that it's isolated to that global actor. For more generality (at least from my view), have the authors consider using @isolated(SomeGlobalActor) instead of @SomeGlobalActor?
    With this approach we can gain several advantages:

    1. @isolated(SomeGlobalActor) express the isolation-to-global-actor idea more clearly (although with several more characters).

    2. When we use words like isolated and nonisolated consistently and frequently, it would be easier to educate and learn from the isolation model in the whole community.

    3. We can express generics over global actors more elegantly. In the current proposal we have this:

      @T
      class X<T: GlobalActor> {
        func f() { ... } // constrained to the global actor T
      }
      

      The biggest issue I see here is that from the first line of code, we can't decide whether T is an attribute name or generics parameter. Its ambiguity can only be resolved when we read the code after that, which means the generics parameter definition goes after its first usage. If we choose the @isolated(SomeGlobalActor) approach, we can write like this:

      @isolated<T: GlobalActor>(T)
      class X<T> {
        func f() { ... } // constrained to the global actor T
      }
      

      IMO this reads much smoother and aligns with style of generics property wrappers.

  • The author should explain in more detail the related behaviors about the implicit conversion from global-actor-qualified functions to async functions. It says

    the async function will first "hop" to the global actor before executing its body

    So from this sentence, I guess that the compiler implicitly creates a new async function that wraps the original one. But what if we call the converted function on the same global actor? For instance, see the code below:

    @MainActor
    func foo(_ closure: () async -> Void) async {
      await closure()
    }
    
    let callback: @MainActor () -> Void = ...
    let callbackAsynchly: () async -> Void = callback
    foo(callbackAsynchly)
    

    Will there be additional hops when we execute closure inside foo? Will this behavior decided by the corresponding global actor type? I hope these questions could be answered.

2 Likes