"Actors are reference types, but why classes?"

Sure, I mentioned this on the original actors thread, but there are multiple things that are going on in that pitch that are difficult to parse out.

The proposal as originally written apparently aims to allow actors to conform to protocols with sync requirements, e.g. CustomStringConvertible as described. In order to do this, it introduces the @actorIndependent attribute.

I have several concerns with this. One is that "@actorIndependent on an actor method" doesn't really make sense conceptually: why would an actor have methods that are independent of it? How does this intersect with static requirements? Why can't static methods in classes fulfill instance requirements? What is the subtyping relationship here?.

The second is that there are a number of limitations that are imposed on @actorIndependent requirements, and new and unprecedented semantics implied to let properties to enable this model that are concerning (as described on other threads) because they violate the resilience and API evolution goals Swift has.

Furthermore, the programming model provided isn't very expressive because the only sync requirements that can be safely satisfied are ones in which all "sync state" is set up at initialization time - this isn't very useful. Furthermore, this model was proposed without the goal of memory safety - considering something like ActorSendable in the model would further constrain the expressible designs that you could achieve with such a model.

I only see a few sound models that we can provide here:

  1. Provide a limited (but not very useful) model as described. The cost of this is language complexity (the @actorIndependent attribute), implementation burden to check these invariants, and conceptual overhead because the set of things that are allowed by this @actorIndependent is a bit weird. It is still not clear to me how @actorIndependent intersects with the global variable model given that that is another open question in the original proposal.

  2. Prevent actors from implementing sync requirements. This is consistent with the "actors are always async" nature of them, and is simple to implement, but this prevents actors from playing with the sync world at all. It seems unfortunate to duplicate many sync concepts into the async world, but I think we're doomed to that anyway in some cases (see AsyncSequence for example) and some things (e.g. Equatable) can't be implemented on actors in any safe way anyway (because you can't have two actors "locked" at the same time, a major issue with Self requirements). If we pursue this path, this would be accepting the fact that actors are a completely different thing that structs and classes because they live in an "async only" world.

  3. We could allow sync protocol requirements to be satisfied by actors directly, but then extend the protocol model to allow P and actor P in the type system. A conversion from "Actor to existential P" and "actor to generic type constrained by P" would maintain the async-ness of the bound existential and generic argument, so you could do something like this:

protocol MySyncThing { func syncRequirement() }

func doIt<async T1: MySyncThing, T2: MySyncThing>(a: T1, b: T2) async {
  await a.syncRequirement() // await required here because T1 is actor qualified
  b.syncRequirement() // no await, this is obviously a sync requirement.
}

this could be used like this:

actor MyActor : MySyncThing { func syncRequirement() {} }

...
{
  let someActor = MyActor()
  doIt(a: someActor, b: someSyncThing) // ok!
  doIt(a: someSyncThing, b: someSyncThing) // ok, sync things convert to async things!
  doIt(a: someSyncThing, b: someActor) // error: cannot pass async actor as sync generic requirement
}

This is a generalization of the "we need to reason about actor hops" design point.

#2 is the simplest model, but I lean slightly towards thinking that #3 could be worth it. However, there are obvious type system complexity issues to deal with here. For example, I don't think actors can safely be used in sync requirements with Self requirements, and this would make addition of new sync requirements to formally async-only protocols be complicated.

In any case, I would love to see alternative design points explored. I think we should aim for a memory safe model with "actors 1.0" and the async/sync gap is an important leg on this stool.

-Chris

2 Likes