"Actors are reference types, but why classes?"

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?
1 Like

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?

1 Like

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.

3 Likes

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.

Doug

15 Likes

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 :slight_smile:. 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 :slight_smile:). 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).

5 Likes

If it's really so hard to add actor a new nominal type, how about just treat actor as an alias actor class.

1 Like

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.

1 Like

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 :slight_smile:


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".

4 Likes

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.

Doug

7 Likes

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.

This is what @Chris_Lattner3 is specifically addressing with protocol-based actor isolation.

Doug

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.

1 Like
Terms of Service

Privacy Policy

Cookie Policy