"Actors are reference types, but why classes?"

Then the question would be actor class or class actor - depending on what we think is the main trait here...

If Actor default to inheritance
actor / final actor should be the option

if Actor default to non-inheritance
actor / actor class should be the option

What's the difference between non-inheritable actor, and (inheritable) actor that doesn't have a subclass?

1 Like

Can you elaborate on this? I understand actor initializers will likely have some restrictions when initializing their stored properties, but how would this translate in the need to distinguish between two types of initializer?

I think that there is a meta point here that I tried to express above but didn't explain well. Classes and actors will be thought about and used in a very different way both because of their concurrency modeling properties, but also because of what that means for users. This all follows from the goals of providing actor isolation, but they are things that users will encounter directly.

The general way people will interface with actors is with async methods, meaning that cross-actor references will be all awaited. While this is possible to do with classes, this will be significantly rarer. Furthermore, the actors proposal lays out a number of pretty significant behavioral differences, limitations, and new attributes needed for actors that don't exist for classes. For example, actors behavior when conforming to protocols with sync requirements is something that is very different (and probably needs additional discussion).

Finally, I think it is also worth considering how people will talk about these things, because it is a lens into the shape of the concept in the speakers mind. People will clearly refer to these verbally as "actors", not "actor classes".

To restate the thesis, actors are their own kind of thing. I don't see a rationale to support subclassing - even Konrad observes:

We shouldn't bias the language design based on something we rarely need, and which there is no strong reason to add.

-Chris

8 Likes

For users who already have a class hierarchy that does its own thread-safety, wouldn't moving to actors be the natural evolution of those APIs to enable async / await? For instance, Alamofire's Request class hierarchy largely replicates the same inheritance tree as URLSessionTask but with more state and APIs tailored to each use case. We keep certain properties thread safe manually using a property wrapper, so this structure seems to obviously lend itself to using actors. But if we can't inherit from one root class, we're stuck with one of a variety of suboptimal solutions, like making a separate eraser protocol for each actor to conform to, which then internally uses the common APIs that previously lived in our Request superclass. Essentially, not allowing inheritance will lead to suboptimal overall design (which is why it was invented in the first place).

Secondly, I really disagree with your assertion that most users will think about classes and actors as different things, especially when actors have nearly all of the same properties and behaviors. When introducing the feature, they'll obviously be compared to something that already exists in the language, classes. The link will be made regardless of whether this proposal refers to them that way since that's how the community will explain the feature. If it's intended that they should be thought about differently, it seems like they should be different things, and they really don't seem to be at this point. Put another way, if actors don't have subclasses or are missing other features from classes, they should just be called actor; if they keep the same functionality, they should be actor class.

2 Likes

I really think that this entire thread has conflated the "spelling of actors" with wanting to ban inheritance or not (since all other properties still apply).

I'm pretty sure many of the replies here are because "actor class" reads terribly (imho), so people prefer actor as the spelling (myself included), and are led by this thread to believe that the naming inherently is tied to inheritance or not. However, picking the nicer name imho has nothing to do with the actual concurrency safety or capabilities of such types.

I find it weird for you to be advocating for increasing the effective complexity of the language to its end-users. And I think this is because we perceive the "complexity" differently:

  • perspective A: banning inheritance means people will not need to learn inheritance, until they have to use a class. if they need inheritance of storage in actors, they should compose and write boilerplate to share implementation bits.
  • perspective B: people inevitably will learn about classes, as it's impossible to design some things without them, and not everything is an actor either. Since there is nothing in the actor design that really relies on preventing inheritance (actor from actor), banning it adds "another special case" to the language.

I think each of the viewpoints attempts to "simplify", and to really debate here, we must understand both viewpoints:

  • A (ban subclassing in actors): aims to "simplify" by removing capabilities that exist in other places, but are seen (objectively) as bad and not worth propagating
  • B (allow subclassing in actors, perhaps only spelled as actor class): aims to "simplify" by creating less special cases in the language;

B comes from the perspective of: Since there is nothing inherently in the model or concurrency/isolation safety restrictions that would prevent us from allowing one actor inheriting from another, and there exist some cases where inheritance is generally useful (inheriting storage that may be tricky to set-up etc).

I fully understand and support Swift's push for trying to use Protocols wherever possible, but at the same time, you say yourself:

They're great for most problems, not all problems.

