"Actors are reference types, but why classes?"

Personally I like the idea of actors as reference types but not as classes. I've long felt that classes are a bit anachronistic in Swift, and they almost feel like an unnecessary holdover from Objective-C. Actually having lighter-weight reference types in general without all the baggage of classes is something I have wished for in the past.

On this point:

we could also consider eliminating “static” stored properties (both var and let) from actors as well

I'm not sure if I like removing static constants since I find them to be a useful tool in a lot of situations. I can imagine them being replaced by static functions, and it seems out of step with the general design decisions of Swift to require me to express something as a function rather than a property.

Maybe static properties on actors could be sugar for some kind of synchronized property wrapper? Or else the the "static" scope for an actor type could itself be an actor somehow?

3 Likes

I bolded "stored" to make this more clear, but I think it is a bit funny that so much e-ink has been spilled over this, this really isn't the core point to the proposal :slight_smile:, and is phrased in a "we could consider" sort of way already.

I don't understand this goal for two reasons:

  1. Doug's post started by defining that "actors are classes so of course they should support subclassing" (my paraphrase). The whole point of this proposal is that actors aren't actually classes, nor are they direct standins for them. While other languages/system typically implement actors with classes (yes, I read your sidenote, thx :), Swift is a different language with an intentionally strong culture around using protocols instead of OO class hierarchies.

  2. Your point about "moving to actors from classes" is more compelling for sure, but there is no smooth migration here. Actors are a "different thing" than classes already - they will be different type names, will not be subclassing existing OO stuff, are pervasively async, and will require significantly different patterns. Also, it isn't the case that all existing OO concurrency patterns involve deep subclassing.

To me, this is exactly the same situation as when we introduced Swift 1.0 and people were asking for subclassing for structs because C++ had it. We should only add subclassing to Actors in Swift if there is a first principles reason to do so, not just because Objective-C and Scala over emphasize OO patterns.

Yes, I agree with you on the implementation details, and the ability to do smart/tricky things with compiler/runtime support to optimize common patterns is a really killer feature of language-based actors. However, I think that many programmers (particularly early on) will think about actors in terms of queues, since that is what they are familiar with. I don't think it is a harmful mental model, even if it isn't fully accurate.

-Chris

6 Likes

This is only true if you have OO subclassing situations already. Why do you think that concurrency equates so strongly to OO subclassing?

I didn't phrase my response earlier very well, and on reflection it looks like I worded it overly strong. I'm sorry for that, I didn't mean to get overly excited, I'll try to keep tone in better check in the future.

The thing I'm observing is that you seem to have a default mental association of "actors are a kind of class" that pervades your writing. Consider your post that I was reacting to:

You are defining actors as classes by saying "actor (class)", but they aren't definitionally classes, this is something we get to decide. They are only definitionally "a reference semantic thing", which is what the whitepaper tries to point out. We get to choose both 1) their behavior/capabilities, and 2) how we explain them to Swift programmers.

What I'm observing is that there is no inherent reason for actors to provide subclassing capabilities. If that is removed, then actors are clearly not classes. In my opinion, this is a beneficial thing, because they are already "not like classes" in other ways, and this divide makes the language behavior more clear and allows us to (continue) pushing programmers away from OO and subclassing as the preferred way to model their apps.

Your points are all objections to protocol oriented programming in general. As you mention, we already have reason to make protocols more powerful, further reducing the relative power and importance of classes.

Again, I can understand where you're coming from here, but this explanation only makes sense if you have already decided that "actors are classes". If you break that association, then the explanation above doesn't make as much sense (to me at least).

-Chris

8 Likes

Let's simplify the problem a little bit, according to summarized discussions upthread what do you think if the definition of actor is

Actor := Class + Queue - Inheritance - Static store props [+/- something else...]

is actor still a class?

If NOT, why not treat actor as actor - a new stuff (nominal type)?

Just like enum and struct in swift, both of them are value types, but enum has some special restrictions and capabilities compared to struct; and we don't need enum struct to declare an enum type, the same logic applies to actor, we shouldn't need actor class to declare a special class type actor.

7 Likes

I wasn't saying I had anything better, just that starting at classes and working backwards is potentially more confusing than beginning with actors as their own thing. How about "actors provide isolated, mutable state through asynchronous messaging, and have reference semantics".

This table should be heavily caveated, is very much in the 'ideal' case, and may be missing things but IMHO shows that, at first glance from someone using Swift's point of view, Actors are very much their own thing.

