"Actors are reference types, but why classes?"

The best option for me would be to make actors and classes closed by default and use open only when referring to inheritance (for classes and actors) and leave access control to the private, public and internal keywords. If you want to allow inheritance only inside a module, for example, you could do public internal(open) actor. I haven't put enough thought on changing the current inheritance syntax for classes, but at the top of my head I can't think of any reason why the compiler wouldn't be able to automate the whole migration. Surely there will be a lot of people reminding me of other things I haven't thought of.

The Swift Evolution proposal for the design of open for classes has long ago been concluded. These alternatives have been considered and rejected. It is not going to be revisited and it’s not germane to the topic here.

The question is whether it’s appropriate to consider actors to be classes. If so, then it stands to reason that they will support subclassing, and they will do so with the same syntax as any other class.

2 Likes

I Imagined that would be the case. I still hold my position that actors are not classes and we should not make them behave the same way classes do for historical reasons.

1 Like

The idea is that actors subclass actors and classes subclass non actor classes. Originally the proposition was that a class that was marked as an actor (e.g 'actor class Name {}') could only subclass other classes marked as an actor in order to maintain thread safety.

We could add a "nonfinal" modifier, to indicate that the declared type may be inherited from. The default would be nonfinal for class and final for actors, needing the other modifier to override the default. (Should repeating the default, nonfinal class and final actor be a warning?) If an actor has a base actor type, but no finality listed, would it be final by default like base-level actors? Or would the nonfinal status of its base be viral?

Can an actor have another actor as a (stored instance-level) property? If so, would the two actors have independent synchronization protection mechanisms? Or would the inner actor reuse the same protector as the outer actor? If the latter, does it actually apply to all actor instances that are contained, at some level, within the same top-level object? (Note that the top-level object may be an actor too.) But, on the other hand, what happens with two actors contained in the same value-type instance? We could mix-and-match, and even have different sharing combinations (struct instance A contains actors #1 and #2, but instance B contains actors #3 and #1 in the corresponding spots instead), so "share the same top-level object" may be amibigous.

They are independent.

Doug

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

I believe there are use-cases for sync methods and sync protocol implemented by an actor.

The first think that comes to mind is a reactive API using Combine or ReactiveSwift. It seems relevant to me that an actor would synchronously return a Publisher.

It seems also relevant to me that an actor would implement CustomStringConvertible and CustomDebugStringConvertible, synchronously returning a string summarising a snapshot of its current state.

Most code in a program is actor-independent, because it doesn't run on any specific actor. It's a natural consequence of the design. Even code that resides lexically within an actor method can be actor-independent, e.g., if it's in a local function or closure:

actor class A {
  private var count: Int = 0

  func f() {
    let fn = { self.count = self.count + 1 }  // error: closure is actor-independent, so it cannot access count
  }
}

So the concept is there whether you give it a name or not, and we've found it useful to give it a name.

The limitations on @actorIndependent fall out of the safety model. They aren't arbitrary.

I thought we had addressed this already, but IIRC your argument was that a let being accessible from outside an actor's isolated context violated API evolution because you couldn't later change it to a computed var. That is technically incorrect---one can change it to a computed var that is @actorIndependent.

I spent a bit of yesterday porting an app over to the concurrency model, and it was very useful. Swift programmers use more immutability that I think you're giving them credit for, and having the ability to refer to a let member of an actor was quite useful. @actorIndependent on synchronous functions is also useful as a transitional aid, so I can provide a completion-handler version of a newly-async'ified function. For example, I found that I could make the best progress by taking a completion-handler function like this:

func existingFunc(completionHandler: @escaping (String) -> Void) {
  // body of function that does something concurrently and then calls the completion handler
}

and make it async:

func existingFunc() async -> String {
  // body of function that does something concurrently and then calls the completion handler
}

It's great, but then I have to update all of the callers... and the caller's callers... and so on. I found it extremely useful to be able to provide the old signature, which wraps this one:

@actorIsolated  // because existingFunc is on a class that's become an actor
func existingFunc(completionHandler: @escaping (String) -> Void) {
  Task.runDetached {
    completionHandler(await self.existingFunc())
  }
}

The first statement is incorrect, but also irrelevant. The ActorSendable notion fits into this scheme fine: a let of an ActorSendable type can be accessed from anywhere, a let of some non-sendable type cannot.

Your statement that this is "not very useful" is in direct conflict with my experience of porting real code to the proposed concurrency model. Let's understand @actorIndependent before dismissing it. I do understand that the proposal needs to describe it better than it does.

I guess it's my turn not to understand. If generic code needs to have something like async attached to various conformances or generic parameters, as in your example here...

Then we are getting re-use of protocols but not any generic code that uses those protocols, including (e.g.) members of protocol extensions.

To be clear #1, is #2 + the ability to use @actorIndependent to make a synchronous actor function satisfy a synchronous protocol requirement. The existingFunction(completionHandler:) example above extends directly to this use case, where you have a protocol with completion-handler-based asynchrony and you want to write new actor code to conform to it. Some day maybe that protocol can become async all the way (if you can update all of your clients simultaneously), but @actorIndependent lets you bridge the gap.

The rules around async/sync and @actorIndependent are data-race-safe within the realm of value-semantic types, and your ActorSendable approach looks promising as a way to improve it. This is why I started by asking for a specific example that you feel breaks data-race safety, because I'm fairly certain we need that made concrete to find the disconnect.

Doug

2 Likes