"Actors are reference types, but why classes?"

Hi all,

One hotly debated thing in the recent actor proposal is whether it should be actor class or actor. I think there is a more fundamental question, which is basically "why should actors support subclassing?"

I wrote up some thoughts on this in this whitepaper: Actors are reference types, but why classes?

I'd appreciate thoughts and comments. Thanks! :turkey:

-Chris

38 Likes

I agree with this direction. I’d like clarification on a couple points though. Would actors conform to AnyObject and would they support static constants?

2 Likes

Both points are debatable, and I don't have a strong opinion here. My loosely held opinion is:

IMO Yes. There is nothing about AnyObject that implies subclassing. Actors are "objects", have the same runtime header, etc.

IMO no. I'm writing a doc exploring global state, but a let constant containing a pointer to a class instance cannot be transparently shared across actors. You could make a complicated model here were you only support let constants of actor sendable types (or something) but it is better to foist that complexity onto global lets/vars, which we have to solve anyway.

I incorporated both of these clarifications into the doc, thanks Matthew!

-Chris

1 Like

I agreed to promote actor class -> actor - a special version of nominal type - even a new semantic explained in below.

Furthermore, I'm also curious that whether it is possible to transform @uiactor @globalActor into actor(ui) actor(global) form, so that actor is just a sugar of actor(local). If possible, we actually already make a new semantic called actor - a container which has a built-in lock-free queue (direct or indirect) to take/isolate async operations, which come across both value/reference semantic boundary.

If all of above were well defined, we could write code like this.

actor X {} //means actor(local)

actor(global) Y 
{ 
    static var shared : X = ...
}
@Y //actor(global)
func asyncFunc() async { ...}

actor(ui) Z {} // @uiactor class Z execute on main UI actor 

@UI //actor(ui) func
func UIFunc() async {}

2 Likes

I'm supportive of not having actor subclassing, but I'm also weary of the no-static-properties rule as I use the somewhat frequently to configure protocol conformances. Would we be able to do the following in this proposal?

protocol Foo {
  static var foo: Self { get }
}
actor bar<T: Foo> {
  var foo: Foo { T.foo }
}

If this is valid, I don't really see a huge benefit to outlawing static constants in actors. If it isn't valid I think we lose quite a bit of generic expressiveness.

3 Likes

What about actor final class? Require all user declared actors to be final but allow the compiler to bless general actors like UI/main actor, or background actor to be defined which users can subclass. Otherwise if the compiler provided let’s say a UI/main actor but not able to subclass, how do we use that actor? Composition?

I imagine we could still impose limitations to an actor class like not allowing static/member.

A nominal actor would be sugar to something close to @noStaticMembers actor final class if we had the concept of @noStaticMembers.

If we ever allow functions to be declared as nominal types, I would like to see them declared as classes with a marker protocol and not it’s own type of reference.

My 2 cents.

1 Like

Just to clarify, but I'm only suggesting that we should prohibit stored static properties in actors. I don't see a problem with supporting computed ones. Computed static members are effectively global functions. There is a similar thing going on with "class members" where you can have computed properties but not stored class properties at the moment.

-Chris

I'm still not sure I follow the rationale between disallowing stored static properties. My point was more that if you want to disallow:

actor Foo {
  static let foo = 42
}

but allow:

protocol Foo {
  static var foo: Int { get }
}
enum Bar: Foo {
  static let foo = 42
}
actor Baz<T: Foo> {
  static var foo: Int { T.foo }
}
let baz = Baz<Bar>()

It may prevent a few obvious bugs but I don't think you'd be achieving that much, and you may force a number of valid use cases to use the more convoluted structure in the second example.

4 Likes

I personally am very pleased by this proposal.

I currently use struct/enum + protocol to describe the universe of possible messages, and final class + protocol to describe the context in which these messages are sent around. It seems clear to me thinking about it now that I can and should reimplement all of my final class types as actor types, because I think that's what they've been in spirit this whole time, though I didn't have a formalized understanding of what an actor was so I couldn't articulate it as such.

