The review of SE-0306: Actors begins now and runs through March 29th, 2021.
This review is part of the large concurrency feature, which we are reviewing in several parts. While we've tried to make it independent of other concurrency proposals that have not yet been reviewed, it may have some dependencies that we've failed to eliminate. Please do your best to review it on its own merits, while still understanding its relationship to the larger feature. You may also want to raise interactions with previous already-accepted proposals ā that might lead to opening up a separate thread of further discussion to keep the review focused.
Reviews are an important part of the Swift evolution process. All review feedback should be either on this forum thread or, if you would like to keep your feedback private, directly to the review manager. If you do email me directly, please put "SE-0306" somewhere in the subject line.
What goes into a review?
The goal of the review process is to improve the proposal under review through constructive criticism and, eventually, determine the direction of Swift. When writing your review, here are some questions you might want to answer in your review:
What is your evaluation of the proposal?
Is the problem being addressed significant enough to warrant a change to Swift?
Does this proposal fit well with the feel and direction of Swift?
If you have used other languages or libraries with a similar feature, how do you feel that this proposal compares to those?
How much effort did you put into your review? A glance, a quick reading, or an in-depth study?
I am very fond of and supportive of this feature and technology direction (you could say +100!). However, I do have some concerns about the details involved and am not as supportive of this particular draft. Detailed comments below.
Yes, this is a very important part of the Swift concurrency model. I think this will align very nicely with the long term direction of Swift and take the community and its APIs to the next level.
I have been studying concurrency for a very long time, and am familiar with the actor models in both Erlang and Scala Akka. I wrote the Swift Concurrency Manifesto back in 2017, which inspired many of these directions. I was very actively involved in the pitch reviews phases and have spent a bunch of time involved with the other Swift concurrency proposals as well.
Detailed comments:
Overall
This proposal is one of the bedrock technologies for concurrency in Swift. I think it will differentiate Swift from other languages in a very positive way, and lift large scale application and server design to the next level. Furthermore, Actors provide a strong basis to build reliability and distributed compute features into Swift. I think the complementary way that Actors and Structured Concurrency compose together is very elegant: they make for beautiful duals.
This proposal itself has made a huge amount of progress over the many iterations it has been through so far. I am very thrilled to see that this tackles thread and race safety head on through isolation and building on the @Sendable feature. There is still a large hole in the model with mutable global state, but that is independent of actors (affecting structured concurrency as well) and has been split off to a subsequent proposal (correctly, in my opinion).
I am also very +1 on many details of the proposal, including the use of the actor keyword to introduce actor declarations, allowing sync methods to be used from outside of the actor (implicitly promoting to async accesses) etc. I'm very happy that actors are allowed to participate in the protocol system, generics, etc. I also think that reentrancy is the right default for actors: the possible future extension to support blocking actors make sense to me if we find that we need them in practice, but I'd prefer to avoid the complexity if we don't.
That said, there are a few areas of the proposal that could use more consideration and iteration.
Direct Cross Actor References to let constants should be part of the "nonisolated" proposal
The proposal says:
A reference to an actor-isolated declaration from outside that actor is called a cross-actor reference . Such references are permissible in one of two ways. First, a cross-actor reference to immutable state is allowed because, once initialized, that state can never be modified (either from inside the actor or outside it), so there are no data races by definition. The reference to other.accountNumber is allowed based on this rule, because accountNumber is declared via a let and has value-semantic type Int .
This rule is problematic in two significant ways:
This is inconsistent with the Swift resilience / API evolution model, which doesn't differentiate between let constants and read-only properties. Furthermore, a let constant in a public API is allowed to be upgraded to a var constant in an API, which this design breaks. As proposed, actors will be inconsistent from the rest of the types in Swift, which will have significant ramifications for Swift API design.
This is inconsistent with how actors in other languages work - all cross-actor accesses are supposed to be async and go through the actor's mailbox. This matters because reliability, distribution and other features of actors only work correctly if accesses are mediated through asynchrony (distribution implies I/O, which must be async). The options here are not good: 1) exclude distributed actors from having let properties, 2) make accesses to let-properties in distributed actors block implicitly, 3) make distributed and non-distributed actors have different semantics for let properties.
Fortunately, the solution to this problem appears to be simple:
remove these special rules from this proposal so let properties behave the same way as get-only var properties. This fixes resilience, actor invariants, and consistency.
Allow an opt-in way for let properties to be annotated with a new declaration modifier (e.g. nonisolated), and
I think that fixing this is very important - we want the resilience model to be consistent across the language, and do not want to prevent people from using let declarations in their actors. We should have a simple and consistent programming model for Swift that works across all types wherever possible.
Actor inheritance isn't needed and doesn't seem aligned with the design goals of Swift
The proposal states:
and "alternatives considered" has a section that tries to explain why inheritance is good for actors.
However, the proposal does not fully address what actor inheritance means or how it works. For example:
This means that actors get the full complexity of required, delegating and convenience initializers, method overriding, final, etc.
As the proposal points out "actors and classes cannot be co-mingled in an inheritance hierarchy, so there are essentially two different kinds of type hierarchies" which adds complexity to the ecosystem.
Non-final actors will needlessly have "virtual method" overhead like classes do, encouraging people to sprinkle final around which is ugly boilerplate.
Actors would be subject to much heavier metadata that bloats apps and slows app startup time like classes.
The proposal points out that "actor types can have static and class methods" - shouldn't these be static and actor methods? I don't think either of class func foo() or actor func foo() are going to send the right message.
There are many other aspects to this that are suggested but not defined - can actors be marked @UIApplicationMain?, @requires_stored_property_inits? @objcMembers? Can you mark an actor as a @propertyWrapper? Classes have a lot of legacy from Objective-C which doesn't have to be pulled forward into a shiny new feature.
Swift has two reference semantic types (classes and closures): there is no need to follow the design of classes just because actors have reference semantics.
A first principles analysis of actors shows that they don't need any of this stuff, for the same reason that enums and structs don't need it: Actors can conform to protocols and use Protocol Oriented Programming, and this is the preferred way to implement type abstraction in Swift for a wide range of reasons. I believe that it would be much simpler and cleaner for actors to omit inheritance and be more similar to the structs and classes that are generally favored for non-legacy APIs and design patterns. Actors are an entirely new thing that Objective-C doesn't have, so the legacy considerations that force Swift classes into their design points don't apply.
The proposal has a nice section in "alternatives considered" that tries to explain why inheritance is a good idea. It am glad it does (and I encourage you to read it), but I'll rebut the points it makes one by one:
Actor inheritance makes it easier to port existing class hierarchies to get the benefits of actors. Without actor inheritance, such porting will also have to contend with (e.g.) replacing superclasses with protocols and explicitly-specified stored properties at the same time.
This is a tiny win in the face of a huge problem. Moving existing code to actors will be inherently a huge remodel, not a small refactoring. The interfaces to actors are required to be async, and async is an effect modifier that quickly ripples through your code. You can't just change class to actor and walk away.
Furthermore, there is a way to make incremental refactoring process: incrementally refactor your Swift 5 class to use protocol oriented programming techniques in place first, then adopt actors when you eliminate OOP.
The lack of inheritance in actors won't prevent users from having to understand the complexities of inheritance, because inheritance will still be used pervasively with classes.
Well sure, but we don't have any reason to add more complexity to other parts of the language ;-).
The design and implementation of actors naturally admits inheritance. Actors are fundamentally class-like, the semantics of inheritance follow directly from the need to maintain actor isolation. The implementation of actors is essentially as "special classes", so it supports all of these features out of the box. There is little benefit to the implementation from eliminating the possibility of inheritance of actors.
Sure, but: 1) disabling inheritance is equally easy for the implementation. 2) implementation burden isn't the primary concern of swift-evolution, long term language quality is.
Actor inheritance has similar use cases to class inheritance.
Yep, and such a move would brings both the joys and follies of implementation inheritance and all the abuses of OOP that people find familiar in traditional class-based object oriented systems.
As a final historical note, when building Swift in the first place, we had to decide whether to allow structs (and enums) to inherit from each other. There is no technical obstacle to doing so, and such a move would have made it much easier to import C++ types. However, we decided not to do this because we believed in the power of protocols for type abstraction. So far, I am very very happy we went with a simpler model here and did not infect structs and enums with the complexity of classes. I hope we make a consistent move with Actors.
If we "must" support inheritance, then I'd encourage us to do so with a simpler model that jettisons the class initialization complexity which is only necessary due to legacy Objective-C design patterns that don't apply to actors.
Note that while I am negative on supporting inheritance of actors, I do think it is important for actors to be usable from Objective-C as ObjC objects. The solution to this is to allow the existing @objc attribute on an actor (just as we allow on structs and enums). There is no need to support inheritance, or to support the legacy NSObject methods on actors as the proposal suggest. Such methods would almost certainly be memory unsafe anyway.
The rules for @escaping closures aren't actor-related and don't fix the problems they set out to
The first rule makes sense, and composes directly with SE-0302, but the second rule does not. As the proposal goes on to describe, @escaping and @Sendable are orthogonal features of a closure, and it makes perfect sense for a closure to be @escaping without being @Sendable. As such, I see several concerns about the proposal's suggestion that we conflate their meaning:
This suggestion has nothing to do with actors. The same concerns raised in the proposal (e.g. interaction with legacy APIs like dispatch_async etc) apply to structured concurrency as well. Trying to fix a memory safety issue for actors but not structured concurrency doesn't make sense to me.
There are reasonable closures in APIs that are escaping but not sendable (e.g. all captured callbacks in normal non-concurrent settings!) and this unnecessarily subjects such APIs to @Sendable checking (but again, only within actors?).
This suggestion isn't fully effective, because it doesn't fix memory safety issues with non-@escaping closures: e.g. arguments to a parallelMap algorithm written in Swift 5 which would be non-@escaping. This issue isn't directly correlated with @escaping, and people using these sorts of APIs in Swift 5 code are already dealing with them.
There are lots of other memory safety issues that will exist when working with Swift 5 code and imported C code. Swift has never been memory safe when working with imported languages.
Once this rule is instilled into Swift it will be almost impossible to take it out: this suggestion turns a short term bump into long term language debt.
I recommend that we remove this unprincipled rule and solve the problem in a different way:
The proposal observes that the problem is common in specific idiomatic cases, e.g. completion handlers and well-known APIs like dispatch_async. It seems like a better solution would be to import these as @Sendable closures through minor changes to the Clang importer in Swift, new attributes in Clang for the few C/Obj-C APIs that need them (this is a much MUCH smaller annotation burden than ImplicitlyUnwrappedOptional auditing).
The narrow case left over is legacy imported Swift 5 code. If this was seen as a problem, then it would be possible to import @escaping closures in Swift 5 modules as @Sendable. Working with Swift 5 modules is not going to be memory safe for a bunch of other reasons (e.g. global variables) anyway, and I think that such an approach would encourage packages to upgrade to Swift 6 faster.
It doesn't appear from the proposal that this approach was explored. The major advantage of it is that it provides a simple, pure, and composable model for Swift 6 and beyond, as opposed to being stuck with legacy compatibility stopgap effectively forever.
The Actor protocol is an type eraser, so it seems more natural to follow the established precedent of AnyClass "The protocol to which all class types implicitly conform."
Beyond AnyClass, there is precedent with AnyHashable: "A type-erased hashable value." and the Any protocol composition, AnyObject, etc. The word Any is the unifying term for a "type erased thingy" in Swift regardless of the implementation of that eraser.
I think that aligning with AnyClass is itself enough reason to name it AnyActor .
Overall I think that the introduction of actors into Swift is a hugely positive direction. I look forward to continued iteration on this proposal!
Without belaboring the point, I agree with Chrisās feedback as to his overall appraisal of the proposal and his first three major design critiques.
I do think that, being a protocol, Actor is the appropriate name, since AnyActor implies a manual type-erasing data structure and Actor isnāt that.
I am sad that thereās no way in the declaration of Actor formally to restrict conformance only to actors, and also that in moving away from the : class notation in favor of : AnyObject we are now stuck with the latter meaning both classes and actors, with no natural way of spelling a constraint for actors specifically without creating a protocol. (And it seems unfortunate that AnyClass exists and is a synonym for AnyObject.) Does this mean we need a protocol Class too?
(That said, if we are not to rework this naming scheme more extensively, AnyActor would be more consistent with AnyClass as precedent, however much the latter would be a misnomer.)
Maybe Iām missing something, but isnāt this addressed by the following things and their implications?
protocol Actor exists,
the only way to conform to this protocol is to be an actor, i.e. no class can just conform to that protocol,
One can declare protocol Worker: Actor ,
one can require <W: Worker>, or just <A: Actor>,
And the only way to conform to such protocols is by being an actor. The same would go for distributed ones, which arguably have more interesting properties than just local ones, though thatās not yet pitched...
ā-
Oh wait, Just as I replied I see that you mean the definition of protocol Actor itself not being able to say protocol Actor: āactorā I suppose, right? Yeah, maybe unfortunate but not a big deal tbh IMHO...
I understand that. The point is that this cannot be expressed (i.e., it is unutterable) in the Swift language itself at the point where protocol Actor is declared.
It allows users to express the constraint in turn by declaring : Actor. Naturally, the question isāif I must spell the constraint using a protocol, how does the author of the protocol spell the constraint in turn? And the facade of the language ends there rather abruptly.
One additional justification for omitting actor inheritance from this proposal is that the cost of omission is fairly low. We can always add inheritance later with the only cost being folks would need to write actor class to disambiguate it from the default case. Chris's point about metadata is particularly salient here, which seems to imply that actors-with-inheritance is a nonzero-cost abstraction (since even final class won't save you from the metadata overhead mentioned in @noahsmartin's article).
This is also very interesting. I may be reading a little too far ahead, but I can see a world where we import C++ types as struct class or enum class which has a nice symmetry to the actor class. I'm not arguing that any of these need to be writable in pure Swift, just highlighting that we might need the concept of "X with inheritance" in the future outside of the actor usecase.
I am very fond of the feature itself. Actors are a great choice for solving the problem.
The big dislike I have about this proposal is the support of inheritance for actors. There is no good reason to support inheritance and I think the only valid reason for class inheritance in Swift is backward compatibility. Since actors are a new feature, this is the opportunity to get rid of this legacy.
Since there is no need for an actor to support inheritance, it would be equally valid to support any other language feature that is not needed for an actor - why not multiple inheritance for example? The answer is clear to me: because it adds more complexity than it adds value.
Actors are difficult enough, no need to add complexity without a good reason.
I am generally supportive of this proposal having followed its development in some detail, made comments on earlier drafts that have been addressed in this one. I have some nitpicks still with the current draft, although I realize that we are late into the design process.
I concur with @clattner totally regarding inheritance, sendable, and AnyActor.
sendable itself is a weird syntax. Type: Sendable allows member access across contexts where it woudn't be allowed otherwise, while @sendable as a closure attribute forbids access where that would be allowed otherwise. I get that these opposite meanings fall out of what it means to be "sendable" in each context but IMO we should have two syntaxes for opposite behavior.
I would prefer a solution with both sendable and nonsendable keypaths. I realize the keypath types are multiplying (writeable x sendable x partial...) but IMO we should build some language feature to address that separately. Introducing blue-colored functions that can't use keypaths is a bit like deprecating keypaths but with extra steps.
I am a bit concerned about all the stuff we need to break later. "Ratchet up the safety in future language versions" sounds a lot like breaking working code. @reentrant(task) as default is similar and I could forsee that breakage discouraging the huge effort it would take to design that feature later. Maybe these become like @escaping is now, people wrote their code and now we have to deal with it. In my view it would be a better balance to implement the strictest version and relax over time, with some @syntax or @_syntax to relax now. It's not ideal since the strictest version has some surprises of its own, but it might be preferable to changing concurrency behavior later.
I think the naming convention of AnyClass and AnyObject doesn't make sense to Actor protocol. We use Any prefix to disambiguate with class and object - the most used OOP programming concepts in the world, make it clear that "I'm not the classical class or object, I'm the Protocol which class/object conforms to by default". To me, Object protocol should be the more swifty name which also maps naturally to NSObject like String <-> NSString relationship. But it's too late to change that.
Actor should follow SwiftUI established pattern View -> AnyView -> SomeView to represent Protocol, unTyped and Typed hierarchy. If we use AnyActor as protocol name there's no chance to create actor type eraser any more; furthermore actor is a special keyword/conception in concurrency world, there's NO potential ambiguity here, so Actor protocol is the reasonable intuitive name for all actors conform to.
Actor Inheritance
Actor is a special kind of class which has reference semantic and should maintain all features class have entirely. Inheritance capability is key to class type from all languages which have class conception built-in, swift should not be the exception.
On the other side, if you don't like/need inheritance to actor you can make it final actor like final class, we already have the opt-out option for developers to disable it completely. Just turn it on/off, your choice!
Is it though? On an implementation level, perhaps, but that should not dictate the user model in my opinion. Just because actors can have inheritance doesnāt mean they have to... I donāt want to repeat what Chris already wrote in detail, but I donāt see a compelling reason for supporting inheritance for actors. āItās easy to implementāor āwe already have itā is not convincing to me
The only thing classes and actors have in common is reference semantics. Actors are in no other way like classes unless you add class features - and that is not a valid argument for classes and actors being similar. Actors own their execution environment, classes don't. That is as different as it gets.
Chris arguments against actor inheritance are all valid, and I there is no argument in favor actor inheritance other than: we should add inheritance to classes because actors are like classes because we can add inheritance.
Please, if there is an argument for actor inheritance I would like to hear it. If it is only 'I like inheritance in classes so actors should have it' I do not count that as an argument.
In the proposal and also in my own understanding, actor is a new nominal type independent of class. More evidence and explanation would be preferred if you want to claim that actor is a special kind of class.
The argument for inheritance is around lowering transition costs. Implementations that are closest to the actor pattern and that are the best low hanging fruit candidates to take up Actors quickly when theyāre introduced are currently built with classes. Some of those implementations rely on inheritance. So declining to provide inheritance will increase the amount of rework for those code bases.
That said, I donāt like the tradeoff. This is a core construct that will underpin important APIs potentially for decades. Trading conceptual simplicity for ease and speed of transition is a poor trade IMHO. Iām generally in favor of practicality over purity, but when it comes to something this foundational... Iād prefer the long view. I agree with Chris that we should not provide Actor inheritance and push the ecosystem to use POP patterns with Actors.
NO, that's not the point. The inheritance, whether you like it or not you need it practically in programming, POP is NOT the silver bullet for all scenarios, inheritance still play an important role and bring huge convenience for developers. When you don't need it just make it final - no burden at all, but cutting inheritance off is too much aggressive to me.
Can you provide more examples regarding why we need inheritance (instead of protocols) when using actors?
I don't think the bar for including or not including some functionality should be whether we "need" it or
not. The list of things we have but never "needed" is huge and ever-expanding. Probably the best example would be property wrappers, because it is just a convenient language feature that didn't enable anything entirely new (beyond convenient syntax), we didn't "need" them.
That being said, as it relates to actor inheritance, the things I think we should be considering are whether enabling it would be actively harmful or overly burdensome in some way, and whether those issues outweigh the convenience of inheritance.