structclassactor
inheritancenoyesno
identitynoyesyes
reference semanticsnoyesyes
dynamic dispatchnoyesno
unit of concurrencynonoyes
isolationyesnoyes
static storageyesyesno
16 Likes

Your proposal's definition of actors overlaps almost completely with classes:

  • Reference semantics, identity, they’d work with the === and !== operators.
  • Common features of nominal types: init, methods, properties, subscripts, extensions, access control, etc.
  • Protocol conformance, including AnyObject
  • ARC, weak/unowned pointers, and deinit
  • Potentially implicit memberwise initializers like structs.
  • Bridging to Objective-C objects.

There is one necessary difference from classes, which is that actors protect their state.

When you have that much overlap, there is absolutely no way to avoid a comparison to classes. And when you compare to classes, there is one difference: the lack of inheritance.

There is no technical reason why actors can't have inheritance:

  • There is no semantic conflict between actors' data isolation and inheritance. How inheritance interacts with data isolation is well-understand and already documented, so you lose nothing (e.g., in expressiveness) be having inheritance.
  • There is almost no implementation cost to supporting inheritance (it falls out from the existing implementation already, and the semantic rules are both known and implemented).
  • There is almost no implementation benefit from not supporting inheritance (the compiler/ABI are already optimized well because Swift's defaults are so static anyway, so any gains from removing inheritance will be small and require more effort than is worthwhile).

The choice here is purely philosophical, and has nothing to do with concurrency. So, are we going to use the concurrency effort as our way to push the Swift community strongly toward Protocol-Oriented patterns and enshrine that in the language, even though there is no technical reason to do so?

@Slava_Pestov made an amusing analogy here. We could just as well ban the use of fileprivate with actors. We're not thrilled with that feature anyway, and you can always work around it if we take it away, so why not use this "break" (where folks will be writing new code) to take away something we don't think folks will need?

Just to reiterate, the massive overlap of actor capabilities with class capabilities makes the comparison unavoidable regardless of one's predispositions. The argument that "if you just try not to think of them as classes, then you'll never expect inheritance" doesn't work, because that overlap forces the issue.

Doug

13 Likes

If we are going to allow all features of classes with actors; how do actors nested in other actors work w.r.t their data isolation? Does the outer most actor "own" the nested actors? or do they act completely in-depended (not allowed to capture each others data etc.)

actor class SomeActor { 
   actor class AnotherActor {}  
}

Each actor is an independent entity and does not share state with anyone else, including such "nested" actor definitions.

They are independent and may execute independently/concurrently after all;

4 Likes

Hi Doug, do you mean Actor should preserve all class features plus state isolation; that's also the reason why you prefer actor class instead of separate actor to declare an actor? .. Because it's a pure class+ stuff
Actor := Class + State isolation

I prefer having actors as new nominal types.

In addition to what everyone up-thread has pointed out, I think there is also a problem with what the "default" is, if actor classes are preferred over standalone actors.

Currently we have final class, because classes by default allow inheritance. If someone wants to disable inheritance for some classes, they must use the final keyword to override the default behaviour.

Classes by default are thread-unsafe. If actors are classes, then those who want thread-safety have to write actor class to override the default unsafe behaviour. In my opinion, this contradicts Swift's "safe by default" design principle.

Even if actor class is sugared to just actor, conceptually it still gives users the notion that the default is unsafe.

4 Likes

I agree, Doug. The arguments made against actors-as-classes in the proposal are dogmatic, not pragmatic.

1 Like

This also overlaps with structs and enums, so I'm not sure how that help make your case. The only difference is reference vs inline semantics (and that they bridge to C structs instead of ObjC classes).

That's the point: Swift has strong continuity across nominal types. I'm agreeing with you that Actors are clearly nominal types with reference semantics, but I'm arguing that there is nothing that says they are classes. In my opinion, making them classes is worse for the language than making them a new nominal type.

I see what you're saying here, but I still find that you are defining the outcome as part of the rationale, particularly when the majority of the continuity is with all other nominal types (structs enums protocols) and not just classes.

If I were to take the same approach from the opposite side to illustrate what I mean, I would argue that "Actors protect their state and do not support subclassing, so they are clearly not like classes".

Whereas your explanation presupposes that inheritance is part of the inherent design of actors and thus actors are close to classes, this statement presupposes that inheritance is not part of the design of actors, leading to a conclusion that they are very unlike classes and should be their own nominal type.

This is why I'm trying to frame this about the inherent aspect of the design of actors instead of coming from an already biased perspective.

I completely agree. I am arguing that we don't want actors to have subclassing for a few reasons:

  • because people will think about and design around Actors and Classes in very different ways in practice, they are a completely different granularity of modeling power.
  • because the exposed API to actors will be pervasively async, not sync forcing awaits at the boundaries.
  • because actors have a large number of restrictions (e.g. related to sync protocol requirements) that classes don't have.
  • because we already have this modeling power elsewhere in the language.
  • because Swift in general pushes people away from classic OOP design patterns.
  • because we have protocols as a better solution for most problems
  • because we don't have any compatibility issues to deal with that "strongly encourage" OOP patterns.

However, I completely agree that we could support subclassing of actors. If we were to go with such a design, I agree that it would be reasonable to call them "actor classes". I just think that is a worse design than making them their own kind of nominal type.

For what its worth, while I expected that implementation cost would be part of the discussion, I really don't think this should be at the forefront of a discussion of what the right thing for the language is. I think you'd agree that the implementation cost is modest regardless of which way we go, so we should stick to "what is the best for Swift programmers and applications over the next couple decades of use".

I agree it has nothing to do with concurrency, this is a standard language design question.

Swift 1.0 and 2.0 pushed people towards Protocol Oriented patterns and enshrined that in the language, even though there was "no technical reason" to do so. We did it because we thought it would lead to a progression in the quality of Swift code compared to other languages and systems. While POP has been over used in some cases, I believe that this decision has stood the test of time well.

As such, I think it is clearly beneficial to maintain consistency with the struct and enum design here - I don't see a reason to back away from protocols, treating the "next nominal type" differently.

-Chris

9 Likes

I haven’t decided exactly which side of this discussion I fall on, but I think it’s also worth noting that it’s ‘easy’ to tack on actor subclassing at a later date if it turns out to be sorely missed, whereas taking away actor subclassing if it’s deemed to be a mistake will be difficult/impossible. (Apologies if this was already pointed out :slightly_smiling_face:)

Another ‘compromise’ position could be to have actor class denote a non-subclassable type by default, with open being explicitly required to subclass an actor (even internally).

6 Likes

I apologize that it's taken a while to craft a reply to this, but I think there is a very important point to address here and I wanted to take the time to get it right.

The argument to which you've replied isn't about how to explain what an actor is (i.e., the first step in teaching users about actors), and certainly not specifically to users who don’t know what a class is. Rather, the question I have raised is whether it makes sense that, in order to master the use of types that protect their data from races, one must also master Swift’s rules about designated initializers and convenience initializers. I contend that it does not, because there is nothing inherent to actors that demands the incorporation of such concepts. I'm pretty happy with what I've said on the issue already, so I'll leave it at that.

I do want to address the scenario that you've brought up here. It seems you dismiss it out of hand, so let me say emphatically that I think we absolutely do want to consider (and place great weight on the consideration of) how actors will be learned by the arbitrarily many users who will have absolutely no idea what a class is. Not only is this not an absurd notion, I would like to make it a reality. Furthermore, I think that this line of thinking will be essential to incorporate for every feature we add to the language in the future:

There is a powerful idea coined by a 19th century biologist (it isn't exactly right, but as a framework on which to hang our thoughts it is very useful): ontogeny recapitulates phylogeny. The classical example brought up for this idea is that human embryos develop gill slits and tails; these are resorbed later in development. The presence of these developmental stages (ontogeny) gives us hints as to our evolutionary ancestors (phylogeny). As a biological principle, it is meant to be descriptive, of course, and not prescriptive.

I bring this up because this concept bears some resemblance to the argument you bring up here. The great majority of us, having come to Swift after experiencing other languages, went through a phase in which we learned about classes; the rest of us, being already Swift users, have learned classes because we already use Swift and it already has classes. Therefore, since actors aren't part of the language yet, all current users must necessarily know how to use classes and not know how to use actors. It is convenient, then, to piggyback on those existing skills when introducing new features.

But, if we are successful in accomplishing one of the tentpole goals of making Swift an excellent first language--or even if we are not, but we succeed in creating a great model for concurrency that is then emulated in other languages that are excellent first languages--then piggybacking on classes for the design of actors will mean that all future users will need to learn classes first before learning about actors, simply because it is the order in which we have added those features to Swift.

As a general principle, I disagree that fixing the order in which users must learn the language based on the order in which language features are introduced is pedagogically optimal. Indeed, if there is no fundamental reason why a user has to learn about classes before they can master actors, then we do not need to saddle them with this evolutionary history. It would be much better, rather, that the arbitrarily many future users who come to the language with neither knowledge of actors nor of classes (that is, every new programmer) can learn these concepts separately and at their own pace. With time, it may even turn out that it makes sense to think of actors as the paradigm of a reference type, with classes being "like actors but without protection from data races." We need not foreclose these possibilities, and I think we should not, simply as a consequence of the evolutionary history of Swift.

27 Likes

This seems to align well with the progressive disclosure principle, too: the programmer progress in detail from actors towards classes.

1 Like

I apologize if I mischaracterize anything here, but it seems like folks agree that a desirable actor implementation will be impossible without all of the things that are currently unique to classes in Swift with the exception of subclassing semantics, meaning I don't see any argument that we absolutely cannot have actors without subclassing. From here, folks split into two camps:

  1. Because actors will need all of the other stuff that is currently unique to classes, it makes sense to throw in subclassing as well.
  2. Because all of the other things are necessary and subclassing is not, we shouldn't default to including subclassing.

From here, I can see why it might be difficult to get to an agreement: sometimes it is nice to get things for free, other times it is inconvenient when something you don't want is bundled with things you do. There are also cognitive overheads on both sides: if your reason mental model of classes is "reference types with inheritance", then it is simpler to think of actors as "reference types with state isolation". If you think about classes as a single concept, there is cognitive overhead to think of actors as "classes, but without inheritance". I suspect the growing ranks of Swift users will have individuals from either camp.

To take a different approach, I'm curious how folks feel about this sentiment:

Personally, I would support using actors as a way to nudge folks toward using protocols over inheritance (i.e. from a "purely philosophical" perspective). I don't think Swift has yet jumped the shark with protocols the way Java has with OO, and I haven't yet seen someone make the argument that pushing folks more towards protocol-oriented patterns is a bad thing.

This may have been mentioned somewhat in jest, but I don't think this analogy holds up. There are many more thorny questions with disabling fileprivate for actors (would files where actors are defined disallow fileprivate anywhere in the file? or only for the actor itself? or for actor and any nested types? what would the behavior be for extensions of actors?). On the other hand, subclassing seems significantly more straightforward, actors either allow it or they don't (and folks seem to agree the implementation overhead of either approach would be roughly equal). If it just so happened that folks weren't thrilled about fileprivate and disabling fileprivate for actors was fairly simple and fit in the model, I think we should absolutely have that discussion.

2 Likes

This might sound like a strange suggestion, but we could have both actor and actor class:

  • Use actor when you don't want to allow subclasses, and then you don't have to care about the complex initializer rules.
  • Use actor class when you want to allow subclasses.

Most people will pick the simpler wording actor and will live happily with that. With this scheme actor acts pretty much like final actor class.

7 Likes

Actors don't just protect the data inside. They also protect the invariance of the data. We likely won't be allowing actors to be arbitrarily initialized even if we expose all of the stored properties (like struct). So we'll end up with certain notion of designated and convenience initializer anyway.

Can subclassing of actors be done safely?

  • Do we allow classes to inherit only from classes, and actors only from actors? Or allow cross-inheritance?
  • If we can mix, and we have only one actor in the hierarchy, does the actor's state protection extend to all the classes' sub-object's stored properties? If yes, even for the classes that are base-ward from the actor type? If there is no protection for the classes' sub-objects, would that indirectly violate the actor sub-object's integrity?
  • If there are at least two actor type in-line in the hierarchy, do they share the same protection factor? If not, how does synchronization between the factors work?

That's pretty good actually, isn't it? I'd +1 that.

From my experience inheritance is rarely needed with actors, but it happens. Outright banning it is pretty arbitrary–and unrelated to concurrency discussions really–for a language that simply has it and there's no getting away from that fact. There are useful applications if used sparingly as well, whenever storage is needed.

I like the spelling actor being the usual case, and actor class opening up the class inheritance feature, as opt-in. It also explains what we're opting-in to pretty well.

I like this opt-in approach since it makes it more of a "nice nudge", rather than slapping people with ridig opinions which are not really represented in other large parts of the language, and causing people problems adopting the actor style of modeling your apps.

5 Likes