protocol having already provided the truly Swifty solution to polymorphism, conceptualizing actor as the truly Swifty solution to reference semantics would finally allow me to respectfully shelve class as a wisened and appreciated elder that hails from a time before such elegantly orthogonal solutions for each domain had been developed. I would get to reach for actor as my default reference type, rather than needing to throw the inheritance straight jacket on class every time.

Referring to actors as "the truly Swifty solution to reference semantics" I suppose could be seen as a bit of a leap - after all, actors bring with them all sorts of new concepts that on the surface don't seem to have to do with "reference semantics" per say - but I think it's possible it could be argued (going out on a limb here) that the entire asynchrony/concurrency system is actually being built in conceptual support of reference-semantics-done-the-right-way. It is only in a world where things have identity that you can know where to return to after you've suspended (asynchrony) and it's only in the world where things have identity that you have to worry about race conditions (concurrency). Maybe it could be argued that classes are simultaneously polymorphism-done-not-quite-the-right-way (due to single-inheritance restriction, among other things) and reference-semantics-done-not-quite-the-right-way (due to thread-non-safety).

I see class in Swift as a somewhat muddy, legacy concept which I therefore treat with caution and use in tightly prescribed ways. Therefore I currently feel very positively toward the idea of letting actor have a clean conceptual slate, rather than making it stand on the shoulders of class.

I'll be very curious to see what the dissenters' point of view is however, because I do remember reading recently that changing actor class to actor was specifically not a change that was likely to be made. I wonder if that stance had more to do with the confusing elision of words where conceptually they were more correct as they were - whereas what you're proposing here is not to "change actor class to actor" but rather to change the concept of actors fundamentally enough that proper keyword becomes actor as a by-product.

3 Likes

Ah, very good point, I see what you're saying. I'll soften the wording on this and say it is something that could be considered, but not make that core to the proposal. The major issue I want to pick with is subclassing, and you're right that this is quite debatable on both sides. Thanks George!

This is an interesting idea, but be careful with this - the key defining concept of actors is that they isolate their state and are communicated with async message sends. There are definitely valid use cases for normal final classes. One basic example would be a binary tree node.

Yeah I get what you mean, but I personally wouldn't go that far though. I agree that classes and implementation inheritance have been vastly overused since the 80's when they came in vogue, and there are specific frameworks out there that go crazy with OO design patterns (some Java frameworks come to mind). That said, classes and inheritance do serve an important purpose, it is just much smaller than what they've been historically (ab)used for.

Also, it is important to consider that actors and classes are very different creatures and should be thought about differently - this is the key observation of the proposal - even though they're both reference semantic. It wouldn't be appropriate to blindly change all classes to actors.

When the implementation is available and solid, you'll discover this quickly, as the core interface to an actor will require you to await and async a lot. This back pressure is a good thing, and should work the same way that throws and try discourage one from just marking everything as throwing.

-Chris

8 Likes

Perhaps this is the logical next step of the discussion then - to get clear on the legitimate use cases for class inheritance, where it's more than just a mistaken attempt at using a protocol. If that were clearly delineated then I think it would be easier to see whether or not it's any real loss to not have inheritance with actors.

3 Likes

I think the point of Chris is to reverse the burden of proof. It is not necessary to prove that inheritance can be avoided in order to move forward with a simplified actor model. Instead, people that need actor inheritance have to convince that it is missing, in a classic evolution process (amend this pitch, or in the future bring a new pitch, implementation, proposal).

13 Likes

Rereading Chris' proposal, trying to distill my understanding of the issue down to a few sentences:

  • The primary need is for the user to be able to mark some of their nominal types in a special way so that the compiler knows to statically enforce thread-safety for the state of those types (actors).
  • Actors must be reference types for the thread-safety enforcement to make sense.
  • Right now, classes are the only nominal reference type in Swift.
  • Of course, this circumstantial fact does not necessarily mean that actors should be modeled as a special case of classes. Classes and actors can have "nominal reference type" as their shared conceptual parent while the two themselves are simply siblings.
  • What it does happen to mean is that certain aspects of "nominal reference type" are currently hard-coded into the compiler definition of a class. Chris gives deinit as well as weak and unowned references as examples of things which would need to be abstracted to accommodate a new nominal reference type.
  • There is nothing about "nominal reference type" which implies the need for inheritance, so if actors and classes were siblings under the concept of "nominal reference type" then inheritance would remain a class-specific feature.
  • Theoretically ? this is the safer approach because if at some point in the future inheritance for actors were desired we could simply factor out the concept of "heritable reference type" from classes and bestow heritability upon actors later on, still leaving the two as siblings?
