Agreed, actor should have subclass/inheritance feature if we define it as a special class/reference type, a class without subclass capability looks so weird for developers.
If developer want actor without subclass for performance reason support final actor should be an option.
tl;dr: the only thing that makes an actor (class) different from a class is that an actor (class) protects its state from data races, so they should be exactly that: nothing less, nothing more.
The proposal states that actors are fundamentally different from classes with this reasoning:
âActor classesâ (as proposed) are a fundamentally different type from classes already: Actor classes cannot subclass normal classes, and normal classes cannot subclass actor classes.
The subclassing rule is not some fundamental difference in types; it is a direct consequence of protecting actor state. If an actor class were to inherit from a non-actor class, it would mean that the whole of the actor class's state has not been protected against data races, because one could easily have data races through the superclass. That would undercut the actor model.
The other direction has the same issue: a non-actor class C inheriting from an actor class A could introduce data races in any instance members it adds, meaning that one could no longer rely on data-race freedom when working with values of (static) type A. That, too, would undercut the actor model.
But the subclassing rule need not be so strict. For example, we could allow an actor class to inherit from a non-actor class if that non-actor class promised to prevent data races via some other mechanism, e.g., by promising that all its state was manually synchronized via something like @actorIndependent(unsafe). Indeed, this is why the NSObject carve-out is legitimate: NSObject has no instance members, so there's nothing to data-race on.
So, this subclassing restriction isn't a rigid dividing line, it's a consequence of the one thing actor classes do differently from classes, which is to protect their state from data races.
Abstractly, I can understand the desire to eliminate subclassing. Lots of complexities in the Swift language come with subclassing, particularly around initializers and protocol conformances. It's temping to cut those inconvenient parts out of actors, because it makes actors simpler---but it doesn't actually manifest in any actual improvements for actors. It also doesn't make Swift simpler, overall, because classes and subclassing don't go away. Rather, we've only made it harder to move classes over to actor classes because we've made some arbitrary restrictions on actors because we decided we don't like certain features. I feel like the suggestion to not have subclassing is guided more by what @austintatious calls the "'structs+protocols+generics'" than by its benefits to actors themselves.
Eliminating support for static stored properties is the other restriction mentioned. Yes, static stored properties aren't part of the actor's isolated state, so we could remove the notion entirely---regardless of whether actors are classes or not----but I don't see the point: there are ways to make them useful (with @actorIndependent or global actors or whatever).
I'm going to pointedly ignore the section about implementation cost in the compiler, because it's not informing the decision here and is more likely to confuse than illuminate.
Instead, we should talk about overall language complexity. Having actors be a small "delta" on top of classes makes them easier to teach and learn. "An actor class is a class that protects its state from data races" sums up the feature. Where actor classes behave differently from classes---disallowing access to stored instance properties except on self, disallowing subclassing with non-actor classes, disallowing synchronous calls to instance methods, etc.---one can explain all these as consequences of that one initial definition.
I can see us having the debate about the spelling of actor or actor class, but making actors a new, distinct nominal type would add complexity, confusion, and unnecessary barriers to the language.
Iâm with Chris on this one. Subclassing doesnât seem to be something intrinsic to classes, rather something that happens to be associated with classes because we recently happened to put subclassing and reference types in the same bucket. (In C++, structs can participate in inheritance too; Fun!)
The mental model I use to think about this is copy-on-write. COW is used pervasively by structs throughout the standard library, but we donât need any special machinery in order to make this work. We just wrap a class in a struct and add a little bit of isKnownUniquelyReferenced sea salt and the whole thing tastes great . I think actors can operate in the same manner, if they need subclassing semantics, they simply wrap a class and everything works great! On the other hand, if you are defining an actor that doesnât need subclassing semantics, you can keep things simple (and you might even get a little bit of added spice in the form of an automatically synthesized internal initializer!).
I think @austintatious is right about one thing: subclassing isnât rocket science (it is easy to learn and teach)! But that same logic can be used for null pointers: surely we can teach someone that a pointer could either be valid, or null (and I hope weâve all internalized the benefit of not having null pointers ). These things only become a problem when you consider them holistically with a multitude of other âfeaturesâ. I like the simplicity of âI want to isolated stateâ meaning I use an actor, and âI want subclassingâ meaning I a class, and âI want bothâ meaning I use an actor, wrapping a class (Maybe I feel the slight sting of an extra indirection, but we all have to make sacrifices).
By that definition a class that only contains immutable members or synchronizes all its accesses using a mutex is an actor class. But thatâs not exactly the concept of actor that is currently floating around. Actor is about protecting from data races by providing an asynchronous execution context for serializing access, as opposed to a blocking context (mutex) or no need for serialization (immutability).
That said, I can appreciate the idea of folding them all in one concept since from the userâs point of view theyâre not all that different. Actors methods are asynchronous, but methods can be asynchronous even on non-actor classes. And actors can have synchronous methods too. From outside the point of view, an actor looks like a final class with a lot of asynchronous methods; whether the class is actually an actor or not is almost (if not entirely) an implementation detail.
And if being implemented as an actor is an implementation detail, then you can strip that label from public interfaces (replaced by final class) and it probably should remain actor class internally (rather than just actor) to mirror final class.
Thanks @Douglas_Gregor, that's a great writeup! I didn't want to misrepresent our collective thoughts on this here so was waiting it out a bit...
I'll just +1 a few crucial pieces:
Static stored properties
I initially read the this writeup to want to ban any static properties which would have been terrible. After re-reading it again I realized that you only mean stored static properties which I guess I don't care about much personally...
For the sake of future outlook, since I have prepared this bit already, in general static (computed or not) properties are tremendously useful for things like shown below, where we can use static properties to configure a library or runtime using actors:
actor Worker { }
extension Worker: ActorMetrics {
static let metricGroup: MetricGroup = "\(App.name).workers" // and other configuration
}
// protocol ActorMetrics: Actor {
// @actorIndependent static var metricGroup: MetricGroup { get }
// }
I re-read the writeup again and realized it only speaks about stored static properties which I guess I don't really care about, but to @Douglas_Gregor's point it makes the language definitely more complex... suddenly in my "basically a class" type I'd get restrictions which have nothing to do with actor state safety? That feels quite weird and arbitrary to me...
Subclassing
I truly "get" the let's-avoid-subclassing stance, but this does not seem like the right place to cut it off.
Doug captured the points for allowing subclassing well I think: It's not that we think it's awesome and great, but the similarity in teaching, "moving to actors from classes" is simply a huge win. There's enough roadblocks we throw at people already when adopting concurrency, why add more somewhat unrelated ones. (you can easily make your class hierarchy into an actor class hierarchy after all, if you have some BaseWorker which has some connections and stuff â why make it harder for people to adopt the right patterns?).
Realistically: I worked on porting an non trivial existing iOS app codebase to actors, and have some other projects using them: inheritance didn't really show up there. Just as inheritance does not show up with classes in Swift all that much â simply because we strongly discourage it. And that should be the same here. No need to conflate the two, IMHO.
I'm also worried about this constant Swift completely locking down everything such that it becomes impossible to experiment much in library land without being a huge pain to use: Say you'd want to provide a "function actor as a service" library. Perhaps similar to Azure's Durable Functions (link or different, but in reality there will have to be some place to put storage into. which are actors really), you really will want some storage in such base actor to implement persistence piece of the puzzle. It is very natural to offer users "extend DurableActor and you'll get some additional durability tools".
Perhaps here we'd get away here with compiler magic for "proxy actors," that have some "special storage field," but it's really quite frustrating that any attempt to experiment with cool libraries ends up having to much in the compiler.
Sidenote: Akka is _not_ about classes
I don't want to go into too much detail, but do feel it is necessary to call out, since this writeup calls out Akka actors as "they're classes," which vastly understates how that actor system and types work â and makes it seem like what Swift Actors are proposing is the same while it's completely different.
Firstly, Typed actors are not defined by extending any Actor type -- they are (potentially even pure) functions returning Behavior. The old style API indeed is about extends Actor however it's not at all focused around the fact that they're classes -- the actual behavior of an actor is the receive function, so again -- functions are the core of it.
Syntax / Spelling
I'd truly love to spell it as actor even if it really is actor class underneath. I would not really care to for it to be a new nominal type, that makes it harder to explain and makes it harder to migrate to from classes.
Primary reason being mostly fatigue from typing tons of words before getting to the body of the actor, say:
public final distributed? actor class Worker {}
public final distributed? actor Worker {}
is really a lot of keywords... I guess it's still a lot without the class, but feels a bit nicer visually.
I think if we had actor object for "true singleton" then actor class / actor object would kind of make more sense, but I don't think such thing is even remotely on the timeline... And even if it was, a better spelling would probably be singleton actor and actor anyway -- so no need to stick to the actor something spelling anyway
That is a bit wording nitpicking but let's follow up on statement since that's not really what the proposals are saying:
Nothing in the actor model nor Swift's specific take on actors claims that the access is specifically protected by queue (dispatch queue or any other mailbox/queue). It only phrases it as "an actor is an exclusive execution context", how it achieves that can vary. Want to implement an actor's enqueue() with locking and synchronously running the task? Weird but okey, you're welcome to do so.
In reality this doesn't really show up in that form. What does show up is for example wanting to execute actors on a limited number of threads. Say maybe exactly 1 thread, or maybe even execute them all synchronously in tests, or what actually is done a lot in the real world: put some specific actors onto specific thread pools e.g. because they're known to call into blocking code and we want to isolate this blocking onto a specific pool).
This is completely separate from the actor's structure (concurrency is not parallelism after all), and really is just runtime configuration. So in other words, how the actor achieves its promised guarantee really can vary, and the model does not promise anything about the "how".
This kind of wrapping involves a ton of boilerplate. You mentioned copy-on-write types as prior art here, but I see that more as a cautionary tale: thereâs a lot of boilerplate that you have to get right, and one missed copy-on-mutation makes for annoying bugs
Manually writing type-erasure wrappers is similar. Sure, you can do it (and people do all the time), but itâs a pile of boilerplate and the result is less efficient than of the language supported you.
Your entire argument here is that if we take subclassing away from actors, one can emulate it again. But still, thereâs no argument that actors would be demonstrably better without subclassing. It all seems like an attempt to sideline an existing, widely-used feature because we donât like it as much as protocols.
I did not exclude weird implementations like locking. Whatâs really weird about it is that itâd force those locking/blocking calls to be async and be awaited needlessly, which I would result in a rather inconvenient API surface. Same for immutable data: all these async methods could run synchronously with no locks and no blocking, but would need to be async still.
My point was that the too general definition of actor as âa class that protects its data from racesâ actually isnât fit for other techniques if it imposes an async requirement. Relying on async is really a specific way to protect from data races. It might be worth having a âprotected from data racesâ flag in the public facing API so you know objects of that class can be safely passed across threads, but it shouldnât force every call to be awaited on.
I feel like you've taken my original statement far too literally. "An actor class is a class that protects its data by serializing access to its stored instance members" is more precise and side-steps all of this.
I'm sympathetic to most of @Chris_Lattner3's concerns, but they seem largely orthogonal to the issues related to actors.
There's a plausible argument to be made that Swift needs---for general use---a simple, predictable, efficient, named reference type that doesn't end up as the butt of endless "Which method gets called?" puzzles on Twitter.
But...doesn't Swift already have exactly this in the form of final class, or more specifically, final class Foo { ... }? The only distinction I can see between Chris's list of desiderata for actors (no method overriding/dispatching, simplified init, no up or downcasting, static dispatch, no shared data) and the actual behavior of noninheriting final classes is that final class permits static data.
If it were decided to impose a no-inheritance rule on actors, I would think it'd be a lot clearer to just say "Actors are class instances, but all actor classes must be final, and they cannot have static data or inherit from other classes" than to explain exactly what it means for actors to exist in a completely different world from classes. Actors being their own type is inherently sort of puzzling, especially if they behave just like final classes.
While you state this as fact, your opinion is one reasonable design, and it isn't the only reasonable design: why is your opinion the best? Why should actors support subclassing just because they are reference semantic?
Enums happen to be value semantic, but have a design conceived from first principles independent of structs, shouldn't actors merit the same affordance?
Right, but I think you're missing the observation here. I understand that you've intentionally carved actors away from classes as a "different thing" that doesn't interoperate with them. This underscores my point that programmers should think about and design towards them being different concepts.
My question is: since they are a different thing, why should they behave the same and have the same affordances?
Classes were constrained by a wide range of concerns that don't affect actors, including a VERY long list of Objective-C framework compatibility issues that drove a lot of complexity into initialization semantics and many other parts of the system
Actors are a new thing that isn't precedented by ObjC. While it is useful for actors to bridge over to ObjC classes in a predictable way, but (unlike classes) there are exactly zero existing legacy ObjC class frameworks to be compatible with, so the class constraints don't apply.
As others have mentioned upthread, the complexity of subclassing for actors should be justified from first principles, based on what the enable. I don't think the "they are reference types therefore they should inherit all the problems of classes" necessarily carries over.
After all, functions are reference types as well and don't get the problems of classes, why should the /third/ reference type get them? (*)
-Cirhs
(*) And here I'm ignoring indirect enums which live in yet another half way zone between all the things that shows that reftypes != inheritance.
You are correct about the boilerplate, but Iâm not sure wrapping a class to get subclassing semantics creates as much opportunity for errors as trying to implement copy-on-write with a struct (in fact Iâm hard-pressed to think of a good example of what could go wrong there, though this is probably related to why code I write is so buggy). Personally, I'd happily take the tradeoff of having a simpler primitive but requiring a bit more boilerplate in the advanced case.
You're misinterpreting my point: the proposal simplifies the rule by separating actor class and non-actor class hierarchies. You're interpreting that as evidence that "these two things must be completely different," when it is not: it's a simplification of the more precise rule I stated.
Subclassing introduces complexity, but beyond the inheritance of initializers, you really can't blame Objective-C for any of it. The argument that "we don't have Objective-C so we don't need subclassing" doesn't follow.
The reasons for subclassing are identical for actors and classes. It provides implementation inheritance and shared storage for subclasses (something people ask for protocols to do). Subclassing provides more efficient dynamic subtyping checks (as? to a subclass will always be much faster than as? to a protocol) and cheaper existentials (one pointer vs. 1+number of protocols pointers).
And existing class hierarchies that make use of these features can migrate over to actor classes if actor classes support subclassing. Take away subclassing and you're forcing folks to rewrite more code to adopt concurrency. Have we improved their experience, or have we thrust our own opinions on them?
They get all of the problems of shared mutable state. They don't run into any of the other issues with subclassing because there aren't meaningful dynamic subtyping relationships, nor do they conform to protocols.
Those are value types with a different storage mechanism, not reference types.
I really, really do understand why folks don't like subclassing. I wouldn't choose to have it in a language, but it's there now, and it will not be going away. It solves real problems for Swift developers, and they use it. And if you don't like it, it doesn't hurt you: final takes it away and you've lost nothing, and for the most part you don't even need to write final for subclassing to be irrelevant if you don't use it.
I think every new concept in Swift (or any language) should be designed by finding a balance between how the language is actually used by the various parts of the community, and where we (the community and the core team, with more weight on the latter) want the language to be headed towards.
Considering just the former would produce a bloated language with an identity crisis; considering just the latter would alienate some productive users and discard some valid patterns.
People like me, that think class inheritance is a terrible idea at any level and only ever write final classes when needing reference types, really don't have problems (usually) with "interface inheritance" - I gladly design protocol hierarchies - or "storage inheritance" - I'd gladly have inheritance in structs -; the problem is in the override keyword: it's overriding stuff and calling super that constitutes the real can of worms that I avoid.
So why not allow for actors inheritance without allowing for override and super? I suspect that it would simplify actors handling at the compiler level, while also retaining some of the niceties of inheritance.
It seems to me as though backing this observation up empirically would go a long way to cutting through this disagreement. A couple motivating examples where subclassing of actors makes a desirable improvement to the code and a concrete example or two of current class code that would be a good candidate to move to actors but would be hampered by a lack of inheritance.
In general Iâm sympathetic to the idea that any new feature should be proposed in its most limited implementation and the onus should be on those pitching it to explicitly justify features on top of that.
Forgive me, but it seems thereâs some circularity to the argument here.
Why conceptualize actors as classes with only this one difference? Because it allows users to understand and use every other aspect of classes, including subclassing, on actors.
Why allow subclassing and every other aspect of classes to be available for actors? Because it allows them to differ from classes only in one thing.
It would seem to me axiomatically true that a explaining actors in terms of classes brings more complexity and a higher barrier to mastery than not explaining actors in terms of classes. While some users work extensively with class hierarchies in Swift, others do not.
I would be lying if I pretended not to review convenience/designated initialization rules every so often when questions arise here about classes. It would be unfortunate if using one of the major tentpole features of Swift concurrency required this sort of review: there is nothing about protecting state from data races that is conceptually built on that knowledge.
It's always a bit more confusing to me to describe something by starting with something else and then taking things away, so saying Actors are 'its a class, but you cant do these things' seems wrong. Doesn't it also sort of goes against Swift's progressive disclosure, because you would have to understand a bunch of things it can't do to understand what it is?
How they are actually implemented is beyond me, but in terms of presentation, the differences in usage, capabilities and semantics justifies them being their own distinct type, IMHO, and described as such, rather than 'classes, but...', especially given that so many of the disallowed capabilities are things that spring to mind when you about class vs struct.
This assumes that there's some large set of Swift programmers out there that know literally nothing about classes, for whom "this is a different kind of type, with reference semantics, that protects access to its data and meets AnyObject requirements" is an easier way to understand actor classes than "it's a class that protects its data from races".
I suspect that giving the first description to the vast majority of Swift programmers will be met with "so... it's a class, then?" and you'll fall back to the second description. And then you have to answer the follow-up question "... so if it's like a class, but why doesn't it have subclassing?"
Personally, the only answer I could give based on this proposal and discussion is "well, there's no technical reason to ban subclassing, but we felt that you shouldn't need this feature with actors."
Do you have a better answer?
I'll give you two. The first is to implement a different queuing strategy:
actor class MyDifferentlyQueuedActor {
private var queue = MyDifferentQueue()
func enqueue(partialTask: PartialAsyncTask) { ... }
}
You need instance data (for the queue) and you need to implement enqueue(partialTask:). Any actor class that wants to use this queue would subclass to get the behavior automatically:
class MyActor: MyDifferentlyQueuedActor { ... }
If you try to do this with a protocol you'll need to make queue a requirement of the protocol, and define it in every subclass. This is boilerplate that also has performance implications.
Beyond that, even the most trivial textbook OO examples can make just as much sense with actors. If I have
class Person {
var name: String
var birthdate: Date
// lots of other attributes
}
class Employee: Person {
var badgeNumber: Int
}
These can be actor classes. Why should we ban that?
I wonder how this discussion would have gone if final was the default for classes and we had something like a (pardon the naming) nonfinal designation which would be semantically equivalent to the current default. Would it be too weird to flip this for actors? So an actor Foo cannot be subclassed but a nonfinal actor Foo could be subclassed in the current modules and an open actor Foo can be subclassed by dependent modules. This could even be done in two phases: first support actor Foo and then add nonfinal actor Foo and open actor Foo in a subsequent pitch.
Well, OK, let's hear it - what is your elevator pitch that explains to the world exactly what new-type actors are? You can leave out all the concurrency management stuff; that wouldn't change.
Just for reference, my (class-based) version was "Actors are class instances, but all actor classes must be final , and they cannot have static data or inherit from other classes."