[Pitch #4] Actors

For those who might feel slightly overwhelmed by the discussion here and would like to either brush up or wisen up what actors are all about I would recommend the following article and the video at the end of the article in particular.

4 Likes

Even more great updates, nice work!

Cool. Is there a specific preference for the name Actor? It seems more natural to follow the established precedent of AnyClass "The protocol to which all class types implicitly conform.", AnyHashable "A type-erased hashable value." etc, and the Any protocol composition. The word Any is the unifying term for a "type erased thingy" in Swift regardless of whether that type erased thingy is a protocol (as in AnyClass), a structure or protocol composition. I think that aligning with AnyClass is itself enough reason to name it AnyActor.

Other random comments:

  • I still don't understand why we'd introduce language complexity for nonisolated(unsafe) proactively, particularly when this can be handled in the library. It seems better to exclude this from the first proposal and add it when other parts of the model get baked out.
  • Don't feel compelled to include the "Isolated or sync actor types" discussion in the alternative considered section, I am not at all advocating that.
  • I see your subsequent post (and respond below) but I still consider it a showstopper if actors can't interact with the protocol and generics subsystem in a way that composes with implementation mixins, standard protocol oriented programming, and other things. Breaking this unifying theory across the language will manifest it significant complexity over the evolution of Swift and force programmers into weird design patterns (e.g. putting all their logic into a struct and then having a boilerplate actor wrapper that forwards the public API). We should fix the fundamental issues here, I'm happy to see you are continuing to explore this.

I disagree with your assessment. It isn't clear how much of the motivation for nonisolated would be satisfied with other facilities that we need to bake out anyway for basic protocol support, and by library support for breaking out of isolation in both safe and unsafe ways. None of this has been baked out, so a claim that nonisolated is required doesn't seem supported.

Furthermore, defaulting protocol conformance syntax to nonisolated conformances seems very wrong to me. Actors are all about isolation: you should have to opt in to non-isolation (e.g. actor X : nonisolated Hashable or whatever). For this reason, I don't agree that the nonisolated proposal as written will allow the correct thing to be added later.

Without it, it would be completely obvious that the proposal is insufficient for isolation, which is what actors are all about. You'd be proposing a nominal type with no support for protocols with sync requirements, making it obvious that the sync implementation logic of actors cannot be abstracted and genericized.

First, we already have a lot of proposals so one more isn't actually that onerous. Second, isolated self does go with the base actor proposal since it is fundamental to the nature of self in an actor and has nothing to do with the nonisolated proposal. This is a point of confusion that would be immediately more clear if you split out the nonisolated proposal. This is what I mean about the nonisolated proposal confusing the core model.

If you are serious about that, then please split out the nonisolated concepts to its own follow-on proposal and we can evaluate the "isolated" features of actors without it, then evaluate the nonisolated features and the details of the additions based on its own motivation. This splits the motivation sections for the two proposals, since they really have two different things going on.


It is perhaps the wrong time to discuss this, but I think that the word isolated for "actor self" is confusing regardless of what happens with nonisolated. The thing we want to convey here is that a function has "direct" access (as opposed to async/mailbox access) to the actor its argument is bound to -- we're not saying that the actor /type/ is nonisolated.

I'm really not sure what the right spelling of this is though, you could imagine several yucky options like:

  • func f(a: direct YourActor)
  • func f(a: nonmailbox YourActor)
  • func f(a: within YourActor)

etc. nonisolated conveys that you've got an actor that doesn't obey the isolation rules, which is not what is going on. Does anyone else have a good suggestion here?


Thank you :raised_hands:

Sounds good. Yes, DataProcessible is an example that is using protocol extensions to share implementation logic across actors and other types. This is one of the key things that draft 5 is lacking.

Correct, I was intentionally trying to narrow the conversation in the P0 thing that I feel need to be covered, excluding 'nice to have' things that should fall out but don't "need to".

The thought process in the whitepaper is that protocols allow sharing logic and abstracting across enums/structs, as well as across enums/structs/classes in some more narrow cases. It would be odd if protocols couldn't do that at least across reference types like class/actor. That said, to reiterate, this is lower priority than allowing actors with isolated sync methods to conform to protocols that have sync requirements. I agree that actors are different in that the "client interface" and "internal interface" side of actors are unique to them.

Yes, this is covering the major case I care about, this is a very promising direction to explore. Does this work with existentials? What happens when you have hierarchies like:

protocol P {...}
protocol Q : P, Actor {...}

?

How do associated types work?

How does this work when you want to abstract across the "external view" of an actor which is all async?

Depending on the answers above, it might be useful to think about this direction as a "different kind of protocol" which would be more naturally spelled like actor protocol P { or @actor protocol P {. The concern here is that "actor" is too broad of a word for this property though, since it speaks to whatever the internal implementation details of an actor are, not all actors uniformly. I suspect that this concept will be more closely aligned with the 'self' keyword, e.g. nonmailbox protocol P{ or whatever.

Right. This seems expendable if we get the basic abstraction functionality in place for actors. It comes at the cost of forking the protocol world, but that is better than not supporting this at all.

Yep, this is going in the right direction, thank you for exploring this!

-Chris

Others requested it, so I'll keep it.

AnyObject is the more direct comparison here, although it's interesting because it's not actually a protocol and cannot (for example) be extended. Most of the Any-prefixed types are type-erasing wrappers rather than protocols, and generally only exist because the type system has failed to make the protocol useful as an existential. For example, with Sequence and AnySequence, Sequence is the protocol and AnySequence is the type-erased wrapper. For actors, Actor is the protocol and we shouldn't need an AnyActor to go with it. That's why I prefer Actor, but it's-just-a-name.

I disagree with this. Isolated conformances differ from every other kind of conformance in the language in that they only work for specific values of the conforming type. All of the other ways in which actors conform to protocols under the proposal---via nonisolated, via async requirements, via Actor protocols---work the same way as all other conformances in Swift, on every instance of the type.

That's helpful, because the Actor protocols having actor-isolated requirements is orders of magnitude less complex than any proposed solution for having actors conforming to the same synchronous protocols as structs and enums.

Sure. This falls out of the model:

func f(a: DataProcessible, b: isolated DataProcessible) async {
  await a.compressData()  // must be async
  b.compressData() // sync is okay
}

because actor isolation is determined by the requirement declaration.

The members of Q are actor-isolated, the members of P are not. To expand out your example:

protocol P {
  func f()
}

protocol Q: P, Actor {
  func g()
} 

actor MyActor: Q {
  func f() { } // error: requirement is non-isolated, so can't use an actor-isolated function here
  func g() { } // okay: requirement is actor-isolated to `self` and so are we
}

They're not really special. They can conform to protocols that involve Actor, or not. You'd need to deal with Sendable on them for cross-actor references, though:

protocol R: Actor {
  associatedtype Element
  func get() -> Element
}

func testR<T: R>(t: T) async {
  await t.get() // error: T.Element does not conform to Sendable
}

Doug

1 Like

Hey all, I've rolled changes from this round of discussion into pitch #6.

Doug

5 Likes