2 Likes

Question though - I don't mean to potentially pollute the thread with undereducated questions but, are we sure that this is true?

Is it possible that an actor struct would just be a degenerate case where there is no thread-safety enforcement to be done? In which case "actor" would be an attribute that is orthogonal to the reference/value dichotomy?

Of course you would not actually use actor struct even if what I'm saying makes sense, but it's pertinent to the question of this thread of how we should conceptualize actors.

1 Like

On this proposal, can arbitrary extensions be made on actor types?

Yes, this is a good way to look at it. Unless there is a clear and compelling reason to allow subclassing, we shouldn't.

I don't know what an "actor struct" would be useful. structs can be copied around, and each copy would have to create its own queue. One could define such a model I guess, but I don't see how it would be practical or useful to solve problems.

Yep, just like any other nominal type.

-Chris

2 Likes
  • Designated / Convenience initializers

I don’t get how these, more so than any other initialiser, becomes more complex for actor types. Maybe I’m dense but could you please go into a little bit more detail?

I’ve wondered what the runtime reflection story is for actor types. Should actors types avoid this altogether? How does the requirements for actor runtime reflection fit in here?

2 Likes

Can we be sure that actors will never need inheritance?

1 Like

The difference between designated and convenience initialization is only relevant when you have inheritance. This is why structs and enums just have a single simple init.

They provably never "need" them, if anything, inheritance is a convenience to avoid having to define both an actor and a class together. However, if it ever became warranted in the future, we could always expand the model later to add inheritance.

-Chris

Conceptually I do believe that actors and classes should be separate and thus actors should be declared as 'actor' and not 'actor class'; however, I don't agree that inheritance, method overriding, designated / convenience initializers, casting, dynamic dispatch should be unavailable for actors simply because it is believed by some that we won't want or need them. There should be a strong technical reason that any individual feature should not persist in the actor specification and each of them should be evaluated for that. For example, if it would be technically problematic and in-fact dangerous to thread safety for property getters and setters to be overridden in actors as they are in classes, then they should not be included. My Thesis: Actors should be distinct reference type entities with most (if not all) the same features of classes.

Why not support subclassing?
In this section Chris refers to a quote that seems to equate subclassing with 'increased complexity', there is no evidence for this -- or the point is just not very well made; in fact I think the concepts of classes and inheritance are so well established and understood (easy to learn and teach) that 'increased complexity' should be applied to more convoluted features like generics (which are increasingly complex, difficult to teach, understand and hard to explain when analyzing a type in a simple View in SwiftUI, for example). Many times a class only overrides one or two methods in a class that contains hundreds of other methods that are not overridden -- that is hardly complex. From the standpoint of the compiler, maybe, but from the programmers perspective they are simple concepts.

In the section " Reusing classes reduces the implementation cost of the compiler", Chris makes a great point, but what he says in What are Actors? What do they require? is more important as Actors require a different model for getting and setting state in such a way that is safe; in the beginning I think that it would SEEM easy to hack actors on the code that has already been established for classes only for us to realize that it needs to be completely stripped out, duplicated and separated (in the code). That is my feeling, and may prove to be incorrect. Basically: Actors can't be a hack job on classes.

Simply put, I would like to subclass my actors. I don't prefer the dogma of 'structs+protocols+generics' as being preferred and I really don't want to be cornered into that strict paradigm. The Swift text books can suggest that not subclassing actors is preferred, but it should be available. There is no safety in the suggestion "if we decide later that we need it, we will add it." If that is the case, then just hack the features onto classes.

I really want to be able to use Swift to develop replacements for all my web/server side software. I am heavily invested in where this all goes.

4 Likes