And I find it tricky to draw the line at actors, because, there is nothing in the isolation model that this ban really gains. So we're having a discussion about inheritance itself in what is meant to be a concurrency and isolation focused type.


The way the quote was used here is misrepresenting my point, so I added the missing piece to clarify.

Our disagreement stems from the "how often" and "how painful" (causing people to write much boilerplate code) are we making those some use-cases for our users wanting to adopt actors, and "do we hate inheritance enough, to remove it even though there are no technical reasons, or implementation wins, from doing so".

So really the entire discussion isn't at all about actors:

Which is the frustrating part of this discussion to be honest, since one perspective takes into focus the actor's (the discussed type's) core requirements, guarantees and adoption routes, and the other is really trying to use the fact of introducing these entities as chance to prevent inheritance in this type -- unrelated to it's actual requirements/guarantees. So I find quite hard to lead a constructive discussion here, since the goals and viewpoints are almost parallel to each other.

What I thought was a neat "eat the cake and keep the cake" solution was the proposed:

  • actor spelling as the "actor semantics + final + don't allow inheriting by this actor",
  • and actor class as the "actor semantics + actor class (inheritance)"

which kind of nudges people the right way -- to avoid inheritance, but allows users to rely on the actor class if they have to move to actors from existing codebases, or they are one of those some situations where inherited storage really helps their API.

This spelling also works for future extensions, if we ever did singleton actor, or singleton actor class etc.

7 Likes

I guess this prompts the question of whether we really want our actors to have inheritance, or if there is some other improvement that can be made to the type-system to make inheritance unnecessary for what you are trying to achieve. E.g., in your case, if you could achieve what you wanted cleanly with protocols, would you still want actor inheritance?

I don't know if it's even possible, but given how some people feel that inheritance shouldn't be required, it would be a shame to muddy the waters or add the complexity of inheritance if a future improvement to Swift makes it unnecessary or non idiomatic again.

At the point where I'm able to replicate what I need using protocols (shared state, overridable APIs), they'd essentially just be classes. Any move to a different abstraction would require some kind of restructuring. Inheritance is the obvious solution to the design problem here (which is somewhat constrained by the design of URLSessionTask).

1 Like

Until this discussion I never realised that inheriting from an actor could even pose problems, I always saw it as a class with a queue that prevents race conditions or whatever parallel access problems might occur.

The last few posts here have triggered flashbacks to associatedtypes in protocols. Like make all these nice protocol things but don’t stick it in an array because all hell will break loose. A lot frustration caused because simple swift users like me don’t know or never heard about existential boxes and the language doesn’t help with that.

Please, please don’t do something similar with actors where you spend a hour, a day, a week, ... thinking about a overall system and then have it all blow up because you forget or added open, final, class,... or started/didn’t start with inheritance or whatever consideration that requires knowledge about compilers. Some users took the long scenic route to acquire programming skills. Please don’t make it too hard on them even if that means extra complexity for you the compiler expert.

Edit: with regard to my first sentence. I am not saying actors must have or not have inheritance. Above my pay grade obviously. Realising that you could have reference types without inheritance is my take away of the day.

3 Likes

I agree that many individual posts are focusing on spelling, but that wasn't the intention of the thread. The whitepaper starts from "what are the requirements" and derives a semantic design from that, and spelling follows from that. It does not starting from the existing design and complaining about spelling.

I am doing what I have done for years now, which is to advocate for what I think will make Swift the best language for its users, the community, and the ecosystem for the decades ahead. This is a design process which involves weighing many factors together. It is usually not something that is simple and easy.

In this case, my primary goal is to capture the inherent complexity we are introducing (memory isolation, single threaded domains, communication to them through async messages) in a simple and consistent concept that can be explained and internalized by programmers. The point is to produce a set of abstractions that allow next gen concurrency designs to be simply and efficiently communicated.

I agree that protocols don't solve all problems, but language features are designed to work together. No one language feature ever solves "all problems". In this specific to this case, classes and actors compose nicely so there is no reason for actors to have all the capabilities of classes just because you might want to use implementation inheritance to implement the logic in an actor.

Are you objecting to the discussion (which, in typical forum style, goes in many directions :-), or are you objecting to the whitepaper? The whitepaper does exactly what you are asking for - talking about the core requirements, guarantees, and adoption routes.

I'm not impressed by this proposal personally: if we end up allowing implementation inheritance, we'd already have a way to spell this final actor class. While you could sugar that in a slightly different way as you propose, I'm more concerned with the language model than the spelling at this point.

