"Actors are reference types, but why classes?"

Inheritance can work in some case, but you're right that it doesn't scale.

This discussion has given me an idea of how we could allow some form of composition with synchronous access for actors. But it's a bit out of scope for this thread. So I wrote a pitch: Interlocking in a Hierarchy of Actors

With regard to actors and inheritance it might be an idea to have a look at the Akka actor framework (akka.io). It is probably the most widely used (distributed) actor framework even before Erlang, I would say. It is for the JVM and written in Scala.

Scala with traits sort of supports multiple inheritance. So if the Akka people thought there is any point for actors to be able to inherit it can probably be seen from what they have done. Akka exists for years and is industry-proven.

I would take Akka with a piece of salt, though. IMHO some things with regard to actors were overdone in the same "spirit" things IMHO were overdone on Scala.

I would much prefer that you provide an example that you believe to be unsound, and I can use that as a way to teach interested folks how the actor proposal maintains soundness. That will help me understand what in the proposal didn't come across well (or if there actually is a hole in the soundness of the model). It wouldn't make a lot of sense to write a white paper based on a misunderstanding of the proposed model.

Doug

By catching up on the discussion, I see the following arguments.

1 - Actors are reference types, not classes. Therefore, they should be treated as their own thing.
2 - Swift should encourage Protocol-Oriented Programming over Object-Oriented Programming.
3 - Subclassing actors is valuable, sometimes, so Swift should allow it.

I agree with all of them and they're all reconcilable. Here are my conclusions from the arguments above.

1 - The syntax for actors should be just actor.
2 - By default, the actor syntax does not afford inheritance.
3 - One can allow subclassing of an actor with the syntax open actor.

This approach is also compatible with progressive disclosure, which I personally also value a lot.

8 Likes

Without commenting on the whole actor-topic, imho this detail is a bad idea: It takes a word that is already established in a similiar context (a good thing), but changes its meaning (not so good).

6 Likes

I see it as more appropriate than how class inheritance works today. Continuing on the topic of progressive disclosure I think it would make sense to lay out the basic Swift types in the following order

  • Value Types
    • struct
    • enum
  • Reference Types
    • final class
    • actor
  • Inheritable Reference Types
    • internal class and external open class
    • open actor

Although this order is what makes more sense, given the complexity of the types and their capabilities, I agree that the disparity between actors, closed by default, and classes, open by default, is weird. I also think it's weird that internally classes are open by default and across modules they're not. Classes, as they stand right now, are inconsistent with Swift's principles of best practices by default. I don't think we should make actors as inconsistent as classes in favor of consistency. I think we should fix classes, a topic for another thread, though.

In an ideal scenario classes should also be closed by default and then we would have the following:

  • Value Types
    • struct
    • enum
  • Reference Types
    • class
    • actor
  • Inheritable Reference Types
    • open class
    • open actor

While this would be feasible is another story, but I think we should at least consider it. I don't think actors need to suffer because of classes. We can do actors the best way and improve classes, if possible.

2 Likes

Can you please clarify how would the meaning change? Maybe I'm missing something.

open, for classes, means code outside the declaring module can subclass the class. You are proposing that open for actors would apply even to code within the same module.

Yes! I also mentioned that, IMO, this behavior should apply to classes. I don't see how the fundamental meaning is different though.

-- edit

By the way, Swift already uses open with a different fundamental meaning for enums.

That is what open would mean:

type other module same module
class public & and subclassing possible no difference
actor ??? subclassing (or maybe "subactoring? :rofl:) allowed

??? could either be more restrictions than for classes (would you have "open open actor"?), or it would take away the option for inheritance only in the same module — which, btw, I consider to be an acceptable compromise that is here to stay.

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
Terms of Service

Privacy Policy

Cookie Policy