-Chris

6 Likes

init is actually less restrictive. It is probably the only place you can safely call actor (sync) functions in a sync context.

If my assumption is correct that the actor is a bag of invariace rather than of properties, then direct storage mutation is very unlikely. This results in a few main inits (where all stored properties are available), and other inits need to eventually call one of the main inits. They are designate and convenience, by means of access control (designate inits have access to all variables, convenience inits do not), not DI.

While I'm not sure which side I'm no yet, I don't think designate vs convenience is particularly a problem.

I don’t see this conversation about removing or banning anything. The question is simply: are actors appropriately classes, or are they not? And this is not a question of spelling.

If actors aren’t classes, then there is nothing to remove; rather, the argument is that we shouldn’t add inheritance as a feature unless there is a compelling reason to do so. If actors are classes, then I think there would be broad agreement to keep all the features of classes.

Since it seems you’re of the persuasion that actors are classes—why?

13 Likes

It was actually my suggestion to have both actor and actor class. I'm also on the fence whether it's a good idea.

On one side, I see it as trying to find a middle ground trying to quell an unproductive discussion with something nobody dislike enough to fight against. I say unproductive because all this conceptual bikeshedding distracts from the concurrency aspects of actors. Of course the problem with middle ground solutions is they often add complexity to a design and add little value.

So does it add value to have both actor and actor class?

I see actor without inheritance as its own concept. It's more convenient to to introduce actors to someone without having to talk about classes. That seems to be the basic idea of this pitch: make actor (as a concept) its own thing independent of a class so you don't have to think in term of classes.

And I see actor class as a class and an actor mixed together. So it can do class things and it can also do actor things. People, including me, already model actor-like state isolation using classes, so it's more practical to transition code to the actor model if you don't have to tear apart your class hierarchy.

By making actor a first-class declaration we are making a statement that this is its own thing and that you don't have to know about classes to master it. It becomes easy to discuss it without mixing it with class stuff. And actor class becomes the hybrid that can be useful to port exiting actor-like classes to true actors. That's the concession to practicality.

Maybe this distinction does not add much value. Technically it doesn't change anything whether it's spelled final actor class or just actor. But I think it sends a different signal to humans who make a mental model in their head.

actor class in a definition like actor class BankAccount seems redundant. How am I supposed to read or teach this? "BankAccount is a limited reference type with state isolation that is also a reference type"?

actor class also might lead to confusion. Can I rearrange the order of actor and class? What's the difference? Why can't I write actor struct since I can give structs reference semantics with its properties? Why can't I write actor protocol to define a protocol that can only be conformed to by concrete actor types?

8 Likes

In addition to encouraging a particular mental model, it also changes the 'path of least resistance' to one which disallows inheritance. If we're correct in assuming that use cases for actor inheritance will be rare (or if we decide that we want to discourage actor inheritance), then the least we could do is make the lighter spelling be the one which does not enable inheritance. This is also what I was getting at with my suggestion of actor class vs. open actor class.

2 Likes

It would be nice to have a small example to demonstrate what kind of feature that must require subclassing in actor, for people like me have not much experience with actor.

I feel actors behave so differently from normal classes when reading the actor proposal.
I think It's better to define them as different nominal types like enum and struct.

3 Likes

I've got a decent 20 year background writing code, and this will be my first experience with actors as well. Based on everything I've been reading and (only somewhat) following along with so far, the takeaway I'm getting is that approaching them from the POV of "these are like special classes" would be wrong, and a "better" POV would be something more like "these are a concurrency primitive".

If the expectation is that teachers will be teaching actors as "these are a special kind of class" then it makes sense for an actor to be ... a special kind of class.

If that's not what teachers should be teaching, or if it's the "wrong way" to think about the problem, then to me that implies they should be a new nominal type. (and based on my limited understanding of everything, I think this is where I'm ending up; please correct me if I'm wrong)

10 Likes

I think an important question is when one wants to expand an actor island, do they extend or compose it:

// Extend       Compose
*---------*    *-------*
| New     |    | Old 1 |
|         |    *-------*
| *-----* |        | 
| | Old | |    *-------*
| *-----* |    | Old 2 |
*---------*    *-------*

If we want to extend it, we probably need to keep inheritance around.

1 Like

I’m curious. Given the choice between protocol oriented programming and inheritance, why would someone choose inheritance for actor types?